Double has_many attributes - ruby-on-rails

I'm beginner in rails and having trouble finding a proper way out with my problem.
I have three models : Conversation, participant, messages which have the following attributes :
Conversation :
module Messenger
class Conversation <ActiveRecord::Base
has_many :participants, :class_name => 'Messenger::Participant'
def messages
self.participants.messages.order(:created_at)
end
end
end
Participant :
module Messenger
class Participant <ActiveRecord::Base
has_many :messages, :class_name => 'Messenger::Message'
belongs_to :conversation, :class_name => 'Messenger::Conversation'
end
end
Message :
module Messenger
class Message <ActiveRecord::Base
default_scope {order(:created_at)}
default_scope {where(deleted: false)}
belongs_to :participant, :class_name => 'Messenger::Participant'
end
end
My trouble is that I'm trying to make a single form to create a conversation with a first message in it. The form looks like this :
= form_for #conversation, url: messenger.conversations_create_path do |f|
.row
.col-md-12.no-padding
.whitebg.padding15
.form-group.user-info-block.required
= f.label :title, t('trad'), class: 'control-label'
= f.text_field :title, class: 'form-control'
.form-group.user-info-block.required
= f.label :model, t('trad'), class: 'control-label'
= f.text_field :model, class: 'form-control'
.form-group.user-info-block.required
= f.label :model_id, t('trad'), class: 'control-label'
= f.text_field :model_id, class: 'form-control'
= fields_for #message, #conversation.participants.message do |m|
= m.label :content, t('trad'), class: 'control-label'
= m.text_area :content, class:'form-control'
.user-info-block.action-buttons
= f.submit t('trad'), :class => 'btn btn-primary pull-right'
I've tried many ways to make this form simple but I've encountered some problems which I don't know how to fix using rails properly.
I've tried using Field_forto include a message in my conversation form, but since I have nothing saved in my database yet it seems I can't link a message to an unexisting participant.
So basically I want my first form, once validated, to create a conversation, link the current user to that conversation and link the message to that first user, but I assume there are ways to do it with the framework and I would not like to do it manually.
What is the proper way to follow to achieve that? Am I even on the good track or shoould I change something or add something?
Edit : to make it more understandable, a participant got a user_id and a conversation_id, which means this is a relation table. I can't adapt the attributes of my models to make it easier since I must keep it in that way for security reasons.

Message needs to belong_to Conversation directly, since you need to disambiguate when participants have more than one conversation.
So having done that, you can build the conversation's default message in the controller using
#conversation.messages.build(participant: #conversation.participants.first)
That's pretty wordy, so you can add a couple of model methods to reduce the controller call to
#conversation.build_default_message
In this case, you want to create a Conversation, but it needs to create a Message with user input as well. So Conversation needs to accept attributes on behalf of Message. You can do that using accepts_nested_attributes_for
class Conversation
accepts_nested_attributes_for :messages
end
This would allow you to create a Conversation with 1 or more associated Messages using
Conversation.create(
...,
messages_attributes: [
{ participant_id: 1, content: 'question' }
]
)

First, in order for your form to accept nested attributes using the fields_for form helper, you need to specify accepts_nested_attributes_for on your Conversation model:
module Messenger
class Conversation <ActiveRecord::Base
has_many :participants, :class_name => 'Messenger::Participant'
# Required for form helper
accepts_nested_attributes_for :participants
[...]
Since you want to save a both Participant as well as its Message from the same form, you need to add a second accepts_nested_attributes_for on your Participant model:
module Messenger
class Participant <ActiveRecord::Base
has_many :messages, :class_name => 'Messenger::Message'
# Required for form helper
accepts_nested_attributes_for :messages
belongs_to :conversation, :class_name => 'Messenger::Conversation'
end
end
Next, in your controller, since this is a new Conversation that doesn't have any Participants at first, you need to build an associated Participant (presumably based on the current_user), and also an associated Message for this new Participant:
def new
#conversation.participants.build(user: current_user).messages.build
end
Finally in your view, specify the attribute fields in three nested blocks, form_for #conversation do |f|, f.fields_for :participants do |p|, and p.fields_for :messages do |m|:
= form_for #conversation, url: messenger.conversations_create_path do |f|
[...]
= f.fields_for :participants do |p|
= p.fields_for :messages do |m|
= m.label :content, t('trad'), class: 'control-label'
= m.text_area :content, class:'form-control'
.user-info-block.action-buttons
= f.submit t('trad'), :class => 'btn btn-primary pull-right'
Side note: the (incorrectly implemented) messages method in Conversation should be replaced by a simple has_many :through relation:
has_many :messages, through: :participants

