Setting a parameter on a join table in rails-admin - ruby-on-rails

I have a model:
class Delivery < ActiveRecord::Base
has_and_belongs_to_many :dropoff_points, :join_table => :delivery_dropoff_points
end
This model has a field scheduled_date.
This is a DeliveryDropoffPoint class:
class DeliveryDropoffPoint < ActiveRecord::Base
belongs_to :delivery
belongs_to :dropoff_point
end
I need to make sure that when I create a record in rails-admin on any given day the dropoff point gets one and only one delivery.
I tried to implement before_save filter:
class DeliveryDropoffPoint < ActiveRecord::Base
belongs_to :delivery
belongs_to :dropoff_point
def set_scheduled_date
self.scheduled_date = delivery.scheduled_date
end
before_save :set_scheduled_date
validates_uniqueness_of :branch_floor, :scope => :scheduled_date, :message => "floor can only have one job assigned on a given day"
end
However, rails-admin seemed to ignore it, regretfully. Is there any other way to set the value of a column in a join table when selecting the items in the select-box in rails-admin?
Thanks!

Related

A small puzzle of Rails Associations

There are 2 tables. One is User(id, name, email) and the other is Student(id, who_id).
I wanna Use this way:
Student.find(id).name, Student.find(id).email
rather than:
User.find(student.who_id).name, User.find(student.who_id).email
to get data.
How should I do?
btw, I cannot change who_id to user_id for any reason.
class User < ActiveRecord::Base
end
class Student < ActiveRecord::Base
end
You can add name and email methods in your Student model, like so:
class Student < ActiveRecord::Base
belongs_to :user, class_name: :User, foreign_key: 'who_id'
def name
user.name
end
def email
user.email
end
end
You could also use Rail's delegate method to do the same thing in less code:
class Student < ActiveRecord::Base
belongs_to :user, class_name: :User, foreign_key: 'who_id'
delegate :name, to: :user
delegate :email, to: :user
end
And once you ge tthat working, rather than Student.find(id).name, Student.find(id).email (which will fetch the data from the database twice) you should instead do this:
student = Student.find(id) #single call to the database
# get the properties from the previous database call
student.name
student.email

Saving Rails associations after creating User

I'm new to Rails and ActiveRecord and need some help. Basically, I have 4 models: User, Property, PropertyAccount, and AccountInvitation. Users and Properties have a many to many relationship via PropertyAccounts. AccountInvitations have a user's email and a property_id.
What I want to happen is that after a user registers on my app, his user account is automatically associated with some pre-created Properties. What I don't know how to do is write the query to get the Property objects from the AccountInvitations and save them to the User object. Please see def assign_properties for my pseudo code. Any help is welcome, thanks so much!
class User < ActiveRecord::Base
has_many :property_accounts
has_many :properties, through: :property_accounts
after_create :assign_properties
# Check to see if user has any pre-assigned properties, and if so assign them
def assign_properties
account_invitations = AccountInvitations.where(email: self.email)
if account_invitations.any?
account_invitations.each do |i|
properties += Property.find(i.property_id)
end
self.properties = properties
self.save
end
end
end
class AccountInvitation < ActiveRecord::Base
belongs_to :property
validates :property_id, presence: true
validates :email, uniqueness: {scope: :property_id}
end
class Property < ActiveRecord::Base
has_many :account_invitations
has_many :property_accounts
has_many :users, through: :property_accounts
end
class PropertyAccount < ActiveRecord::Base
belongs_to :property
belongs_to :user
end
Thanks to #wangthony , I looked at the includes method on http://apidock.com/rails/ActiveRecord/QueryMethods/includes and tweaked one of their examples in order to get this to work. Here's the solution:
def assign_property
self.properties = Property.includes(:account_invitations).where('account_invitations.email = ?', self.email).references(:account_invitations)
self.save
end
I believe you can do this:
user.properties = Property.includes(:account_invitations).where(email: user.email)
user.save

Don't build another association if one already exists

