Validation in activeadmin with custom controller action - ruby-on-rails

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.

Related

Rails 5 dependent: :destroy doesn't work

I have the following classes:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
puts "Product Destroy!"
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
puts "Category Destroy!"
end
end
Here, I am trying to override the destroy method where I eventually want to do this:
update_attribute(:deleted_at, Time.now)
When I run the following statement in Rails console: ProductCategory.destroy_all I get the following output
Category Destroy!
Category Destroy!
Category Destroy!
Note: I have three categories and each category has more than one Products. I can confirm it by ProductCategory.find(1).products, which returns an array of products. I have heard the implementation is changed in Rails 5. Any points on how I can get this to work?
EDIT
What I eventually want is, to soft delete a category and all associated products in one go. Is this possible? Or will ave to iterate on every Product object in a before destroy callback? (Last option for me)
You should call super from your destroy method:
def destroy
super
puts "Category destroy"
end
But I definitely wouldn't suggest that you overide active model methods.
So this is how I did it in the end:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
# run all callback around the destory method
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
I am returning true from the destroy does make update_attribute a little dangerous but I am catching exceptions at the ApplicationController level as well, so works well for us.

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

Ruby on Rails - redirect_to the next video that is not marked as completed

How can I redirect to the next lesson that does not have userLesson (problem is lessons belongs to a course through a chapter)
Models:
class Course
has_many :lessons, through: :chapters
end
class Lesson
belongs_to :chapter
has_one :lecture, through: :chapter
end
class User
has_many :user_lessons
end
class UserLesson
#fields: user_id, lesson_id, completed(boolean)
belongs_to :user
belongs_to :lesson
end
class Chapter
has_many :lessons
belongs_to :lecture
end
here user_lessons_controller:
class UserLessonsController < ApplicationController
before_filter :set_user_and_lesson
def create
#user_lesson = UserLession.create(user_id: #user.id, lession_id: #lesson.id, completed: true)
if #user_lesson.save
# redirect_to appropriate location
else
# take the appropriate action
end
end
end
I want to redirect_to the next lesson that has not the UserLesson when saved. I have no idea how to do it as it belongs_to a chapter. Please help! Could you please help me with the query to write...
Here is the answer for your question:
Inside your user_lessons_controller:
def create
#user_lesson = UserLession.create(user_id: #user.id, lession_id: #lesson.id, completed: true)
if #user_lesson.save
#You have to determine the next_lesson object you want to redirect to
#ex : next_lessons = current_user.user_lessons.where(completed: false)
#This will return an array of active record UserLesson objects.
#depending on which next_lesson you want, you can add more conditions in `where`.
#Say you want the first element of next_lessons array. Do
##next_lesson = next_lessons.first
#after this, do:
#redirect_to #next_lesson
else
#redirect to index?? if so, add an index method in the same controller
end
end
This code will only work if you define show method in your UserLessonsController and add a show.html in your views.
Also, in config/routes.rb, add this line : resources :user_lessons.

Ruby on Rails relationship model

