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.
Related
So I apologize for how noobish these questions may seem. I'm new to rails and as a first task I also brought in Neo4J as it seemed like the best fit if I grow the project.
I'll explain the flow of actions then show some example code. I'm trying to add in step 3-5 now.
User logs in via FB
The first login creates a user node. If the user exist, it simply retrieves that user+node
After the user node is created, the koala gem is used to access the FB Graph API
Retrieves friendlist of each friend using the app.
Go through each friend and add a two way friendship relationship between the two users
As 3-5 only needs to happen when the user first joins, I thought I could do this in a method associated with after_save callback. There is a flaw to this logic though as I will need to update the user at some point with additional attributes and it will call after_save again. Can I prevent this from occurring with update?
SessionsController for reference
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
So in my user.rb I have something like this
has_many :both, :friendships
after_save :check_friends
def self.from_omniauth(auth)
#user = User.where(auth.slice(:provider, :uid)).first
unless #user
#user = User.new
# assign a bunch of attributes to #user
#user.save!
end
return #user
end
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
friendship.rb
from_class User
to_class User
type 'friendship'
def self.create_friendship(user,friend_node)
friendship = Friendship.create(from_node: user, to_node: friend_node)
end
I'm not sure if I'm on the right track with how to create a relationship node. As I just created #user, how do I incorporate that into my check_friends method and retrieve the user and friend node so properly so I can link the two together.
Right now it doesn't know that user and friend_user are nodes
If you see other bad code practice, please let me know!
In advance: Thanks for the help #subvertallchris. I'm sure you will be answering lots of my questions like this one.
This is a really great question! I think that you're on the right track but there are a few things you can change.
First, you need to adjust that has_many method. Your associations always need to terminate at a node, not ActiveRel classes, so you need to rewrite that as something like this:
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
You'll run into some problems otherwise.
You may want to consider renaming your relationship type in the interest of Neo4j stylistic consistency. I have a lot of bad examples out there, so sorry if I gave you bad ideas. FRIENDS_WITH would be a better relationship name.
As for handling your big problem, there's a lot you can do here.
EDIT! Crap, I forgot the most important part! Ditch that after_save callback and make the load existing/create new user behavior two methods.
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
#user = user.nil? ? User.create_from_omniauth(env["omniauth.auth"]) : user
session[:user_id] = #user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
end
class User
include Neo4j::ActiveNode
# lots of other properties
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
def self.from_omniauth(auth)
User.where(auth.slice(:provider, :uid)).limit(1).first
end
def self.create_from_omniauth(auth)
user = User.new
# assign a bunch of attributes to user
if user.save!
user.check_friends
else
# raise an error -- your user was neither found nor created
end
user
end
# more stuff
end
That'll solve your problem with getting it started. You may want to wrap the whole thing in a transaction, so read about that in the wiki.
But we're not done. Let's look at your original check_friends:
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
You're not actually passing it an argument, so get rid of that. Also, if you know you're only looking for a single node, use find_by. I'm going to assume there's a facebook_id property on each user.
def check_friends
facebook.get_connection("me", "friends").each do |friend|
friend_node = User.find_by(facebook_id: friend["id"])
Friendship.create_friendship(user,friend_node) unless friend_node.blank?
end
end
The create_friendship method should should return true or false, so just make that the last statement of the method does that and you can return whatever it returns. That's as easy as this:
def self.create_friendship(user, friend_node)
Friendship.new(from_node: user, to_node: friend_node).save
end
create does not return true or false, it returns the resultant object, so chaining save to your new object will get you what you want. You don't need to set a variable there unless you plan on using it more within the method.
At this point, you can easily add an after_create callback to your ActiveRel model that will do something on from_node, which is always the User you just created. You can update the user's properties however you need to from there. Controlling this sort of behavior is exactly why ActiveRel exists.
I'd probably rework it a bit more, still. Start by moving your facebook stuff into a module. It'll keep your User model cleaner and more focused.
# models/concerns/facebook.rb
module Facebook
extend ActiveSupport::Concern
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
end
# now back in User...
class User
include Neo4j::ActiveNode
include Facebook
# more code...
end
It's really easy for your models to become these messy grab bags. A lot of blogs will encourage this. Fight the urge!
This should be a good start. Let me know if you have any questions or if I screwed anything up, there's a lot of code and it's possible I may need to clarify or tweak some of it. Hope it helps, though.
I am using the obfuscate_id gem ( https://github.com/namick/obfuscate_id ).
We obfuscate ID's by inserting one line into the top of each model:
obfuscate_id
It works great and as expected. My ID's are obfuscated.
However, as part of some logic in my ApplicationController, I have some logic to check the current user and each controller has access to these methods as helpers:
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
However, I get an error:
Couldn't find User with id=5164061535
It doesn't seem to be able to convert the obfuscated ID back to its normal form for a find().
How can I get the controllers to recognise this obfuscation that's made in each model.
My user model is like so:
class User < ActiveRecord::Base
# This part obfuscates the user ID
obfuscate_id
has_one :profile, dependent: :destroy
has_many :pins
has_many :replies, through: :pins
end
Any ideas how I can get the ApplicationController to recognise this? Doing find() in each controller itself is fine, but as ApplicationController doesn't have its own model, it doesn't seem to know of it.
Thanks,
Michael.
Try this...
#current_user ||= User.find(User.deobfuscate_id(session[:user_id])) if session[:user_id]
Weirdly enough, I ended up trying this:
#current_user ||= User.find_by_id(session[:user_id]) if session[:user_id]
And it worked! But, why?
User.find() in itself was not working with this gem. So, although it's now working, it concerns me a little as to why exactly.
If anyone could add anything here that'd be great.
Thanks!
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
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.
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