I have some problems getting a nested form working. In the below example I have a User that can define multiple custom labels for a post. The user should be able to enter a value for each particular label.
So one Post can have multiple labels, but should only have one value for each label! In example: Post can have a label named "Date" and also have a label named "Mood". For both labels there should be just one value.
The problem is when a User creates a Label -let say "Date"- it should only be possible to enter one value for this post for this particular label. So if a value for date is given, the form shouldn't build another field for date again. (the post already has a date)
User creates custom labels (this works)
On the edit page of the Post, User sees the labels he created in step 1 (this works)
User can enter a value for each of the Label (here is the problem)
I have the following models:
class User < ActiveRecord::Base
has_many :posts
has_many :labels
end
class Post < ActiveRecord::Base
has_many :label_values
belongs_to :user
accepts_nested_attributes_for :label_values, allow_destroy: true
end
class Label < ActiveRecord::Base
has_many :label_values
belongs_to :user
end
class LabelValue < ActiveRecord::Base
belongs_to :post
belongs_to :label
end
In my controller I have
class PostsController < ApplicationController
def edit
#labels = current_user.labels.all
#post.label_values.build
end
end
My form:
= simple_form_for #post do |f|
=f.fields_for :label_values do |s|
=s.association :label, :include_blank => false
=s.input :value
= f.button :submit
Like this, every time a User enters a value for a particular label, the next time a new label_value is build again and that is not what I want. For each label the User should be able to enter one value.
If you intend to use Label to attach metadata to Post you may want to consider using a relationship somewhat like the following:
class User < ActiveRecord::Base
has_many :user_labels # Labels which have been used by this user.
has_many :labels, through: :user_labels
has_many :posts
end
class Post < ActiveRecord::Base
has_many :post_labels
belongs_to :user, as: :author
has_many :labels, through: :post_labels
accepts_nested_attributes_for :post_labels
def available_labels
Label.where.not(id: labels.pluck(:label_id))
end
end
class Label < ActiveRecord::Base
# #attribute name [String] The name of the label
has_many :user_labels
has_many :post_labels
has_many :users, through: :user_labels
has_many :posts, through: :post_labels
validates_uniqueness_of :name
end
# Used to store label values that are attached to a post
class PostLabel < ActiveRecord::Base
belongs_to :post
belongs_to :label
validates_uniqueness_of :label, scope: :post # only one label per post
# #attribute value [String] the value of the label attached to a post
end
# Used for something like displaying the most used labels by a user
class UserLabel < ActiveRecord::Base
belongs_to :user
belongs_to :label
end
In this setup labels act somewhat like "tags" on Stackoverflow. Each separate label is unique in the system but may be attached to many posts - by many users.
Note the validates_uniqueness_of :label, scope: :post validation which ensures this.
The actuals values attached to a tag are stored on the value attribute of PostLabel. One major drawback to this approach is that you are really limited on by type of the PostLabel value column and may have to do typecasting. I really would not use it for dates as in your example as it will be difficult to do a query based on date.
def PostController
def new
#available_labels = Label.all()
end
def edit
#post = Post.joins(:labels).find(params[:id])
#available_labels = #post.available_labels
end
def create
#post = Post.new(create_params)
if (#post.save)
#post.labels.each do |l|
#post.user.labels << l
end
# ...
else
# ...
end
end
private
def create_params
params.permit(:post).allow(:post_labels)
end
end
Added
This is simply some opinionated recommendations:
To add new labels to a post I would add a text input below the already assigned labels.
I would autocomplete using GET /labels. and filter against the inputs already present in the form. You can allow users to create new labels on the fly by doing an ajax request to POST /labels.
I finally came to something. In the Post model I created:
def empty_labels
Label.where.not(:id => labelValue.select(:label_id).uniq)
end
def used_simfields
LabelValue.where(post_id: id)
end
Then in the view I did:
= simple_form_for #post do |f|
-#post.used_labels.each do |used_label|
=f.fields_for :label_values, used_label do |old_label|
=old_label :value
-#post.empty_labels.each do |empty_label|
=empty_label.fields_for :label_values, #post.label_values.new do |new_label|
=new_label.association :label
=new_label.input :value
I am very sure there are nicer ways to achieve this, so new ideas are very welcome.

Possible to specify what fields a belongs_to or has_many relationship object(s) return(s)?

A Contact has a User assigned to them:
class Contact < ActiveRecord::Base
...
belongs_to :user
...
end
The user model has a field I want to exclude any time a user object or objects are returned from db. One of the ways to make it work is to add a default scope:
class User < ActiveRecord::Base
...
has_many :contacts
...
default_scope select((column_names - ['encrypted_password']).map { |column_name| "`#{table_name}`.`#{column_name}`"})
end
So in console if I do:
User.first
The select statement and result set do not include 'encrypted_password'.
However, if I do:
c = Contact.includes(:user).first
c.user
they do. The default scope on the User model does not get applied in this case and the 'encrypted_password' field is shown.
So my question is why? And also, is there a clean way to specify what fields should be returned on related object(s)?
You should just be able to use the :select option on the belongs_to relationship. Something like this:
class Contact < ActiveRecord::Base
...
belongs_to :user, :select => [:id, :first_name, :last_name, :email]
...
end

Using ActiveRecord belongs_to with two keys

I have two ActiveRecord models with a hasMany / belongsTo association:
class User < ActiveRecord::Base
has_many :letters
end
class Letter < ActiveRecord::Base
belongs_to :user
end
The User model has a revision_number attribute, to which I would like to scope the belongs_to association, so the letter is associated to a User by both user.id and user.revision_number.
I tried using the :conditions key as documented in the API docs:
class Letter < ActiveRecord::Base
belongs_to :user, :conditions => "revision_number = #{client_revision}"
end
but this attempts to call client-revision on the Letter class, not the instance of Letter. Could anyone point me in the right direction for scoping the belongs_to association correctly?
I'm using the acts-as-revisable plugin to version the User model.
I am having a hard time understanding why you would want to scope the belongs_to in this way. Correct me if I am wrong, but it might be better to do something like this. I am assuming you want some sort of version control system:
class User < ActiveRecord::Base
has_many :letters
end
class Letter < ActiveRecord::Base
has_many :revisions, :class_name => "LetterVersion"
belongs_to :current, :class_name => "LetterVersion"
end
class LetterVersion < ActiveRecord::Base
belongs_to :letter
end
Finally figured out what I needed was something like composite keys, which Rails ActiveRecord doesn't support. The solution (for now at least) was to write custom client accessors on the Letter to support the composite keys (id and revision_number):
class Letter < ActiveRecord::Base
def client
Client.find_by_id(self.client_id).try(:find_revision, self.client_revision)
end
def client=(c)
self.client_id = c.id
self.client_revision = c.revision_number
end
end
class Client < ActiveRecord::Base
acts_as_revisable
has_many :letters
end
With this setup, Client#1.letters will retrieve an array of both letters, but Letter#2.client will retrieve Client#1r2, whilst Letter#2.client will retrieve Client#1r4:
Client id: 1 1 1 1 1 1
rev_number: 1 2 3 4 5 6
Letter id: 1 2
client_id: 1 1
client_revision: 2 5
Still not sure if this is the best approach to this problem, but it seems to work for now.

Resources