Rails validate associated models - ruby-on-rails

I am trying to prevent users from creating duplicate chatrooms (chatroom includes 2 users). But I have no idea how to validate if chatroom with the same users already exists before save.
Thats my create method in chatroom controller:
def create
#chatroom = Chatroom.new
#friend = User.where(username: params[:friend]).last
#chatroom.chatroom_users.build(user: #friend, chatroom: #chatroom)
#chatroom.chatroom_users.build(user: current_user, chatroom: #chatroom)
if #chatroom.save
flash[:notice] = "chatrooom created"
redirect_to #chatroom
else
flash[:notice] = "chatrooom not created lol"
redirect_to authenticated_root_path
end
end
And that is how I am trying to validate if there is no chatroom with 2 users like the new one:
In my class Chatroom
after_save :duplicate?
# Checks if chatroom containing specific users exists.
def duplicate?
user = self.users.first
friend = self.users.second
# lines below work fine - they check if there is already such chatroom
(Chatroom.all - [self]).each do |chatroom|
if ((chatroom.users & [user, friend]) - [user, friend]).empty?
self.errors.add(:chatroom, "Such chatroom already exists.")
end
end
end
The problem is: if I use after_save in validating method I can get the self.users.first to set user and friend variables, but then It does not stop from creating that record and I am not sure If deleting it there is a good idea. Secondly - I use validate instead of after_save self.users.first and self.users.second returns nil, so I can't check for duplicates.
PS: I do not want to have users id as the attributes in the chatrooms table because I want to add ability to connect to chat for as many ppl as you want.

How about something like this?
def duplicate?
is_duplicate = (Chatroom.all.to_a - [self]).any? do |chatroom|
chatroom.users.map(&:id).sort == self.chatroom_users.map(&:user_id).sort
end
if is_duplicate
errors.add(:chatroom, "Such chatroom already exists.")
end
end
Here are all of the models.
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class Chatroom < ApplicationRecord
has_many :chatroom_users, :dependent => :destroy
has_many :users, :through => :chatroom_users
before_validation :duplicate?
def duplicate?
is_duplicate = (Chatroom.all.to_a - [self]).any? do |chatroom|
chatroom.users.map(&:id).sort == self.chatroom_users.map(&:user_id).sort
end
if is_duplicate
errors.add(:chatroom, "Such chatroom already exists.")
end
end
end
class ChatroomUser < ApplicationRecord
belongs_to :chatroom
belongs_to :user
end
class User < ApplicationRecord
has_many :chatroom_users, :dependent => :destroy
has_many :chatrooms, :through => :chatroom_users
end
And here is a test
require 'test_helper'
class ChatroomTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
test 'does not allow duplicate chatrooms' do
first_user = User.create!
second_user = User.create!
chatroom = Chatroom.create!
chatroom.chatroom_users.build(user: first_user, chatroom: chatroom)
chatroom.chatroom_users.build(user: second_user, chatroom: chatroom)
chatroom.save!
duplicate_chatroom = Chatroom.create!
duplicate_chatroom.chatroom_users.build(user: first_user, chatroom: chatroom)
duplicate_chatroom.chatroom_users.build(user: second_user, chatroom: chatroom)
assert !duplicate_chatroom.valid?
end
end
Note: This code is in Rails 5.

Related

Validation in activeadmin with custom controller action