In Ruby on Rails 4, how do you create a many-to-many relationship inside a relationship model for a friends list such as Facebook using the has_many :through ... syntax ?? I'm a newbie and currently learning Ruby on Rails 4. I have looked at this link.
But still have a hard time grasping it.
you will need a join table that references both sides of the relations
let us say you have an relation Post and another relation Category with a many to many relationship between them you need a join table to be able to represent the relationship.
migration for a join table would be
class CreateCategoriesPosts < ActiveRecord::Migration
def change
create_table :categories_posts do |t|
t.integer :category_id
t.integer :post_id
t.timestamps
end
add_index :categories_posts, [:category_id, :post_id]
end
end
and in the models/post.rb
Class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
end
and in the models/category.rb
Class Category < ActiveRecord::Base
has_and_belongs_to_many :posts
end
more here:
http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
I think #RAF pretty much nailed it. But to use the OP's example:
class User < ActiveRecord::Base
has_and_belongs_to_many :users_list
end
class UsersList < ActiveRecord::Base
has_and_belongs_to_many :users
end
Although at first it might seem like a User should have only one list of friends (UsersList), that might not always be the case. Think of types within the UserList model, such as: 'close friends', 'work friends', 'all friends' for example.
My advice: dig into the Rails guides. This is a concept worth learning and truly understanding (which I'm still doing :).
many-to_many relationships are a simple concept, but complex when using the database because of the way databases work. A person could have 1 to N different friends, which means that a single entry for a database would need a dynamic amount of memory for each entry, which in the db world is a no-no. So instead of creating a list of friends you would have to make a table that represents the links between friends, for example:
friendship.rb
class Friendship < ActiveRecord::Base
belongs_to :friend, foreign_key: 'friend_A' # this entry has a field called 'friend_A'
belongs_to :friend, foreign_key: 'friend_B' # this entry has a field called 'friend_B'
end
These links will represent your network of friends. However, as the two previous answers have mentioned, Rails has some nifty magic, "has_and_belongs_to_many", which will do this for you.
NOTICE: The problem here is that in my StatusesController, in the index action, the #relationship object only gets the statuses of all your friends, but does not get your own statuses. Is there a better way of approaching this? I am trying to create a view to view all statuses of users that are your friends, and your own statuses too, and so far, I can't seem to figure out how to order it chronologically, even if in my status model, i included "default_scope -> { order(created_at: :desc) } ". Any advice would be deeply appreciated
class User < ActiveRecord::Base
has_many :relationships
has_many :friends, :through => :relationships
has_many :inverse_relationships, class_name: 'Relationship', foreign_key: 'friend_id'
has_many :inverse_friends, through: 'inverse_relationships', :source => :user end
#
class Relationship < ActiveRecord::Base
# before_save...
belongs_to :user
belongs_to :friend, class_name: 'User'
end
#
class RelationshipsController < ApplicationController
def friend_request
user_id = current_user.id
friend_id = params[:id]
if Relationship.where( user_id: user_id, friend_id: friend_id, accepted: false).blank?
Relationship.create(user_id: user_id, friend_id: friend_id, accepted: false)
redirect_to user_path(params[:id])
else
redirect_to user_path(params[:id])
end
end
def friend_request_accept
# accepting a friend request is done by the recipient of the friend request.
# thus the current user is identified by to_id.
relationship = Relationship.where(user_id: params[:id], friend_id: current_user.id).first
if Relationship.exists?(relationship) and relationship.accepted == false
relationship.update_attributes(accepted: true)
end
redirect_to relationships_path
end
def friend_request_reject
relationship = Relationship.where(user_id: params[:id], friend_id: current_user.id).first
relationship.destroy
redirect_to relationships_path
end
################################
def index
#relationships_pending = Relationship.where(friend_id: current_user.id, accepted: false)
end
end
#
class StatusesController < ApplicationController
def index
#status = Status.new
#relationship = Relationship.where('friend_id = ? OR user_id = ?', current_user.id, current_user.id).
where( accepted: true)
end
def new
#status = Status.new
end
end
#

ActiveRecord custom validadion error not preserved when building relations

I have models:
class Agency < ActiveRecord::Base
SPECIALIZATIONS_LIMIT = 5
has_many :specializations
has_many :cruise_lines, through: :specializations
validate :validate_specializations_limit
protected
def validate_specializations_limit
errors.add(:base, "Agency specializations limit is #{SPECIALIZATIONS_LIMIT}.") if specializations.count > SPECIALIZATIONS_LIMIT
end
end
class CruiseLine < ActiveRecord::Base
has_many :specializations
has_many :agencies, through: :specializations
end
class Specialization < ActiveRecord::Base
belongs_to :agency, inverse_of: :specializations
belongs_to :cruise_line, inverse_of: :specializations
end
In my service I try to save agency and specialization relations like this:
attributes = params.require(:agency).permit(
:name, :website, :description, :booking_email, :booking_phone,
:optional_booking_phone, :working_hours, :cruise_line_ids => []
)
agency.update_attributes(attributes)
attributes[:cruise_line_ids].select{|x| x.to_i > 0}.each do |cruise_line_id|
agency.specializations.build(cruise_line_id: cruise_line_id)
end
How this code works:
1) it updates attributes;
2) it saves relations and updates them if I deselect some checkboxes with cruise lines;
3) in case I enter incorrect data or select too many cruise line checkboxes, it does not save data, but also shows no errors!
If I add some inspect code puts agency.errors.inspect immediately AFTER the block that builds relations - it works exactly as I expect: if everything saved - shows success message, if validation errors occured - shows error messages.
QUESTION:
why adding puts agency.errors.inspect immediately after saving code makes everything work as expected?
I've found bug: it was in my controller, that made redirect to edit page always after storing method worked (even if model was not saved!). Controller code looks like this:
if AgencyService.update_agency(current_agent.agency, params)
flash[:success] = "Agency data updated successfully"
redirect_to action: :edit
else
render :edit
end
The only thing I had to do was to add this code to the end of my update_agency method
# ... build relations
return false if agency.errors.present?
true
end
So if my AgencyService.update_agency return false - user is not redirected and all errors are shown on the page :)

Resources