Double has_many attribute

I am starting to work on rails and cannot find the right way out with my problem.

I have three models: conversation, participant, messages that 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 problem is that I am trying to create a single form for creating a conversation with the first message in it. The form is as follows:

 = 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 tried many ways to make this form simple, but I ran into some problems that I don’t know how to properly fix using rails.

I tried using Field_for to include a message in my conversation form, but since I didn’t save anything in my database, it seems to me that I cannot associate the message with an unused participant.

So basically, I want my first form, after validation, to create a conversation, associate the current user with this conversation, and associate a message with this first user, but I assume there are ways to do this with the framework, and I would not like do it manually.

What is the right way to achieve this? Am I even on a good track or do I need to change something or add something?

Edit: to make it more understandable, the participant received user_id and chat_id, which means it is a relationship table. I cannot adapt the attributes of my models to simplify them, since I have to keep this in line with security considerations.

+7
ruby-on-rails activerecord ruby-on-rails-4
source share
3 answers

First , in order for your form to accept nested attributes using the fields_for auxiliary form element, you must specify accepts_nested_attributes_for in 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 both Participant and your Message from the same form, you need to add a second accepts_nested_attributes_for to 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 

Further , in your controller, since this is a new Conversation that does not have Participant at the beginning, you need to build related Participant (presumably based on current_user ), as well as a related 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 the 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' 

Lateral note: the (incorrectly implemented) messages method in Conversation should be replaced with a simple has_many :through relation:

 has_many :messages, through: :participants 
+1
source share

The message should belong_to Conversation directly, since you need to disambiguate when participants have more than one conversation.

This way you can create a default conversation message in the controller using

 @conversation.messages.build(participant: @conversation.participants.first) 

This is pretty verbose, so you can add some model methods to reduce the controller call by

 @conversation.build_default_message 

In this case, you want to create a dialog, but it also needs to create a message with user input. Therefore, the conversation must accept attributes on behalf of Message. You can do this with accepts_nested_attributes_for

 class Conversation accepts_nested_attributes_for :messages end 

This will allow you to create a conversation with 1 or more related messages using

 Conversation.create( ..., messages_attributes: [ { participant_id: 1, content: 'question' } ] ) 
+2
source share

First of all, I think you have errors in this method:

 def messages self.participants.messages.order(:created_at) end 

since Conversation has_many :participants :

self.participants will return an array, not one active Participant record object. Therefore, you cannot directly call messages in an array. You need to repeat this array and call messages for each object.

Use the nested form method and fields_for and accepts_nested_attributes_for (you can find how to write them from SO or the documentation) and post the code and error you received. Then someone can help you.

And use fields_for :

Since you cannot associate a message directly with Conversation , and you need a message box, you need to associate @message with any member. you can create a Participant from current_user or the first user ie User.first for @conversation , and then build @message for that Participant .

0
source share

All Articles