Using Concerns to Extend User Model With Roles - ruby-on-rails

I'm experimenting in building a roles-based data structure in Rails using Concerns with role-specific methods. I've explored other solutions, like STI, polymorphic associations, but opted to try this method.
All attributes depicted are stored in one table, users.
module Client
extend ActiveSupport::Concern
def self.extended(target)
target.class_eval do
validates :emergency_contact, presence: true
end
end
end
module Doctor
def self.extended(target)
target.class_eval do
validates :credentials, presence: true
end
end
end
class User < ApplicationRecord
validates :role, allow_blank: true, inclusion: { in: roles }
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true
after_initialize { extend_role }
# Extend self with role-appropriate Concern
def extend_role
extend role.camelize.constantize if role
end
def role=(role)
super(role)
extend_role
end
end
My challenge is during the changing of a role, specifically, what can be done (if anything) to negate the previous role's Concern having extended the User?
Each user will have 1 role, so having the Client and Doctor concerns both mixed into the User would not be appropriate (at this time, at least).
In an ideal solution, the User instance would morph with the role change and retain all changed attributes.
So, calling this:
user.update_attributes({ first_name: 'Wily', last_name: 'McDoc', role: 'doctor' })
...would first handle the role change and then update the attributes.

guess could, in part, remove all methods:
Module.instance_methods.each{|m|
undef_method(m)
}
also, How can I reverse ruby's include function, in case that may be of interest

Related

How can I prevent an attribute from being a certain value?

I have a model "User" with attribute "Username". Can I use validations to prevent a User being created with the Username "home"?
class User < ActiveRecord::Base
validates :username, presence: true
end
You can use an exclusion validator:
class User < ActiveRecord::Base
USERNAME_BLACKLIST = ['home'].freeze
validates :username, presence: true, exclusion: { in: USERNAME_BLACKLIST }
end
Alternatively, you can always rely on a custom validation method, using validate instead of validates, for more complex types of validation that aren't easily expressed using built-in validators:
class User < ActiveRecord::Base
validates :username, presence: true
validate :username_not_on_restricted_list
protected
def username_not_on_restricted_list
errors.add(:username, :invalid) if username == 'home'
end
end
You could also write a custom validator if you intend to reuse this functionality across multiple models.

Rails - new User builds dependent Email and validates both?

I'm building a quick Rails project that allows users to manage their email addresses. Users can have many emails, but one (and only one) of those emails has to be marked as 'primary' (for login), and a user cannot exist without a primary email.
I've been struggling to get this to work right - it seems so circular to me. I need to build a User, and then the Email, but I don't want to save the User into the database unless the Email is valid, which it won't be until the User is saved (because of the validates :user, presence: true constraint).
Accepts nested resources for doesn't seem to work with .new (works fine with .create), and if my Email fails its validations, the User still shows as valid.
Been having a difficult time trying to find good resources (or SO questions) for building/validating multiple/dependent models from a single form.
What's the most Rails way to do this?
User
has_many :emails
has_one :primary_email, -> { where(primary: true) }, class_name: "Email"
accepts_nested_attributes_for :primary_email
validates :first_name, presence: true
validates :last_name, presence: true
validates :birthday, presence: true
validates :password_digest, presence: true
Email
belongs_to :user
validates :user, presence: true
validates :address, presence: true, uniqueness: {
case_sensitive: false
}
UsersController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
# do something
else
# show #user.errors
end
end
private
def user_params
params.require(:user).permit(
:first_name,
:last_name,
:birthday,
:password,
:password_confirmation,
:primary_email_attributes => [:address]
)
end
EDIT
The Email model also contains the following fields:
label = string, eg. 'Personal', 'Work', etc
primary = boolean, whether it's marked as primary email or not
confirmation_code = autogenerated on creation, used to confirm ownership
confirmed = boolean, whether it's been confirmed or not
class User
user has_many :emails
user has_one :primary_email, -> { where(primary: true) }, class_name: "Email", autosave: true
after_initialize {
build_primary_email if new_record?
}
end
class Email
# use gem https://github.com/balexand/email_validator
validates :my_email_attribute, :email => true
end
So after a user initialized its building a primary_email so that record is already associated, or at least it will be if it can be saved. the autosave is working pretty cool - if the primary-email can't be saved due validation error, the user can't neither. should work out of the box, im in a bus right now, can't check it. cheers
futher information: http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
If validations for any of the associations fail, their error messages will be applied to the parent. That means, the Parent Model (in your case User) is having errors, and thats why the saving is not possible! that's what you are looking for.
I would store a primary email as a common field and additional emails some another way. I would prefer to store additional emails in another field too that is Array rather than in an associated table. You shouldn't store a primary email in another table. Just imagine, every time you need authorize user or just get his email you will perform an extra request to db.
Meant to post this months ago.
The solution, keeping users and emails normalized across different models without storing a primary email as an attribute on the user, is to use inverse_of:
User.rb
class User < ActiveRecord::Base
has_many :emails, inverse_of: :user, dependent: :destroy
accepts_nested_attributes_for :emails
validates :emails, presence: true
end
Email.rb
class Email < ActiveRecord::Base
belongs_to :user, inverse_of: :emails
validates :user, presence: true
end
This allows validations to be performed using in-memory objects, rather than via database calls (ie the object associations are being validated, rather than the presence of an id/record in the database). Therefore they both pass validation and can both be saved in the same transaction.
See: https://viget.com/extend/exploring-the-inverse-of-option-on-rails-model-associations