In our web app, a composition has many authors through a table named contributions. We want to check that an admin does not accidentally delete all the authors of one composition in activeadmin (at least one should remain). If this happens, the update fails with an error message and the edit view for a composition is rendered again.
Calling the model validation with
validates_presence_of :authors, on: :update
is not suitable here, because the addition of new contributions (thus authors) is done while calling the success.html on the update function of the activeadmin controller, to prevent some previous bugs that created double entries for authors.
Models are:
class Composition < ApplicationRecord
has_many :contributions
has_many :authors, through: :contributions
end
----
class Contribution < ApplicationRecord
belongs_to :composition
belongs_to :author
end
----
class Author < ApplicationRecord
has_many :author_roles, dependent: :delete_all
has_many :contributions
has_many :compositions, through: :contributions
end
Our code in admin has some background logic to handle what has been described before:
ActiveAdmin.register admin_resource_name = Composition do
...
controller do
def update
author = []
contribution_delete = []
params[:composition][:authors_attributes].each do |number, artist|
if artist[:id].present?
if artist[:_destroy] == "1"
cont_id = Contribution.where(author_id: artist[:id],composition_id: params[:id]).first.id
contribution_delete << cont_id
end
else
names = artist[:full_name_str].strip.split(/ (?=\S+$)/)
first_name = names.size == 1 ? '' : names.first
exist_author = Author.where(first_name: first_name, last_name: names.last, author_type: artist[:author_type]).first
author << exist_author.id if exist_author.present?
end
end if params[:composition][:authors_attributes] != nil
params[:composition].delete :authors_attributes
update! do |success, failure|
success.html do
if author.present?
author.each do |id|
Contribution.create(author_id: id, composition_id: params[:id])
end
end
if contribution_delete.present?
contribution_delete.each do |id|
Contribution.find(id).destroy
end
end
...
redirect_to admin_composition_path(#composition.id)
end
failure.html do
render :edit
end
end
end
end
...
end
Do you have any idea how I can control the authors_attributes and throw a flash message like "There must be at least one author" if the number of to-be-deleted authors is equal to the number of existing authors?
I thought maybe it's possible to handle this before the update! call so to convert the success into a failure somehow, but I have no idea how.

Accessing additional values on has_many through Rails

I am having real trouble accessing the value of an additional parameter called permission on a has_many through. It is probably something simple.
My 3 Models are
class User < ActiveRecord::Base
has_many :players_users
has_many :players, through: :players_users
end
class Player < ActiveRecord::Base
has_many :players_users
has_many :users, through: :players_users
end
class PlayersUser < ActiveRecord::Base
belongs_to :user
belongs_to :player
validates :player_id, uniqueness: { scope: :user_id }
end
My controller saves the record without issue. Adding the permission value to the correct joining table.
def create
#players = Player.new(players_params)
#user= current_user
if #players.save
#player = Player.last
#user.save && #user.players_users.create(:player_id =>#player.id, :permission =>"owner")
redirect_to '/players'
else
render 'new'
end
end
However I seem unable to access it properly
I have tried
perm = User.find(current_user).players_users.includes(:Permission)
if perm == "owner"
Which gives an ActiveRecord::AssociationNotFoundError, association named 'Permission' was not found on PlayersUser; perhaps you misspelled it?
I have also tried
perm = User.players_users.where(player_id = #player.id && user_id = current_user)
perm.permission
or
perm = User.Player.where(player_id = #player.id && user_id = current_user)
or
perm = User.players.where(player_id = #player.id && user_id = current_user)
Which gives an undefined method error.
undefined method `Player'
I know this is something small in my setup but cannot figure out what it is. Any help appreciated.
players_users and players are associated with User object, so you can fetch the results as,
current_user.players_users.pluck(:permission)
I've solved this issue before with my own code.
I'll post that in a second, but first you need to refactor your controller, the current is inefficient:
#app/controllers/players_controller.rb
class PlayersController < ApplicationController
def create
#player = current_user.players.new players_params
if #player.save
current_user.player_users.find(#player.id).update(permission: "owner")
redirect_to players_path
end
end
private
def player_params
params.require(:player).permit(....)
end
end
To access the permission of the player_user, you'll need to use the following:
#permission = current_user.player_users.find(#player.id).permission
--
A much more DRY approach (if you're explicitly setting permission as owner each time) would be to introduce an enum into the Player model to act as a default:
#app/models/player.rb
class Player < ActiveRecord::Base
enum permission: [:owner, :member, :moderator] #-> defaults to :owner
end
This will do away with having to define the permission in the create method (unless of course you want to change it):
#app/controllers/players_controller.rb
class PlayersController < ApplicationController
def create
#player = current_user.players.new players_params
redirect_to players_path if #player.save
end
end
To understand this, you must remember that since player_users is a join association, ActiveRecord will automatically populate it if you create a player on the current_user object (current_user.players).
Association Extensions
In regard to pulling the permission data, I built a script which appends the permission to the player object (uses proxy_association.target etc):
#current
#player = current_user.players.find params[:player_id]
#permission = current_user.player_users.find(params[:player_id]).permission
#script
#player = current_user.players.find params[:player_id]
#permission = #player.permission
It works similarly to an SQL Alias column - so whilst you cannot manipulate the data, it will allow you to call #user.players.find(params[:player_id].permission.. except it's all done in memory:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :player_users
has_many :players, through: :player_users, -> { extending PlayerPermission }
end
#app/models/concerns/player_permission.rb
module PlayerPermission
#Load
def load
permissions.each do |permission|
proxy_association.target << permission
end
end
#Private
private
#Permissions
def permissions
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({permission: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:permission)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
--
As an aside, convention is to keep all aspects of a model name singular (ProductUser).

Creation of data in a :through relation

I'll ask my question first:
Will this code logically work and it is the right thing to do (best practices perspective)? First off, it looks strange having a user being passed to a static subscription method
User and magazine have a many to many relationship through subscriptions (defined below). Also you can see, I've used through joins instead of the has and belongs to many so that we can define a subscription model.
after creating a user they need to have default subscriptions. Following the single responsibility principle, I don't think a user should have to know what default magazines to subscribe to. So how, after a user has been created can I create default subscriptions. The user.likes_sports? user.likes_music? should define which subscriptions methods we want.
Am I on the right track? I don't have anyone to review my code, any code suggestions highly appreciated.
class User < ActiveRecord::Base
after_create create_default_subscriptions
has_many :magazines, :through => :subscriptions
has_many :subscriptions
def create_default_subscriptions
if self.likes_sports?
Subscription.create_sports_subscription(self)
end
end
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :magazine
#status field defined in migration
def self.create_sports_subscription(user)
Magazine.where("category = 'sports'").find_each do |magazine|
user.subscriptions << Subscription.create(:user => user, :magazine => magazine, :status=>"not delivered")
end
end
.
.
end
class Magazine < ActiveRecord::Base
has_many :users, :through => :subscriptions
has_many :subscriptions
end
The code is too coupled in my view. This can get out of hand really easily.
The right way to do this in my view would be to create a new service/form that takes care of creating the user for you
class UserCreationService
def perform
begin
create_user
# we should change this to only rescue exceptions like: ActiveRecord::RecordInvalid or so.
rescue => e
false
end
end
private
def create_user
user = nil
# wrapping all in a transaction makes the code faster
# if any of the steps fail, the whole user creation will fail
User.transaction do
user = User.create
create_subscriptions!(user)
end
user
end
def create_subscriptions!(user)
# your logic here
end
end
Then call the code in your controller like so:
def create
#user = UserCreationService.new.perform
if #user
redirect_to root_path, notice: "success"
else
redirect_to root_path, notice: "erererererooooor"
end
end

Get the owner of an unsaved HABTM collection proxy in ActiveRecord?

Lets say I have two ActiveRecord classes:
class Student < ActiveRecord::Base
has_and_belongs_to_many :guardians
end
class Guardian < ActiveRecord::Base
has_and_belongs_to_many :students, inverse_of: :guardian
before_validation :generate_username, on: :create
def generate_username
count = students.count
self.username = 'g'+students.first.admission_no
end
end
And the create function of my GuardiansController:
def create
#student = Student.find(params[:student_id])
#guardian = #student.guardians.build(guardian_params)
flash.now[:success] = 'Guardian was successfully created.' if #guardian.save
respond_with(#guardian)
end
The generate_username function of the Guardian ActiveRecord Class fails because students returns an empty Association CollectionProxy.
How can I get the owner (student) of a Guardian before it is saved??

Rails: How to set up modelĀ“s attribute on initialize

I am trying to set up an attribute of a model as soon as it is initialised. I have tried before_initialise, after_initialise, before_create, after_create.
What am I doing wrong?
describe "turn based system" do
before(:each) do
#user1 = Factory :user
#user2 = Factory :user
#rota = #user1.rotas.create
#rota.users << #user2
end
it "should be user1's turn at the beginning" do
#rota.current_turn.should == #user1.id --> fails
end
Implementation:
class Rota < ActiveRecord::Base
has_many :rotazations
has_many :users, :through => :rotazations
has_many :invitations
after_create :set_current_turn
private
def set_current_turn
if self.users.count > 0
self.current_turn = self.users[0].id
end
end
end
EDIT
If I construct the rota model using:
#rota = #user1.rotas.create(:current_turn => #user1.id)
it works - however, I feel like im duplicating code - I always want the current_turn field to be the object creator's id.
Thanks!
You have a typo: change before_filter to after_create
1) before_filter is the controller callback function.
2) You want to use after_create so the user has already been associated and you can use the association.

Resources