Environment: Rails 3.2.13 + simple_form 2.1.0 + CanCan 1.6.10 + etc.
Model thumbnail: Articles have authors (Users) and Comments. Comments are a nested resource within Articles. The Comment model includes content, the commenter (currently logged-in user ID) and article ID.
Issue: Creating a new Comment on an Article causes the Article to be updated, understandably. At present, CanCan's Ability class is hardwired to allow that Article to be updated by that user. I want to limit that to allowing the update if the Article's Comments — and only that field — are updated. I've been poking around in pry for a couple of hours trying to figure out how to tell what's being updated, and am drawing a blank so far.
Models are posted in this Gist in response to Michael Szyndel's question.
Help?
In-lieu of identifying the culprit, which I would guess is related to the reliance on accepts_nested_attributes_for, I would rather offer a solution-- implement a before_update callback on the Article model.
before_update :verify_update_authorization
# virtual attribute to supply CanCan a user candidate
def initiator
#initiating_user if #initiating_user
end
def initiator=(user)
#initiating_user = user
end
private
def verify_update_authorization
return false if Ability.new(initiator).cannot?(:update, self)
end
The controllers would then need to set the Article's virtual attribute when an update is desired. In this particular case, it would be proper to override the InheretedResources update action.
Related
I am facing a strange thing on Ruby on Rails and I don't know what is the correct way to deal with this situation.
I have two models, Book and Page.
Book(name:string)
Page(page_number: integer, book_id: ID)
class Book < ActiveRecord::Base
has_many :pages
accepts_nested_attributes_for :pages
end
class Page < ActiveRecord::Base
belongs_to :book
validates_uniqueness_of :page_number, scope: :book_id
end
I have created a view from where I can update a book. I accept nested attributes for pages and there is a section where I can update book pages as well as add new pages (by a Javascript function that lets the user to add a new page row by clicking + button). As you can see I have a validation that requires the page number to be unique for a certain book to prevent duplication. I have also defined an unique index on the database level (using Postgres)
On my update action in Book's Controller I have:
def update
#book = Book.find(params[:id])
if #book.update_attributes(params[:book])
flash[:notice] = 'Book successfully modified!'
end
end
The problem with my approach is that sometimes the validation about the page_number that I have defined on Pages model is bypassed and I get an error directly from PG ("PG::UniqueViolation: ERROR: duplicate key value violates unique constraint").
This scenario happens on 2 cases:
Trying to create two or more pages with the same number directly on one form submission
Updating existing page_number (ex from page_number: 4 to nr: 5) and creating one new page with number 4 on one form submission.
It seems that there is some problem of concurrency and order of processing the updates/creates.
For the point 2, we should somehow tell Rails to look over all records and see if there we are trying to do any duplication by combining updates with creates. Point 2 is a valid option and there should be no validation error thrown but because Rails is doing creation first it laments that a page with page_number 4 already exists (without taking into considerance that page_number 4 is being updated to 5)
I would be thankful I you could advice me how to handle this situation and be able to predict all use cases so that I do not hit the database in case of a validation error.
If this is impossible, is there a way that I can catch the error from Postgres, format and display it to the user?
I appreciate any advice!
I'm putting together a side project for a teacher/student type of website where the student will share a dashboard with the teacher. The teacher and student can both upload files and leave comments in the dashboard.
The part that I'm stuck on is the permission. For student, I've set up the index controller to this method
def index
#Homework = Homework.where(:user_id = current_user)
end
With this, I'm able to have the student only see the work that they have, but I'm confused on how to get the teacher to see each individual student's work?
Suggestions? Thanks
Here's a simple solution if you only ever need to support a single class in your application:
def index
if current_user.teacher?
#homeworks = Homework.all
else
#homeworks = Homework.where(user_id: current_user)
end
end
Otherwise, your Homework schema does not seem to be correctly designed. See your query below:
Homework.where(:user_id = <student_id>)
This works to retrieve a student's homeworks, but it does not work to retrieve a teacher's students' homeworks. You may need a class_id for a teacher to see each individual student's work:
Homework.where(:class_id = <class_id>, :user_id = <student_id>)
A Class has_many Teachers and has_many Students. This design will allow you to support multiple classes.
Some more guiding questions:
Is teacher/student both kept in the same User model?
How do you differentiate between teacher/student in your current User model?
Is there a "user_type" column somewhere in User?
What happens if the current_user is of the "teacher" user_type?
For complex user permissions, use CanCanCan: https://github.com/CanCanCommunity/cancancan
Don't use uppercase instance variables. ex: #Homework should be #homework
Check out the gem CanCan. Install it (follow the instructions, you should have to put something in application controller), Then, put in your ability file:
class Ability
include CanCan::Ability
def initialize(user)
can :manage, Homework, user_id: user.id
end
end
Then at the top of your StudentController put
load_and_authorize_resource
And the index action should look like:
#homework = #student.homework
Now, you didn't post your whole controller so this is a much as I can help.
I believe you may have a bigger underlying issue. You have students and teachers has_many homework i read in your comment. Then in your example you use user_id. You are likely overriding your students and teacher ownership of homework. You would need a has_and_belongs_to_many relationship OR you would need a student_id and teacher_id columns on the homework table
Cancan automatically generate a number of instance variables which can make it feel like magic. Watch the free railscasts on cancan the same guy who made the video wrote the CanCan library.
I have a simple blog engine using Rails and Mongoid ORM.
I have 2 models in the blog, 'Article' and 'Url'.
The Article model contains all of the post content, and the Url class is the generator function that takes the slug of the Article and creates a Short URL for it.
E.g. my-sample-blog-post -> ai3n etc. etc.
The problem is I am having problems linking the two. I can't embed the URL class in the Article class either.
My question is, can I generate a Short URL on the fly, as the post is created, inside the Article model? The Article model already uses Mongoid::slug to give me nice post slugs, but I also need short URLs for each post.
Any help on this would be much appreciated.
I think you could probably use an after create callback to generate the short url and then store it in a field inside the Article model.
Something like this:
class Article
field :title
slug :title
field :short_url
after_create :generate_short_url
def generate_short_url
self.short_url = shorten_it(self.slug) # assuming you implement shorten_it
self.save
end
end
I've been reading up on rails security concerns and the one that makes me the most concerned is mass assignment. My application is making use of attr_accessible, however I'm not sure if I quite know what the best way to handle the exposed relationships is. Let's assume that we have a basic content creation/ownership website. A user can have create blog posts, and have one category associated with that blog post.
So I have three models:
user
post: belongs to a user and a category
category: belongs to user
I allow mass-assignment on the category_id, so the user could nil it out, change it to one of their categories, or through mass-assignment, I suppose they could change it to someone else's category. That is where I'm kind of unsure about what the best way to proceed would be.
The resources I have investigated (particularly railscast #178 and a resource that was provided from that railscast), both mention that the association should not be mass-assignable, which makes sense. I'm just not sure how else to allow the user to change what the category of the post would be in a railsy way.
Any ideas on how best to solve this? Am I looking at it the wrong way?
UPDATE: Hopefully clarifying my concern a bit more.
Let's say I'm in Post, do I need something like the following:
def create
#post = Post.new(params[:category])
#post.user_id = current_user.id
# CHECK HERE IF REQUESTED CATEGORY_ID IS OWNED BY USER
# continue on as normal here
end
That seems like a lot of work? I would need to check that on every controller in both the update and create action. Keep in mind that there is more than just one belongs_to relationship.
Your user can change it through an edit form of some kind, i presume.
Based on that, Mass Assignment is really for nefarious types who seek to mess with your app through things like curl. I call them curl kiddies.
All that to say, if you use attr_protected - (here you put the fields you Do Not want them to change) or the kid's favourite attr_accessible(the fields that are OK to change).
You'll hear arguments for both, but if you use attr_protected :user_id in your model, and then in your CategoryController#create action you can do something like
def create
#category = Category.new(params[:category])
#category.user_id = current_user.id
respond_to do |format|
....#continue on as normal here
end
OK, so searched around a bit, and finally came up with something workable for me. I like keeping logic out of the controllers where possible, so this solution is a model-based solution:
# Post.rb
validates_each :asset_category_id do |record, attr, value|
self.validates_associated_permission(record, attr, value)
end
# This can obviously be put in a base class/utility class of some sort.
def self.validates_associated_permission(record, attr, value)
return if value.blank?
class_string = attr.to_s.gsub(/_id$/, '')
klass = class_string.camelize.constantize
# Check here that the associated record is the users
# I'm leaving this part as pseudo code as everyone's auth code is
# unique.
if klass.find_by_id(value).can_write(current_user)
record.errors.add attr, 'cannot be found.'
end
end
I also found that rails 3.0 will have a better way to specify this instead of the 3 lines required for the ultra generic validates_each.
http://ryandaigle.com/articles/2009/8/11/what-s-new-in-edge-rails-independent-model-validators
Suppose we have the standard Post & Comment models, with Post having accepts_nested_attributes_for :commments and :autosave => true set.
We can create a new post together with some new comments, e.g.:
#post = Post.new :subject => 'foo'
#post.comments.build :text => 'bar'
#post.comments.first # returns the new comment 'bar'
#post.comments.first.post # returns nil :(
#post.save # saves both post and comments simultaneously, in a transaction etc
#post.comments.first # returns the comment 'bar'
#post.comments.first.post # returns the post 'foo'
However, I need to be able to distinguish from within Comment (e.g. from its before_save or validation functions) between
this comment is not attached to a post (which is invalid)
this comment is attached to an unsaved post (which is valid)
Unfortunately, merely calling self.post from Comment doesn't work, because per above, it returns nil until after save happens. In a callback of course, I don't (and shouldn't) have access to #post, only to self of the comment in question.
So: how can I access the parent model of a new record's nested associations, from the perspective of that nested association model?
(FWIW, the actual sample I'm using this with allows people to create a naked "comment" and will then automatically create a "post" to contain it if there isn't one already. I've simplified this example so it's not specific to my code in irrelevant ways.)
I think it is strange that Rails does not let you do this. It also affects validations in the child model.
There's a ticket with much discussion and no resolution in the Rails bug tracker about this:
Nested attributes validations
circular
dependency
And a proposed resolution:
nested models: build should directly
assign the
parent
Basically, the deal is, the nested attributes code doesn't set the parent association in the child record.
There's some work-arounds mentioned in the second ticket I linked to.
I don't think you can do this. On the other hand, your validations shouldn't be failing, as the order of the transaction will create the post record before saving the comment.