How to design a database for police fines?

176 Views Asked by At

Using Rails 6 I am designing an application to manage police fines. A user can violate many articles, an article can have many letters and a letter can have many commas.

This is my implementation:

#models/fine.rb
class Fine < ApplicationRecord
  has_many :violations
  has_many :articles, through: :violations
  has_many :letters, through: :violations
  has_many :commas, through: :violations
end
#models/article.rb
class Article < ApplicationRecord
  has_many :letters
  has_many :violations
  has_many :fines, through: :violations
end
#models/letter.rb
class Letter < ApplicationRecord
  belongs_to :article
  has_many :commas

  has_many :violations
  has_many :fines, through: :violations

end
#models/comma.rb
class Comma < ApplicationRecord
  belongs_to :letter

  has_many :violations
  has_many :fines, through: :violations
end
#models/violation.rb
class Violation < ApplicationRecord
  belongs_to :fine

  belongs_to :article
  belongs_to :letter, optional: true
  belongs_to :comma, optional: true
end

When I print the fine in PDF I need to show violations: articles, letters and commas. I have difficulty creating a form to compile the fine because it is too deep. I am using Active Admin, when I create a new fine I want to associate many violations.

Violation example:

Violation.new
 => #<Violation id: nil, article_id: nil, fine_id: nil, letter_id: nil, comma_id: nil, note: nil, created_at: nil, updated_at: nil> 

How can I create a form (using Active Admin, which uses Formtastic) to associate many violations to a fine? Example form:

enter image description here

Example (with sample data):

Violation.new fine: fine, article: a, letter: a.letters.last, comma: a.letters.second.commas.last
 => #<Violation id: nil, article_id: 124, fine_id: 66, letter_id: 10, comma_id: 4, note: nil, created_at: nil, updated_at: nil> 
2

There are 2 best solutions below

0
user1066183 On BEST ANSWER

Solved:

  f.has_many :violations do |vf|
    vf.input :article, as: :select, include_blank: false, collection: options_for_select(Article.all.map {|article| [article.number, article.id, { :'data-article-id' => article.id, :'data-letters' => article.letters.map(&:id).to_json }]})
    vf.input :letter, as: :select, collection: options_for_select(Letter.all.map {|letter| [letter.letter, letter.id, { :'hidden' => true, :'data-letter-id' => letter.id, :'data-article-id' => letter.article.id, :'data-commas' => letter.commas.map(&:id).to_json }]})
    vf.input :comma, as: :select, collection: options_for_select(Comma.all.map {|comma| [comma.number, comma.id, { :'hidden' => true, :'data-comma-id' => comma.id, :'data-letter-id' => comma.letter.id }]})
  end

And with a bit of javascript:

$(document).on('has_many_add:after', '.has_many_container', function (e, fieldset, container) {
    selects = fieldset.find('select');
    article_select = selects[0];
    letter_select = selects[1];
    comma_select = selects[2];

    $(article_select).on("change", function () {
        $(letter_select).prop('selectedIndex', 0);
        $(comma_select).prop('selectedIndex', 0);
        $("#" + letter_select.id + " option").prop("hidden", true);
        $("#" + comma_select.id + " option").prop("hidden", true);

        letters = $(this).find(':selected').data('letters');

        $.each(letters, function (index, id) {
            $("#" + letter_select.id + " option[data-letter-id='" + id + "']").removeAttr("hidden");
        });
    });

    $(letter_select).on("change", function () {
        $(comma_select).prop('selectedIndex', 0);
        $("#" + comma_select.id + " option").prop("hidden", true);

        commas = $(this).find(':selected').data('commas');

        $.each(commas, function (index, id) {
            $("#" + comma_select.id + " option[data-comma-id='" + id + "']").removeAttr("hidden");

        });
    });
});

I show all Articles, Letters and Commas in the selectbox. Initially Commas and Letters are hidden, then when a User click an Article the Letter's selectbox show only the related Letters. The Commas code works same as Letters. After I can add some validations in the Violation model.

0
pfac On

In my humble opinion, your question is rather vague and difficult to answer based only on the provided information. Since I can't produce an answer that will definitely solve your issue, allow me to try and point you in the right direction.

Rendering the form

First let's understand the problem here: you're trying to create an association record in a nested resource form.

You need to customize the form for Fine to include a form for each violation. Look at how ActiveAdmin handles nested resources. It should be something like:

ActiveAdmin.register Fine do
  form do |f|
    inputs 'Violations' do
      f.has_many :violations do |vf|
        vf.input :article, as: :select, collection: Article.all
        vf.input :letter, as: :select, collection: Letter.all
        vf.input :comma, as: :select, collection: Comma.all
      end
    end
  end
end

Put simply, this is the answer to your question "How can I create a form (using Active Admin, who use Formtastic) to associate many violations to a Fine?".

Caveats

As you probably already noticed, there are a couple of problems with this approach.

First, it is nothing like your example. You can easily change things for Formtastic to add the check-boxes by using as: :check_boxes, but you'll find the check-boxes are not organized as you want with that pretty indentation. As far as I know, there is no way for you to do this with Formtastic. Instead, I believe you would have to use a partial.

Using a partial you can easily go through the articles, and render a check-box for each of them and go through each one's letters, and so on. However, bear in mind this form will require you to customize the controller so it understands each of these check-boxes and creates the respective violations. Not as straight forward.

Second, there is nothing enforcing the data integrity here. One could select an article, the letter of another one, and the comma of a third one (by the way, I hope you have a validation to protect you from this). To have the form dynamically change, so only the letters of a given article are shown after its selection, and same thing for the commas, would require some client-side logic. Not worth the trouble if you ask me.

Conclusion

Your question is far from simple and obvious, both to answer and to solve. One option you always have is a custom set of routes for managing such resources outside ActiveAdmin. Remember, tools like this are only as valuable as the work they take from you. If you're having to fight it, better to just step out of each other's way.

Hope this helps, in any way.