validation error on update_attributes of a subclass (STI) - ruby-on-rails

I implemented a signin/out machinery as Michael Hartl suggested in his tutorial (http://ruby.railstutorial.org/chapters/sign-in-sign-out). All worked perfectly: creating, deleting, updating user from a profile page.
Then I created a Teacher model as a subclass of User (STI). It hasn't its own validation and should use the same validation as parent model (user).
But updating attributes for user with type = "Teacher" is no more possible (for the others users it continues to work). When I compile the update form it returns:
`error messages say me teacher.attributes.password is missing validation
while the teacher should inherit the same validation of user.
In routes.rb, after the creation of the teacher model I added:
resources :teachers, :controller => 'users', :type => "Teacher"
This is the update method in the user controller:
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user])
redirect_to #user ...
If I change #user.update_attributes(params[:user]) to #user.update_attributes(params[:teacher]) It works, but obviously only for teacher.
I think the solution is quite easy but I'm a novice.
Any help would be appreciated!

I solved my problem thanks to the solution proposed by d11wtq here:
Is subclassing a User model really bad to do in Rails?
I edited the Teacher model like this:
class Teacher < User
def self.model_name
User.model_name
end
end

Related

Creating model and nested model (1:n) at once with ActiveRecord

My Rails5 application has an organization model and a user model (1:n relationship). The workflow of creating an organization should include the creation of the organization's first user as well. I thought this would be able with ActiveRecord through nested models, however the create action fails with the error message "Users organization must exist".
class Organization < ApplicationRecord
has_many :users, dependent: :destroy
accepts_nested_attributes_for :users
end
class User < ApplicationRecord
belongs_to :organization
end
class OrganizationsController < ApplicationController
def new
#organization = Organization.new
#organization.users.build
end
def create
#organization = Organization.new(organization_params)
if #organization.save
redirect_to #organization
else
render 'new'
end
end
def organization_params
params.require(:organization).permit(:name, users_attributes: [:name, :email, :password, :password_confirmation])
end
end
In the view I use the <%= f.fields_for :users do |user_form| %> helper.
Is this a bug on my side, or isn't this supported by ActiveRecord at all? Couldn't find anything about it in the rails guides. After all, this should be (theoretically) possible: First do the INSERT for the organization, then the INSERT of the user (the order matters, to know the id of the organization for the foreign key of the user).
As described in https://github.com/rails/rails/issues/18233, Rails5 requires integrity checks. Because I didn't like a wishy-washy solution like disabling the integrity checks, I followed DHH's advice from the issue linked above:
I like aggregation through regular Ruby objects. For example, we have a Signup model that's just a Ruby object orchestrating the build process. So I'd give that a go!
I wrote a ruby class called Signup which encapsulates the organization and user model and offers a save/create interface like an ActiveRecord model would. Furthermore, by including ActiveModel::Model, useful stuff comes in to the class for free (attribute hash constructor etc., see http://guides.rubyonrails.org/active_model_basics.html#model).
# The Signup model encapsulates an organization and a user model.
# It's used in the signup process and helps persisting a new organization
# and a referenced user (the owner of the organization).
class Signup
include ActiveModel::Model
attr_accessor :organization_name, :user_name, :user_email, :user_password, :user_password_confirmation
# A save method that acts like ActiveRecord's save method.
def save
#organization = build_organization
return false unless #organization.save
#user = build_user
#user.save
end
# Checks validity of the model.
def valid?
#organization = build_organization
#user = build_user
#organization.valid? and #user.valid?
end
# A create method that acts like ActiveRecord's create method.
# This builds the object from an attributes hash and saves it.
def self.create(attributes = {})
signup = Signup.new(attributes)
signup.save
end
private
# Build an organization object from the attributes.
def build_organization
#organization = Organization.new(name: #organization_name)
end
# Build a user object from the attributes. For integritiy reasons,
# a organization object must already exist.
def build_user
#user = User.new(name: #user_name, email: #user_email, password: #user_password, password_confirmation: #user_password_confirmation, organization: #organization)
end
end
Special thanks to #engineersmnky for pointing me to the corresponding github issue.
You're looking for "Association Callbacks". Once you send those params to your organization model you have access to them inside that model. If everytime an organization is created there will be a new user assigned to it you can just do the following in your Organization Model:
has_many :users, dependent: :destroy, after_add: :create_orgs_first_user
attr_accessor: :username #create virtual atts for all the user params and then assign them as if they were organizational attributes in the controller. This means changing your `organization_params` method to not nest user attributes inside the array `users_attributes`
def create_orgs_first_user
User.create(name: self.username, organization_id: self.id, etc.) # You can probably do self.users.create(params here) but I didn't try it that way.
end
The "Users organization must exist" error should not occur. ActiveRecord is "smart," in that it should execute two INSERTs. First, it will save the model on the has_many side, so that it has an id, and then it will save the model on the belongs_to side, populating the foreign key value. The problem is actually caused by a bug in accepts_nested_attributes_for in Rails 5 versions prior to 5.1.1. See https://github.com/rails/rails/issues/25198 and Trouble with accepts_nested_attributes_for in Rails 5.0.0.beta3, -api option.
The solution is to use the inverse_of: option or, better yet, upgrade to Rails 5.1.1.
You can prove that this is true by removing the accepts_nested_attributes_for in your Organization model and, in the Rails console, creating a new Organization model and a new User model, associating them (eg myorg.users << myuser) and trying a save (eg myorg.save). You'll find that it will work as expected.

How do I make some attributes accessible only to Users that have a specific role?

In my User model, I have this:
attr_accessible :role_ids, :as => :admin
But that doesn't seem to work.
I want this particular attribute to be only accessible if the current_user.can?(:edit, Role) - i.e. only users with a role of admin or superadmin should be able to access those attributes.
How do I do this?
Edit 1: I am using Devise, CanCan & Rolify.
Like I said, we think the best way to restrict attributes to certain roles is to use the strong parameters gem that DHH introduced recently. This will also be part of Rails4 thankfully.
Even if it doesn't fit, it's a good idea to start integrating the principles as it will make your Rails 3 to 4 upgrade easier.
If you have a Railscasts Pro membership, Ryan Bates has made another fantastic tutorial on it.
In brief, here's what's recommended in that Railscast.
After installing the gem, remove attr_accessible from your model.
Add this to an initializer:
ActiveRecord::Base.send(:include, ActiveModel::ForbiddenAttributesProtection)
Alter your update action in your controller::
def update
#topic = Topic.find(params[:id])
if #topic.update_attributes(topic_params)
redirect_to topics_url, notice: "Updated topic."
else
render :edit
end
end
Create a private method in your controller:
def topic_params
if current_user && current_user.admin?
params[:topic].permit(:name, :sticky)
else
params[:topic].permit(:name)
end
end
In your situation, like we do, you'd have to change the topic_params method to use your roles.
There's a few more suggestions in the RailsCast and it's really worth the $9! (I'm in no way affiliated with the site, it's just proven invaluable to us)
Let me know if this helps.
in my user model
class User
attr_accessible :nickname, as: :admin
end
in my rails console
User.first.update_attributes!({nickname: "uschi"})
# WARNING: Can't mass-assign protected attributes: nickname
User.first.update_attributes!({nickname: "uschi"}, {as: :admin})
# true
UPDATE
maybe i am to stupid to get your problem, but just do a
if current_user.can?(:edit, Role)
user.update_attributes!({nickname: "uschi"}, {as: :admin})
else
first.update_attributes!({nickname: "uschi"})
end
and there are probably smarter ways of doing this...

Rails polymorphic user model with Devise

So I know this question has been ask a ton of times but my question goes a little bit further.
When modeling my application I have two types of users that have a polymorphic association to the user model. Such as:
class User < ActiveRecord::Base
belongs_to :profileable, :polymorphic => true
end
class User_Type_1 < ActiveRecord::Base
has_one :user, :as => :profileable
end
class User_Type_2 < ActiveRecord::Base
has_one :user, :as => :profileable
end
The reason I did this, instead of an STI, is because User_Type_1 has something like 4 fields and User_Type_2 has something like 20 fields and I didn't want the user table to have so many fields (yes 24-ish fields is not a lot but I'd rather not have ~20 fields empty most of the time)
I understand how this works, my question is I want the sign up form to only be used to sign up users of type User_Type_1 but the sign in form to be used to both. (I will have an admin side of the application which will create users of User_Type_2)
I know I can use the after_sign_in_path_for(resource) override in AppicationController somehow to redirect to the right part of the site on sign in. Something like:
def after_sign_in_path_for(resource)
case current_user.profileable_type
when "user_type_1"
return user_type_1_index_path
when "user_type_2"
return user_type_1_index_path
end
end
So I guess my questions are how would I make the form to work with Devise and only allow signups of type User_Type_1 and then sign them in after sign_up?
Also, if I am going about this the wrong way, what is the right way?
I was able to answer my own question and am putting it here so that maybe it can help someone else with the same problem.
The login problem was easy, just use the default devise login and the after_sign_in_path_for in ApplicationController as described above
I realized the answer to the form question as typing it out here:
I just created a normal form for the User_Type_1 with nested attributes for User
and had it post to the UserType1Controller
Then saved both objects and called the sign_in_and_redirect helper from Devise
class UserType1Controller < ApplicationController
...
def create
#user = User.new(params[:user])
#user_type_1 = UserType1.new(params[:patron])
#user.profileable = #user_type_1
#user_type_1.save
#user.save
sign_in_and_redirect #user
end
...
end
Then the after_sign_in_path_for method from above sent it to the right place and it was all good.

Commenting system for RoR Blogging Platform

I'm trying to design a comment system for my RoR blogging site, and I am having some conceptual problems with the architecture. As far as models are concerned, I have Blogposts, Users, and Comments.
A User has_many Blogposts
A Blogpost belongs_to one User
A Blogpost has_many Comments
A Comment may or may not belong to a registered User (I want people not registered with the site to be able to comment as well).
My question is this: in order to enforce the link between a comment and a blogpost, I create each new comment (#comment) through the blogpost association (#blogpost.comments.build(:args)). However, I do not know how to associate a particular registered User with his/her comment. I left the user_id attribute OUT of the attr_accessible for the Comment model because I wanted to prevent the possibility of people attributing comments to the wrong users.
Any ideas on how best to implement a commenting system with such a relation? Thanks so much in advance!
Assuming:
User has_many comments
Comment belongs_to user
In your controller when saving the comment, you can simply do:
#comment.user = current_user if current_user
#comment.save
If the comment is done by an unregistered user #comment.user just stays empty.
You can just have an association :
User has_many comments through blog_posts
So, now you can do :
current_user.comments
Another way to do it is via blog_post:
current_user.blog_post.comments
Moreover, you can use the nice act_as_commentable plugin :)
https://github.com/jackdempsey/acts_as_commentable
There's no need to have user_id as attr_accessible if you have access to the currently logged in user in your save or post new comment methods.
If they aren't logged in then you expect current user to be empty / false.
This should be available if you're using any of the authentication plugins such as authlogic or devise. In my experience with authlogic you typically have a current_user method in your ApplicationController.
class ApplicationController
helper_method :current_user_session, :current_user
private
def current_user_session
return #current_user_session if defined?(#current_user_session)
#current_user_session = UserSession.find
end
def current_user
return #current_user if defined?(#current_user)
#current_user = current_user_session && current_user_session.user
end
end
Above code from the Authlogic quick example
You can add an association between Comment and User, then create the comment with current_user:
# User.rb
has_many :comments
# Comment
belongs_to :user
Setting up the associations only really adds the association methods, so there's no problem with creating Comment without a logged in user. You don't want to build the comment off of current_user as current_user.comments.create(...), because that will throw a NilClass error if nobody is logged in.
#user = current_user # #user should be nil if commenter is not logged in
# be fancy and use a block
#blogpost.comments.create(params[:comment]) do |c|
c.user = #user
end
As long as there is no validation for User in Comment, the nil user should just pass through without trouble.

Rails authentication gift list for each user?

I am trying to get to grips with the basics of authentication in Rails. To start with I have used the nifty_authentication generator by Ryan Bates. It's helping me learn the basic options for user logins etc.
I have a simple application the has a person and gift table in the database. The idea is, each user creates a list of people and then assigned possible gifts to each of those people.
So from a structural point of view:
person belongs to user
gift belongs to person
So I have the models set up as follows.
person model
class Person < ActiveRecord::Base
has_many :gifts
end
gift model
class Gift < ActiveRecord::Base
belongs_to :person
end
user model
currently doesn't contain any belongs_to has_many etc.
How do I go about making sure each user has their own list of people. So one user cannot see another users list of people or gifts.
Would I simply add the following to the user model?
has_many :people
and the following to the person model?
belongs_to :user
Would that work, or am I missing something?
Thanks,
Danny
UPDATE:
The app so far is on Heroku and Github.
http://giftapp.heroku.com/
http://github.com/dannyweb/GiftApp
Would that work, or am I missing
something?
Very short answer: yes that would work; no you are not missing something.
I looked at your code.
Instead of:
def index
#people = Person.find(:all)
end
You need something along the lines of:
def index
#people = current_user.people
end
Where current_user is the User object that refers to the logged in user.
In the create method you will need to assign the newly created person to the current_user:
def create
#person = Person.new(params[:person])
#person.user = current_user # This associates #person with current_user
if #person.save
flash[:notice] = "Successfully created person."
redirect_to #person
else
render :action => 'new'
end
end

Resources