Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Double has_many attributes

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.

like image 900
RiddlerNewComer Avatar asked Jan 24 '17 10:01

RiddlerNewComer


2 Answers

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' } 
  ]
)
like image 158
fylooi Avatar answered Nov 15 '22 12:11

fylooi


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
like image 36
wjordan Avatar answered Nov 15 '22 14:11

wjordan