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:
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>
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.
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.
Related
There are 4 models.(Fashion,FashionUnderwear,Underwear,Brand).Each user can select his own style from the fashion form.The fashion form has various item's drop boxes such as jeans and socks.I wanted to rearrange the items classified by brand in the Drop box alphabetically, so I used the scope.
#### Underwear dropbox
Adidas
adidas underwear 1
adidas underwear 2
NIKE
NIKE underwear 1
NIKE underwear 2
NIKE underwear 3
It worked well with the scope, but now I got a warning that you should include the scope.
In the code below, opening Fashions/new.html will give me a warning "Please Include UnderwearNameAsc".
I studied various things and tried it, but in all cases using child models, I can not find a clue to solve.
### Fashion model
has_one :fashion_underwear
accepts_nested_attributes_for :fashion_underwear
### FashionUnderwear model(Intermediate table)
belongs_to :fashion
belongs_to :underwear
### Underwear model
has_many :fashion_underwears
belongs_to :brand
scope :UnderwearNameAsc, -> { order(UnderwearName: :asc) }
### Brand model
has_many :underwear
has_many :UnderwearNameAsc, -> { order(UnderwearName: :asc) }, class_name: 'Underwear'
### Fashions.controller
def new
#fashion = Fashion.new
#fashion.build_fashion_underwear
#brand = Brand.includes(:fashion_underwears).joins(:fashion_underwears).order(brand_name: :asc)
end
### Fashion/new.html
= simple_form_for(#fashion) do |f|
= f.simple_fields_for :fashion_underwear do |p|
= p.input :underwear_id, collection: #brand, as: :grouped_select, group_method: :UnderwearNameAsc, group_label_method: :brand_name, label_method: :UnderwearName
Bullet is pointing out that you have an n+1 query problem. It suggests that you change your has_many association within the Brand model by adding an includes to solve the problem, like this:
has_many :UnderwearNameAsc, -> { includes(:underwear).order(UnderwearName: :asc) }, class_name: 'Underwear'
While you are at it, you really should change the names of your scopes and methods to snake_case in standard Ruby style. Otherwise Ruby will think those methods and scopes are constants.
Also, I would just name your scope for what it is returning, not for a single attribute on that model. Finally, I'd use a scope separately and call it explicitly when you want the resources to be returned sorted.
has_many :underwear # Or has_many :underwears if Rails interprets the plural that way
scope :sorted, -> { includes(:underwear).order(underwear: {name: :asc} }
Then you can do:
Brand.all # Returns unsorted records
Brand.all.sorted # Returns records sorted by underwear.name
In my application I have models Campaign & Map:
class Campaign < ActiveRecord::Base
has_many :maps, :dependent => :destroy
class Map < ActiveRecord::Base
is_impressionable :counter_cache => true, :unique => :request_hash
belongs_to :campaign, :counter_cache => true, touch: true
In my Map model, I'm using is_impressionable that comes with installing impressionist gem & I have added counter_cache as well that will update my impressions_count for every visit (in my maps table).
In my campaigns#show view, Im trying to add a chart for maps impressions by using morris.js (I have added all needed files in my application and can see the chart).
In my chart, I want to show impressions on campaign.maps, but I am getting wrong data into my chart.
So basically, go to my impressions table, and sum all visits that my campaign.maps has on the giving day.
This is what I have until now:
module CampaignsHelper
def impressions_chart_data_campaign(maps)
(7.days.ago.to_date..Date.today).map do |date|
{
created_at: date,
impressions: Impression.where("date(created_at) =?", date).where(id: maps).size
}
end
end
end
My campaigns#show view:
= content_tag :div, "", id: "impressions_charts_double", data: {impressions: impressions_chart_data_campaign(#campaign.maps)}
:javascript
Morris.Area({
element: 'impressions_charts_double',
data: $('#impressions_charts_double').data('impressions'),
xkey: 'created_at',
ykeys: ['impressions'],
labels: ['Impressions']
});
As mentioned I am getting completely wrong numbers. This is queries in my console:
I'm not sure What Im doing wrong.
This line looks suspect:
impressions: Impression.where("date(created_at) =?", date).where(id: maps).size
The where(id: maps) is selecting on the id column for impressions, whereas you presumably want to be selecting on impressionable_type and impressionable_id?
Rails 4.2.5, Mongoid 5.1.0
I have three models - Mailbox, Communication, and Message.
mailbox.rb
class Mailbox
include Mongoid::Document
belongs_to :user
has_many :communications
end
communication.rb
class Communication
include Mongoid::Document
include Mongoid::Timestamps
include AASM
belongs_to :mailbox
has_and_belongs_to_many :messages, autosave: true
field :read_at, type: DateTime
field :box, type: String
field :touched_at, type: DateTime
field :import_thread_id, type: Integer
scope :inbox, -> { where(:box => 'inbox') }
end
message.rb
class Message
include Mongoid::Document
include Mongoid::Timestamps
attr_accessor :communication_id
has_and_belongs_to_many :communications, autosave: true
belongs_to :from_user, class_name: 'User'
belongs_to :to_user, class_name: 'User'
field :subject, type: String
field :body, type: String
field :sent_at, type: DateTime
end
I'm using the authentication gem devise, which gives access to the current_user helper, which points at the current user logged in.
I have built a query for a controller that satisfied the following conditions:
Get the current_user's mailbox, whose communication's are filtered by the box field, where box == 'inbox'.
It was constructed like this (and is working):
current_user.mailbox.communications.where(:box => 'inbox')
My issue arrises when I try to build upon this query. I wish to chain queries so that I only obtain messages whose last message is not from the current_user. I am aware of the .last method, which returns the most recent record. I have come up with the following query but cannot understand what would need to be adjusted in order to make it work:
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
This query produces the following result:
undefined method 'from_user' for #<Origin::Key:0x007fd2295ff6d8>
I am currently able to accomplish this by doing the following, which I know is very inefficient and want to change immediately:
mb = current_user.mailbox.communications.inbox
comms = mb.reject {|c| c.messages.last.from_user == current_user}
I wish to move this logic from ruby to the actual database query. Thank you in advance to anyone who assists me with this, and please let me know if anymore information is helpful here.
Ok, so what's happening here is kind of messy, and has to do with how smart Mongoid is actually able to be when doing associations.
Specifically how queries are constructed when 'crossing' between two associations.
In the case of your first query:
current_user.mailbox.communications.where(:box => 'inbox')
That's cool with mongoid, because that actually just desugars into really 2 db calls:
Get the current mailbox for the user
Mongoid builds a criteria directly against the communication collection, with a where statement saying: use the mailbox id from item 1, and filter to box = inbox.
Now when we get to your next query,
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
Is when Mongoid starts to be confused.
Here's the main issue: When you use 'where' you are querying the collection you are on. You won't cross associations.
What the where(:messages.last.from_user => {'$ne' => current_user}) is actually doing is not checking the messages association. What Mongoid is actually doing is searching the communication document for a property that would have a JSON path similar to: communication['messages']['last']['from_user'].
Now that you know why, you can get at what you want, but it's going to require a little more sweat than the equivalent ActiveRecord work.
Here's more of the way you can get at what you want:
user_id = current_user.id
communication_ids = current_user.mailbox.communications.where(:box => 'inbox').pluck(:_id)
# We're going to need to work around the fact there is no 'group by' in
# Mongoid, so there's really no way to get the 'last' entry in a set
messages_for_communications = Messages.where(:communications_ids => {"$in" => communications_ids}).pluck(
[:_id, :communications_ids, :from_user_id, :sent_at]
)
# Now that we've got a hash, we need to expand it per-communication,
# And we will throw out communications that don't involve the user
messages_with_communication_ids = messages_for_communications.flat_map do |mesg|
message_set = []
mesg["communications_ids"].each do |c_id|
if communication_ids.include?(c_id)
message_set << ({:id => mesg["_id"],
:communication_id => c_id,
:from_user => mesg["from_user_id"],
:sent_at => mesg["sent_at"]})
end
message_set
end
# Group by communication_id
grouped_messages = messages_with_communication_ids.group_by { |msg| mesg[:communication_id] }
communications_and_message_ids = {}
grouped_messages.each_pair do |k,v|
sorted_messages = v.sort_by { |msg| msg[:sent_at] }
if sorted_messages.last[:from_user] != user_id
communications_and_message_ids[k] = sorted_messages.last[:id]
end
end
# This is now a hash of {:communication_id => :last_message_id}
communications_and_message_ids
I'm not sure my code is 100% (you probably need to check the field names in the documents to make sure I'm searching through the right ones), but I think you get the general pattern.
Is it possible to make a query like this? (Pseudo-code)
u=User.includes(all_delegated_attributes_from_relationships).all
How?
Further explanation:
class User<ActiveRecord::Base
has_one :car
delegate :wheel, :brake, :motor, to: :car, prefix: true
end
and then:
u=User.includes(delegated_car_parts).all
#<user id: 1, car_id: 1, name: "John", car_wheel: "foo", car_motor: "bar", car_brake: false>
I know that this can sound a little odd but I have to add a feature to an old app to export all delegated attributes from a model to CSV and this model has 14 relationships and 300 delegations... I just learnt Demeter's law when I made this app...
Assuming wheel, break and motor are relationships on car, you can do this:
User.includes(:car => [:wheel, :brake, :motor]).all
There is no build-in method to do this. You could try sth like:
class User < ActiveRecord::Base
has_one :car
DelegatedCarMethods = [:wheel, :brake, :motor]
delegate *DelegatedCarMethods, to: :car, prefix: true
scope :with_car_delagations, lambda do
select_array = ["#{table_name}.*"]
select_array += DelegateCarMethods.map {|method| "#{Car.table_name}.#{method} AS car_#{method}"}
select(select_array.join(', ')).joins(:car)
end
end
But it isn't extremely pretty. Why do you need this? Calling user.wheel or user.motor doesn't feel right.
I have two questions:
In Rails 3 you can update multiple records using
Product.update(params[:products].keys, params[:products].values)
How do you do the same thing in Rails 4 using Strong Parameters? How about creating multiple records at the same time? Can you please elaborate your solution with an example in a format like the following:
params = ActionController::Parameters.new(...)
Product.create!(params.require(...)permit(...)
Also, my products model has a column called number which is equal to the order that they are updated. Is there a way to pass a counter value to the number while updating?
Thanks.
Solution w/o accepts_nested_attributes_for
This is my 2nd ever answer on SO, but I came across this problem multiple times, couldn't as of this writing find a reasonable solution and finally figured it out myself after doing a little nested_attributes hypothesizing and wanted to share my best understanding of it. I believe the strong params line you're after is this:
product_params = params.require(:products).permit(product_fields: [:value1, :value2, etc...])
I don't know exactly what your form looks like, but you will need to have a fields_for to nest the params in the product_fields(or any_name):
form_for :products do |f|
f.fields_for product_fields[] do |pf|
pf.select :value1
pf.select :value2
...
end
end
This will allow the permit() to accept a single explicit hash
product_fields => {0 => {value1: 'value', value2: 'value'}}
instead of the key/value pairs
0 => {value1: 'value', value2: 'value'}, 1 => {value1: 'value', value2: 'value'}, etc...
which you'd otherwise have to name individually:
.permit(0 => [value1: 'value', value2: 'value'], 1 => [...], 2 => [...], ad infinitum)
This allows you to update multiple products without having to use a parent model accepting nested attributes. I just tested this on my own project in Rails 4.2 and it worked like a charm. As for creating multiple: How would I save multiple records at once in Rails?.
As for a counter, you may need to iterate over each model individually:
product_params[:product_fields].keys.each_index do |index|
Product.create!(product_params.merge(counter: index))
end
Thought it's been so long you probably resolved that on your own. :-)
Maybe you're thinking about using nested_attributes?
That would look something like this:
params.require(:parent_model).permit(
:id,
child_models_attributes: [
:id,
:parent_model_id,
:child_model_attribute_1,
:child_model_attribute_2
]
)
with
params = {
id: parent_model.id,
child_models_attributes: {
'0' => {
id: child_model_1.id,
parent_model_id: parent_model.id,
child_model_attribute_1: 'value 1',
child_model_attribute_2: 12312
}
}
}
You would need to allow nested_attributes for the parent model like this though:
class ChildModel < Activerecord::Base
belongs_to :parent_model
end
class ParentModel < Activerecord::Base
has_many :child_models
accepts_nested_attributes_for :child_models
end