Using rails associations for models (undefined method `has_one')

I am trying to open up the registration page to create an account (which has a payment) and I get this error "undefined method `has_one' for Account:Class". I am not using a database so I am not using active record. Is there a way around this?
account.rb
class Account
include ActiveModel::Model
attr_accessor :company_name, :phone_number,
:card_number, :expiration_month, :expiration_year, :cvv
has_one :payment
end
payment.rb
class Payment
include ActiveModel::Model
attr_accessor :card_number, :expiration_month, :expiration_year, :cvv
belongs_to :account
validates :card_number, presence: true, allow_blank: false
validates :cvv, presence: true, allow_blank: false
end
account_controller.rb
class AccountController < ApplicationController
def register
#account = Account.new
end
end
has_one and belongs_to are not part of ActiveModel::Model. They are part of ActiveRecord as they specify how to get the objects from the relational database.
In your case I guess you should just have another attribute payment in Account model.
class Account
include ActiveModel::Model
attr_accessor :company_name, :phone_number,
:card_number, :expiration_month,
:expiration_year, :cvv,
:payment
end
and then in your controller do something like
class AccountController < ApplicationController
def register
#account = Account.new
#account.payment = Payment.new
end
end
or you could event initialize the payment in initializer of Account class. Also it seems like Payment does not need to know about Account.
Certainly this is a very old question, but I ran across it while trying to solve a similar problem and eventually found that solutions have arisen in the intervening years. So, given the only posted answer was never marked as "accepted," I thought I'd throw my hat in the ring.
Rails 5 introduced the ActiveRecord Attributes API, and by way of this post by Karol Galanciak describing it you may be interested to take a look at a gem created by the same author. If you took a look at the issues for that gem, you might be interested to read in issue #12 that the Attributes API functionality is now present in ActiveModel, though it is not publicly documented (the module Attributes is flagged with #:nodoc:, see attributes.rb) and perhaps should not be relied upon to be consistent from version to version, though the wind, soft as it may seem at times, certainly seems to be blowing in the direction of a "public" ActiveModel::Attributes API.
Nevertheless, if you were to throw caution to the wind and use the not-quite-public ActiveModel::Attributes you could do something like this (note: I cobbled this together for my own project and rewrote it a bit to fit your example, your needs may be different than mine):
class AccountPayment
include ActiveModel::Model
attr_accessor :card_number, :expiration_month, :expiration_year, :cvv
validates :card_number, presence: true, allow_blank: false
validates :cvv, presence: true, allow_blank: false
end
class AccountPaymentType < ActiveModel::Type::Value
def cast(value)
AccountPayment.new(value)
end
end
class Account
include ActiveModel::Model
include ActiveModel::Attributes #being bad and using private API!
attr_accessor :company_name, :phone_number, :card_number, :expiration_month, :expiration_year, :cvv
attribute :payment, :account_payment
end
And somewhere you must register the type - in rails it'd be in an initializer, but in my code I just stashed it at the top of the equivalent of your Account model:
ActiveModel::Type.register(:account_payment, AccountPaymentType)

Integration testing rails-api model using rspec and fixtures

I'm pretty new to Rails and I've finished (I think) my rails API, and now I'm up to test it.
Say I have this model:
class Child < ActiveRecord::Base
belongs_to :parents
validates :id, presence: true
validates :type, presence: true
validates :parent_id, presence: true
before_create :update_child_if_exists
private
def update_child_if_exists
conditions = {type: self.type, parent_id: self.parent_id}
if existing_measure = Child.find_by(conditions) # or Child.where(conditions).last
new_values = (existing_child.values || []) + [self.values]
existing_measure.update_attribute(:values, new_values)
end
end
end
This is meant to update a field in the child table if the record by type and parent_id already exists, else it would be created. How can I write a test for this using RSpec?
Any help would be much appreciated, I'm pretty lost here.
If any additional information is needed, please ask me and I will provide it.

Validation of nested resource fails on build

I'm building instances of a model in another model controller. All seems to work fine, child instances are well created with the parent id but as soon as I add validations for parent_id in this resource, the instance is no longer valid. Any idea what I'm missing ?
Mission model:
class Mission < ActiveRecord::Base
has_many :planned_times
validates :code, presence: true, uniqueness: { case_sensitive: false }
validates :days_sold, presence: true
end
PlannedTime model:
class PlannedTime < ActiveRecord::Base
belongs_to :mission
validates :date, presence: true
validates :mission_id, presence: true # this is the validation which causes problem
end
Mission controller:
class MissionsController < ApplicationController
def create
#mission = Mission.new(mission_params)
week_nums = params[:weeks].split(/[\s]*[,;\-:\/+][\s]*/).uniq
year = params[:year].to_i
week_nums.each do |week_num|
date = Date.commercial(params[:year].to_i,week_num.to_i)
#mission.planned_times.build(date: date)
end
if #mission.save
flash.now[:success] = "Mission added"
end
end
private
def mission_params
params.require(:mission).permit(:code, :days_sold)
end
end
So validating the presence of associations is a little tricky. In your case you're putting the mission_id validator on the child association but rails runs the validation on planned_time before it saves the mission so it will fail because mission_id is still nil. Also, by putting the validation on planned_time it'll mean that that validation won't run if you never mission.planned_items.build because the associated planned_time won't exist and therefore not run its validations.
With minimal changes to your code or validation logic you can get it to work like this:
class PlannedTime < ActiveRecord::Base
belongs_to :mission
validates :mission_id, presence: { if: ->(p) { p.mission.nil? } }
end
This part presence: { if: ->(p) { p.mission.nil? } } will check if there is a mission object present (albeit without an id yet) and if there is no mission object the validation will fail. So good, now we know we can't create a planned_time without its parent mission object present. But this says nothing about the mission requiring the planned_time to be created. If this is what you want then that's the solution. Although I'm left wondering if you really wanted it the other way around where you want to make sure a mission is always created along with its planned_time?

Resources