First of all, I think, you have mistakes in this method:
def messages
self.participants.messages.order(:created_at)
end
since Conversation has_many :participants:
self.participants will return an array, not a single Participant active record object. So you can't directly call messages on an array. You need to iterate that array and call messages on each object.
Use nested form and fields_for method and accepts_nested_attributes_for (you can find how to write these from SO or documentation) and post the code code and error what you got. Then someone might be able to help you.
And to use fields_for:
Since you can't link a message directly to a Conversation and you need a message field, you need to link a #message to any participant. you can build a Participant from current_user or first user i.e User.first for the #conversation and then build a #message for this Participant.

Related

How do I update Rails object when object to update is only determined in the form itself?

I'm looking to update a record but not in the standard Rails way in a form on the same controller. My form works fine there. I have a form in a different controller/view called Shoes.
The problem I'm having is the record a user is updating isn't a static record that can be pulled from the url (I think that's how Rails pulls generically - I'm learning so please don't decimate me for this if I'm not completely right here). When a user clicks an item to add/update to their client_sub_folder, the two collection_select options in the form allow the user to select which client_folder and client_sub_folder they are adding to and subsequently that will determine which client_folder and client_sub_folder the item they are adding will go to/update.
I'm using Devise for authentication on the Designer model.
How do I do this?
My error message:
No route matches {:action=>"show", :client_folder_id=>nil, :controller=>"client_sub_folders", :name_prefix=>"client_folder_"} missing required keys: [:client_folder_id, :id]
I assume some JQuery magic is going to be needed here.
Models:
class Designer < ApplicationRecord
has_many :client_folders, dependent: :destroy
has_many :client_sub_folders, through: :client_folders
accepts_nested_attributes_for :client_folders
accepts_nested_attributes_for :client_sub_folders
end
class ClientFolder < ApplicationRecord
belongs_to :designer
has_many :client_sub_folders, dependent: :destroy
accepts_nested_attributes_for :client_sub_folders
end
class ClientSubFolder < ApplicationRecord
belongs_to :client_folder
end
shoes/index.html.erb
<%= form_for([Designer.current_designer.client_folders,#sub_client_folder], url: client_folder_client_sub_folder_path(#client_sub_folder), method: :patch) do |f| %>
<%= f.collection_select :client_folders, :client_folder_ids, Designer.current_designer.client_folders, :id, :client_name, {include_blank: true}, {id: "get-folder"} %>
<%= f.grouped_collection_select :client_sub_folders, :client_sub_folder_ids, Designer.current_designer.client_folders.order(:client_name), :client_sub_folders, :client_name, :id, :room_name, {include_blank: true}, {id: "get-sub"} %>
<%= f.submit %>
<% end %>

Create parent model from child controller

I'm working on on a web app that takes in information about companies. Information can be taken for a PreferredOffering (of stock) or an Incorporation. So in other words, when I create a new entry to either one of those models, a new company is formed.
It works out that my database is cleaner if PreferredOffering and Incorporation are children of Company, even though I'm trying to go through the preferred_offerings_controller or theincorporations_controller to create a new Company. Here lies my question; I'm trying to figure out how to configure my view and controllers to create a parent model from a child controller. I've done some research and have seen two other S/O posts on how to accomplish this with Rails 3, however it would seem that the addition of strong params adds another layer of complexity to the endeavor.
So I have my models set up like this
class Company < ActiveRecord::Base
belongs_to :user
has_one :incorporation, dependent: :destroy
has_many :preferred_offerings, dependent: :destroy
accepts_nested_attributes_for :preferred_offerings, allow_destroy: true
accepts_nested_attributes_for :incorporation, allow_destroy: true
end
.
class Incorporation < ActiveRecord::Base
belongs_to :company
end
.
class PreferredOffering < ActiveRecord::Base
belongs_to :company
end
The controller and view are what I'm iffy on.
Let's just take a look at the incorporation view/controller. If I were to configure it so that Incorporation has_one :company, I would set it up as follows:
class IncorporationsController < ApplicationController
def index
end
def new
#user=current_user
#incorporation = #user.incorporations.build
#company = #incorporation.build_company
end
def create
#incorporation = current_user.incorporations.build(incorporation_params)
end
private
def incorporation_params
params.require(:incorporation).permit(:title, :trademark_search, :user_id, :employee_stock_options, :submit, :_destroy,
company_attributes: [:id, :name, :employee_stock_options, :options_pool, :state_corp, :street, :city, :state, :zip, :issued_common_stock, :outstanding_common_stock, :fiscal_year_end_month, :fiscal_year_end_day, :user_id, :_destroy]
)
end
end
And the view would be:
<%= simple_form_for #incorporation, html: {id:"incorporationform"}, remote: false, update: { success: "response", failure: "error"} do |f| %>
(incorporation-specific fields)
<%= f.simple_fields_for :company do |company| %>
(Company-specific fields)
<% end %>
<% end %>
So my question is:
How do I need to modify my controller and view to create a Company from the incorporations_controller IF Company has_one :incorporation
Any suggestions would be much appreciated.
While it isn't the "Rails Way", there is nothing really wrong with having #company being the parent in your form, even though it is in the incorporations#new action. Your view would change to this:
<%= simple_form_for #company, html: {id:"companyform"}, remote: false, update: { success: "response", failure: "error"} do |f| %>
(company-specific fields)
<%= f.simple_fields_for :incorporation do |incorporation| %>
(incorporation-specific fields)
<% end %>
<% end %>
And your strong params would change so that Company is the parent and Incorporation is the child.
Another option would be to simply go through the Company controller. You could create two new actions: new_preferred_offering and new_incorporation. You would then create the objects in those actions. Or you could pass in some kind of :type param so that the normal new action renders one of two forms based on which one you want.

Creating a form with nested attributes and prompt (has_many association)

So the issue is i have these classes.
class Post < ActiveRecord::Base
belongs_to :category
has_many :text_fields
has_many :integer_fields
accepts_nested_attributes_for :text_fields
accepts_nested_attributes_for :integer_fields
end
class Category < ActiveRecord::Base
has_many :fields
end
class TextField < ActiveRecord::Base
belongs_to :post
end
class IntegerField < ActiveRecord::Base
belongs_to :post
end
Category has many Fields, every field (not to confuse with TextField or IntegerField) has a column "type" (to specify if it is Text or Integer field) and description (eg. "how many pets do you have" for integerField). When User want to create a new post, first he chooses category. then, based on what Fields each category has, proper TextFields and IntegerFields should be build and rendered.
I hope my point is clear, i want to have Post generator which generate different fields based on category, simple as that.
Post form (without steps for now)
= simple_form_for #poster do |f|
= f.input :category_id, collection: Category.all, prompt: "Choose category"
= f.input :title
= f.simple_fields_for :text_fields do |ftf|
= ftf.input :description
= f.simple_fields_for :integer_fields do |fif|
= fif.input :integer_number
= f.submit
And heres whats wrong. i dont know how to give each field a proper prompt. each TextField or IntegerField is there just because Category requires it (based on fields). But can i make it has a proper prompt, not just 'integer number' every time?
Here's how i build those fields, its open for changes:
def build_fields_based_on_category
category.fields.each do |field|
self.send("#{field.kind}_fields").build
##### 'kind' can be integer or 'text'
end
end

Nested form in active_admin with select or create option

We are using active_admin for our administration backend.
We have a model "App" that :belongs_to model "Publisher":
class App < ActiveRecord::Base
belongs_to :publisher
end
class Publisher < ActiveRecord::Base
has_many :apps
end
When creating a new entry for the "App" model I want to have the option to either select an existing publisher or (if the publisher is not yet created) to create a new publisher in the same (nested) form (or at least without leaving the page).
Is there a way to do this in active_admin?
Here's what we have so far (in admin/app.rb):
form :html => { :enctype => "multipart/form-data" } do |f|
f.inputs do
f.input :title
...
end
f.inputs do
f.semantic_fields_for :publisher do |p| # this is for has_many assocs, right?
p.input :name
end
end
f.buttons
end
After hours of searching, I'd appreciate any hint... Thanks!
First, make sure that in your Publisher model you have the right permissions for the associated object:
class App < ActiveRecord::Base
attr_accessible :publisher_attributes
belongs_to :publisher
accepts_nested_attributes_for :publisher, reject_if: :all_blank
end
Then in your ActiveAdmin file:
form do |f|
f.inputs do
f.input :title
# ...
end
f.inputs do
# Output the collection to select from the existing publishers
f.input :publisher # It's that simple :)
# Then the form to create a new one
f.object.publisher.build # Needed to create the new instance
f.semantic_fields_for :publisher do |p|
p.input :name
end
end
f.buttons
end
I'm using a slightly different setup in my app (a has_and_belongs_to_many relationship instead), but I managed to get it working for me. Let me know if this code outputs any errors.
The form_builder class supports a method called has_many.
f.inputs do
f.has_many :publisher do |p|
p.input :name
end
end
That should do the job.
Update: I re-read your question and this only allows to add a new publisher, I am not sure how to have a select or create though.
According to ActiveAdmin: http://activeadmin.info/docs/5-forms.html
You just need to do as below:
f.input :publisher
I've found you need to do 3 things.
Add semantic fields for the form
f.semantic_fields_for :publisher do |j|
j.input :name
end
Add a nested_belongs_to statement to the controller
controller do
nested_belongs_to :publisher, optional: true
end
Update your permitted parameters on the controller to accept the parameters, using the keyword attributes
permit_params publisher_attributes:[:id, :name]

How can I elegantly construct a form for a model that has a polymorphic association?

Here are my models:
class Lesson < ActiveRecord::Base
belongs_to :topic, :polymorphic => true
validates_presence_of :topic_type, :topic_id
end
class Subject < ActiveRecord::Base
has_many :lessons, :as => :topic
end
class Category < ActiveRecord::Base
has_many :lessons, :as => :topic
end
Now, what I need is a form that will allow the user to create or update Lessons. The questions is, how can I provide a select menu that offers a mix of Subjects and Categories? (To the user, on this particular form, Subjects and Categories are interchangeable, but that's not the case elsewhere.)
Ideally, this would look something like this:
views/lessons/_form.html.haml
= simple_form_for(#lesson) do |f|
= f.input :title
= f.association :topic, :collection => (#subjects + #categories)
That won't work because we'd only be specifying the topic_id, and we need the topic_types as well. But how can we specify those values?
I guess the crux of the problem is that I really want a single select menu that specifies two values corresponding to two different attributes (topic_id and topic_type). Is there any elegant railsy way to do this?
A few notes:
a) Single table inheritance would make this issue go away, but I'd like to avoid this, as Categories and Subjects have their own relationship… I'll spare you the details.
b) I might could pull some javascript shenanigans, yes? But that sounds messy, and if there's a cleaner way to do it, some magic form helper or something, then that's certainly preferable.
c) Though I'm using simple_form, I'm not wedded to it, in case that's complicating matters.
Thanks
If you don't wish to use STI, you can do something similar: create a new model Topic(name:string) which will polymorphically reference Subject or Category.
class Lesson < ActiveRecord::Base
belongs_to :topic
validates_presence_of :topic_id
end
class Topic < ActiveRecord::Base
belongs_to :topicable, :polymorphic => true
end
class Subject < ActiveRecord::Base
has_one :topic, :as => :topicable
has_many :lessons, :through => :topic
accepts_nested_attributes_for :topic
end
class Category < ActiveRecord::Base
has_one :topic, :as => :topicable
has_many :lessons, :through => :topic
accepts_nested_attributes_for :topic
end
In the view where you create a new Subject/Category:
<%= form_for #subject do |subject_form| %>
<%= subject_form.fields_for :topic do |topic_fields| %>
<%= topic_fields.text_field :name %>
<% end %>
<% end %>
After thinking this through, the less dirty implementation IMO would be to hire the JS shenanigans (b):
= f.input_field :topic_type, as: :hidden, class: 'topic_type'
- (#subjects + #categories).each do |topic|
= f.radio_button :topic_id, topic.id, {:'data-type' => topic.class.name, class: 'topic_id'}
With a sprinkle of JS (your needs may vary):
$('input:radio.topic_id').change(function() {
$('input:hidden.topic_type').val($(this).attr('data-type'));
});
Notes:
I use a radio button to select a topic (category or subject) in a list
The class name of each of possible topic is stored in an attribute 'data-type'
When a radio button is selected, the class name is copied to the hidden input via JS
Using: HTML5, jQuery, haml, simple_form

Resources