Say I have a Rails model and I want to get the state of a list of an association before and after an update to see the difference between them. How would I go about doing this? I had tried just doing a before_update and after_update callback to maintain an array of the association and taking a rejection of what was in the array after the update compared to before the update but it seems that my array is never matching the state before the update.
Any ideas?
# == Schema Information
#
# Table name: lectures
#
# id :integer not null, primary key
# organization_id :integer
# training_type_id :integer
# name :string(255)
# description :text
# start_date :date
# end_date :date
# training_method :string(255)
# trainer_name :string(255)
# trainer_phone :string(255)
# trainer_email :string(255)
# location :string(255)
# total_seats :integer
# available_seats :integer
# hours :integer
# created_at :datetime
# updated_at :datetime
#
class Lecture < ActiveRecord::Base
# # # # # # # # # # # # # # #
# Relations
# # # # # # # # # # # # # # #
belongs_to :organization
belongs_to :training_type
has_many :training_histories
has_many :users, through: :training_histories
# belongs_to :training_type
# has_many :training_histories, inverse_of: :lecture, :autosave => true
accepts_nested_attributes_for :training_histories
# # # # # # # # # # # # # # #
# Validations
# # # # # # # # # # # # # # #
validates :name,
:start_date,
:end_date,
:location,
:organization,
:trainer_email,
:trainer_name,
:hours,
:presence => :true
validate :start_must_be_before_end_date
validates :trainer_email, email: true
after_update :email_users_about_update, email_supervisors_about_signup
def start_must_be_before_end_date
errors.add(:start_date, "must be before end date") unless self.start_date <= self.end_date
end
def email_users_about_update
self.training_histories.each do |history|
UserMailer.updated_training_email(history.user, self).deliver unless history.completed
end
end
def email_supervisors_about_signup
binding.pry
if self.users.changed?
users_dif = self.users - self.users_was
unless users_dif.nil?
users_dif.each do |user|
UserMailer.create_subordinate_training_sign_up(user.leader, user, self).deliver
end
end
end
end
end
You can use an after_add callback:
# add the callback to our association
has_many :users, through: :training_histories, after_add: :invite_to_training
# this method gets called when a user is added
private
def invite_to_training(user)
UserMailer.create_subordinate_training_sign_up(user.leader, user, self).deliver
end
For more information on association callbacks see: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Association+callbacks
Related
I've got this whole surveys thing:
Survey.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: surveys
#
# id :bigint not null, primary key
# description :string
# hidden :boolean default(FALSE)
# internal_description :string
# internal_name :string
# min_green :integer
# min_orange :integer
# min_red :integer
# required :boolean default(FALSE)
# survey_type :integer default("medical")
# title :string
# created_at :datetime not null
# updated_at :datetime not null
#
class Survey < ApplicationRecord
has_many :questions, class_name: 'Question', dependent: :destroy
has_many :filled_surveys
has_many :study_surveys
has_many :studies, through: :study_surveys
accepts_nested_attributes_for :questions
enum survey_type: %i[medical law]
validates :title, :description, :internal_name, :internal_description, presence: true
end
filled_survey.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: filled_surveys
#
# id :bigint not null, primary key
# score :integer
# state :integer default("pending")
# created_at :datetime not null
# updated_at :datetime not null
# survey_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_filled_surveys_on_survey_id (survey_id)
# index_filled_surveys_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (survey_id => surveys.id)
# fk_rails_... (user_id => users.id)
#
class FilledSurvey < ApplicationRecord
belongs_to :survey
has_many :questions, through: :survey
belongs_to :user, class_name: 'User'
has_many :answers, class_name: 'Answer'
accepts_nested_attributes_for :answers
has_one :study, through: :survey
enum state: %i[pending done redo]
end
question.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: questions
#
# id :bigint not null, primary key
# description :string
# max :integer
# min :integer
# order :integer default(0)
# placeholder :string
# question_type :integer
# title :string
# created_at :datetime not null
# updated_at :datetime not null
# survey_id :bigint not null
#
# Indexes
#
# index_questions_on_survey_id (survey_id)
#
# Foreign Keys
#
# fk_rails_... (survey_id => surveys.id)
#
class Question < ApplicationRecord
belongs_to :survey
has_many :question_options, class_name: 'QuestionOption', dependent: :destroy
has_many :answers, dependent: :destroy
accepts_nested_attributes_for :question_options, allow_destroy: true, reject_if: proc { |att| att['name'].blank? }
enum question_type: %i[short long number single multiple date]
validates :title, presence: true, on: :create
end
question_option.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: question_options
#
# id :bigint not null, primary key
# display :string
# name :string
# score :integer default(0)
# created_at :datetime not null
# updated_at :datetime not null
# question_id :bigint not null
#
# Indexes
#
# index_question_options_on_question_id (question_id)
#
# Foreign Keys
#
# fk_rails_... (question_id => questions.id)
#
class QuestionOption < ApplicationRecord
belongs_to :question, optional: true
validates :name, :display, :score, presence: true
end
answer.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: answers
#
# id :bigint not null, primary key
# content :string
# created_at :datetime not null
# updated_at :datetime not null
# filled_survey_id :bigint not null
# option_id :bigint
# question_id :bigint not null
#
# Indexes
#
# index_answers_on_filled_survey_id (filled_survey_id)
# index_answers_on_option_id (option_id)
# index_answers_on_question_id (question_id)
#
# Foreign Keys
#
# fk_rails_... (filled_survey_id => filled_surveys.id)
# fk_rails_... (question_id => questions.id)
#
class Answer < ApplicationRecord
belongs_to :filled_survey
belongs_to :question
belongs_to :option, class_name: 'QuestionOption', optional: true
validates :content, presence: true, length: { minimum: 1 }, on: :update
validate :content, :validate_question_rules, on: :update
end
And view:
/ Body
.w-full.flex-grow.overflow-y-auto
/ Surveys questions forms
= form_for #selected, url: update_filled_survey_path(#selected.id), html: { data: { 'participant-target' => "#{#selected.id}_form" } } do |form|
= form.hidden_field :survey_id
- #selected.questions.includes(:question_options).each do |question|
.rounded-md.bg-gray-100.opacity-75.flex.items-center.justify-between.mb-2.p-2.relative
%span.flex-grow
= render "components/inputs/#{question.question_type}", question: question, form: form
.flex.justify-center.bg-gray-100.rounded-md.cursor-pointer.my-3
%button.w-full.px-6.py-2.bg-green-200.rounded-md.cursor-pointer.hover:bg-green-300{'data-action' => 'click->participant#updateSurvey', 'data-filled' => #selected.id } #{t('views.participant_portal.surveys.complete')}
Partial(for single type question):
.input_group.flex.flex-col.justify-start
%label.text-gray-700.text-md.font-bold
= question.title
- if question.description
%label.text-gray-600.text-sm #{question.description}
.p-4
= form.fields_for question.question_options do |question_form|
= question_form.collection_select(:option_id, question.question_options, :id, :display, { selected: question.question_options.option_id, include_blank: false }, { id: question.id, name: "#{question.id}_answer", class: 'bg-transparent mx-2'})
JS controller (stimulus JS):
updateSurvey (e) {
document.getElementsByTagName('form')[0].submit()
}
Controller
before_action :set_filled, only: %i[fill_survey]
def set_filled
if current_user.profile.active_study && current_user.profile.paid?
#surveys = current_user.profile.active_study.surveys.select(&:medical).map do |survey|
FilledSurvey.includes.find_or_create_by(survey: survey, user: current_user)
end.reject(&:done?)
elsif current_user.profile.active_study && current_user.profile.approved?
#surveys = current_user.profile.active_study.surveys.select(&:medical?).map do |survey|
FilledSurvey.find_or_create_by(survey: survey, user_id: current_user.id,
created_at: FilledSurvey.this_month)
end.reject(&:done?)
elsif action_name == 'law_quiz'
#surveys = current_user.profile.active_study.surveys.select(&:law?).map do |survey|
FilledSurvey.find_or_create_by(survey: survey, user_id: current_user.id,
created_at: FilledSurvey.this_month)
end.reject(&:done?)
end
end
def fill_survey
set_meta_tags(title: t('page_title.participant_portal.fill_survey'))
aasm_state = current_user.profile.aasm_state
redirect_to authenticated_root_path unless %w[paid approved].include?(aasm_state) &&
!#surveys.empty?
#active_study = current_user.profile.active_study
#selected = params[:id] ? FilledSurvey.find(params[:id]) : #surveys.first
# #answers = #selected.survey.questions.map do |question|
# Answer.find_or_create_by(question_id: question.id, filled_survey_id: #selected.id)
# end
# #answers.sort_by { |answer| answer.question.order }
end
What I wanted to do:
Submit all forms with one button
Get ID of QuestionOption that I select for each question
Create Answer for each Question based on QuestionOption
What I get:
"undefined method `option_id' for #<ActiveRecord::Associations::CollectionProxy [#<QuestionOption id: 328, name:(...)"
Can someone explain how to make it works? I want to understand what's wrong with this code, so I can implement this right way
I'm trying to modify my existing callback on a App model to be executed via delayed job. I am getting a error undefined local variable or method app_name for main:Object, when deleted an app.
app/models/app.rb
# == Schema Information
#
# Table name: apps
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime
# updated_at :datetime
# app_type :string(255)
# package_name :string(255)
# icon :string(255)
#
# app/models/app.rb
class App < ActiveRecord::Base
has_many :versions, dependent: :destroy
app/models/version.rb
class Version < ActiveRecord::Base
DEFAULT_ICON_URL = 'placeholder_med#2x.png'
belongs_to :app
delegate :name, :id, :users, :app_type, to: :app, prefix: true
after_create :notify_subscribers
before_destroy :remove_stored_files
scope :since, ->(time) { where('created_at > ?', time) }
def updated_or_created_at
updated_at || created_at
end
def display_icon
if icon_url.blank? || icon_url.match(/default.png/)
DEFAULT_ICON_URL
else
icon_url
end
end
def main?
version_type == 'main'
end
def release_notes?
!release_notes_url.blank?
end
private
def notify_subscribers
AppMailer.notify_new_build(id)
end
def remove_stored_files
Delayed::Job.enqueue(DeleteAppFilesJob.new(app_name, version_number, build_number), priority: 1, run_at: 5.minute.from_now)
end
end
app/jobs/delete_app_files_job.rb
class DeleteAppFilesJob < Struct.new(app_name, version_number, build_number)
def perform
remove_stored_files(app_name, version_number, build_number)
end
protected
def remove_stored_files(app_name, version_number, build_number)
S3_BUCKET.objects.select { |obj| obj.key.match(%r{(ios|android)/#{app_name}/#{version_number}/#{build_number}}) }.each do |obj|
puts "Deleting #{obj.key}"
obj.delete
end
end
end
To create an anonymous Struct (which you generally want when subclassing like that), you pass Symbol arguments to Struct.new:
class DeleteAppFilesJob < Struct.new(:app_name, :version_number, :build_number)
You're trying to pass variables that don't exist, hence the "undefined local variable or method" error.
I've models GuestOrder, OrderBatch, OrderItem
# == Schema Information
#
# Table name: guest_orders
#
# id :integer not null, primary key
# notes :string(255)
# adults :integer
# children :integer
# created :datetime
# placed :datetime
# billed :datetime
# created_at :datetime
# updated_at :datetime
#
class GuestOrder < ActiveRecord::Base
has_many :order_batches, :dependent => :destroy
end
# == Schema Information
#
# Table name: order_batches
#
# id :integer not null, primary key
# placed :datetime
# guest_order_id :integer
# created_at :datetime
# updated_at :datetime
#
class OrderBatch < ActiveRecord::Base
belongs_to :guest_order
has_many :order_items, :dependent => :destroy
end
# == Schema Information
#
# Table name: order_items
#
# id :integer not null, primary key
# quantity :integer
# accepted :datetime
# cooking :datetime
# ready :datetime
# delivered :datetime
# cancelled :datetime
# order_batch_id :integer
# dish_id :integer
# created_at :datetime
# updated_at :datetime
#
class OrderItem < ActiveRecord::Base
belongs_to :order_batch
belongs_to :dish
end
I'm trying to render json in the following method to get a guest_order and its belonging order_batches and order_items by passing guest_order id as parameter.
def getOrderDetails
#To get the details of a particular guest_order and its batches and items
#guest_order = GuestOrder.find_by_id(params[:id])
render :json => #guest_order.to_json(:except => [:created_at, :updated_at],
:includes => {:order_batches => {:except => [:guest_order_id, :created_at, :updated_at],
:includes => {:order_items => {:except => [:order_batch_id, :created_at, :updated_at] } } } } )
end
But I didn't get the expected result, only the details from the guest_orders table is rendered. How to solve this?
I use :include rather than :includes, not sure if that's significant.
Try using the :include without the :except first, and when you get that working, add in the :except.
I find my models getting more and more clumped and messy. below is my actual user model, any suggestions on cleaning things up and arranging things for better readability?
Im getting frustrated by the unreadability of this all, so any general thoughts on this code are welcome. I really like code that does the same to be positioned together but like I have now is just one big bulk unreadable mess.
# == Schema Information
#
# Table name: users
#
# id :integer(4) not null, primary key
# email :string(255) default(""), not null
# encrypted_password :string(255) default(""), not null
# reset_password_token :string(255)
# reset_password_sent_at :datetime
# remember_created_at :datetime
# sign_in_count :integer(4) default(0)
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string(255)
# last_sign_in_ip :string(255)
# password_salt :string(255)
# confirmation_token :string(255)
# confirmed_at :datetime
# confirmation_sent_at :datetime
# unconfirmed_email :string(255)
# failed_attempts :integer(4) default(0)
# unlock_token :string(255)
# locked_at :datetime
# authentication_token :string(255)
# username :string(255)
# is_blocked :boolean(1)
# is_deleted :boolean(1)
# role :string(255)
# slug :string(255)
# created_at :datetime not null
# updated_at :datetime not null
# last_seen :datetime
# credits :integer(4)
#
class User < ActiveRecord::Base
devise :database_authenticatable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable,
:token_authenticatable,
:encryptable,
:confirmable,
:lockable,
:timeoutable,
:lastseenable
#:omniauthable
attr_accessible :username,
:login,
:email,
:password,
:password_confirmation,
:remember_me,
:profile_attributes,
:is_moderated,
:is_blocked,
:is_deleted,
:credits,
:role,
:confirmed_at,
:last_seen,
:invite_code
attr_accessor :login
#attr_accessor :invite_code
has_one :profile
has_one :account
accepts_nested_attributes_for :profile
accepts_nested_attributes_for :account
extend FriendlyId
friendly_id :username, use: :slugged
before_create :default_values
# AFTER CREATE -------------------------------------------------------------------------------------------------------
after_create :add_account
def add_account
self.create_account
end
def default_values
self.credits = -1
self.invite_code = invite_code
#self.reset_authentication_token!
beta = Beta.where(:code => invite_code).first
beta.used = 1
beta.save
end
# ROLES --------------------------------------------------------------------------------------------------------------
before_create :setup_default_role_for_new_users
ROLES = %w[admin default vip]
# VALIDATIONS --------------------------------------------------------------------------------------------------------
before_validation { |u| u.username.downcase! }
before_validation { |u| u.email.downcase! }
validates_uniqueness_of :username,
:email,
:case_sensitive => false
validates_presence_of :email,
:username,
:invite_code
validates :username,
:exclusion => {:in => ["admin", "root", "administrator", "superuser", "myhost", "support", "contact", "chat", "boo"],
:message => "is reserved"}
validate :check_email, :on => :create
validate :check_invite_code, :on => :create
def check_invite_code
errors.add(:invite_code, "Invalid code") unless Beta.where(:code => invite_code, :used => 0).first
end
# Devise
def active_for_authentication?
super && !is_deleted
end
# Devise
def confirm!
#welcome_message
#super
end
# Devise
def soft_delete
update_attribute(:deleted_at, Time.current)
end
def is_moderated?
return self.is_moderated
end
def is_online?
if self.last_seen < 10.minutes.ago
return true
else
return false
end
end
private
def setup_default_role_for_new_users
if self.role.blank?
self.role = "default"
end
end
def welcome_message
#::Devise.mailer.welcome_instructions(self).deliver
::Devise.mailer.welcome_instructions(self).deliver
end
def check_email
host = email.split("#").last
username = email.split("#").first
reserved_word_filters = %w(admin hostmaster root support )
if /.*(#{reserved_word_filters.join("|")}).*\#/.match(email)
errors.add(:email, "Invalid username in email")
end
if email.include? 'myhost'
errors.add(:email, "Invalid email")
end
end
# DEVISE: --------------------------------------------------------------------------------------------------
protected
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
login = conditions.delete(:login)
where(conditions).where(["lower(username) = :value OR lower(email) = :value", {:value => login.strip.downcase}]).first
end
end
The only thing I would do to clean up models is to modularize them a bit. DHH himself posted a great example gist showing how to clean up a model that had gotten too large. I don't think yours is particularly too big, but if you wanted to move all the devise stuff into its own module, it certainly would make your model a bit more tidy.
You could also get in the habit of treating booleans as first class expressions. For example,
def is_online?
if self.last_seen < 10.minutes.ago
return true
else
return false
end
end
is more clearly written as:
def is_online?
last_seen < 10.minutes.ago
end
1.
You have return statements all over your code. Unless you are returning something prematurely in a method, the return of the last statement in a method is what Ruby returns automatically. Like so:
def square(x)
val = x**2
return val
end
can be shortened to:
def square(x)
x**2
end
It's a contrived example, but there it is.
2.
Many of the selfs are redundant. In an model instance's scope, when setting an attribute to a value or calling a method, you do not need to prepend self, since that method/variable is already being called from that same scope.
What I would like to do is when a user is created, it automatically gives them a role.
The thing is that the role is related to the user via an assignment table and a has_many through association.
The code is as follows:
User model
# == Schema Information
# Schema version: 20110214082231
#
# Table name: users
#
# id :integer not null, primary key
# email :string(255)
# encrypted_password :string(128)
# password_salt :string(255)
# reset_password_token :string(255)
# remember_token :string(255)
# remember_created_at :datetime
# sign_in_count :integer
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string(255)
# last_sign_in_ip :string(255)
# username :string(255)
# f_name :string(255)
# l_name :string(255)
# created_at :datetime
# updated_at :datetime
# invitation_token :string(60)
# invitation_sent_at :datetime
# plan_id :integer
# current_state :string(255)
# confirmation_token :string(255)
# confirmed_at :datetime
# confirmation_sent_at :datetime
#
class User < ActiveRecord::Base
before_create :assign
has_many :assignments
has_many :roles, :through => :assignments
def role_symbols
roles.map do |role|
role.name.underscore.to_sym
end
end
def assign
#assignment.build_role(:user_id => self.id, :role_id => '3')
end
end
Assignments model
# Table name: assignments
#
# id :integer not null, primary key
# user_id :integer
# role_id :integer
# created_at :datetime
# updated_at :datetime
#
class Assignment < ActiveRecord::Base
belongs_to :role
belongs_to :user
end
Role model
# == Schema Information
# Schema version: 20101117094659
#
# Table name: roles
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime
# updated_at :datetime
#
class Role < ActiveRecord::Base
has_many :assignments
has_many :users, :through => :assignments
end
Firstly, don't use hard-coded ids to find your roles as they may change.
Secondly, use association builders, or just access the roles association yourself.
Thirdly, follow the rule of "Don't Make Me Think" (DMMT) when naming your methods.
With these three things in mind:
def assign_default_role
self.roles << Role.find_by_name("User")
end