sending specific data into a collection partial - ruby-on-rails-3

I have a User class with a has_many :messages and a Message class which belongs_to :user. In the Message controller's show action has the corresponding view:
<% if #messages.any? %>
<ol class="microposts">
<%= render partial: 'shared/message', collection: #messages %>
</ol>
<% end %>
And the shared/_message.html.erb template looks like this:
<li id="<%= message.id %>">
<span class="content"><%= message.body %></span>
<% user_id = message.from %>
<% user = User.find(user_id) %>
From: <%= user.name %>
</li>
I feel like the following two lines should be done in the Messages controller from what I read in tutorials on Rails:
<% user_id = message.from %>
<% user = User.find(user_id) %>
But how would I pass each message's corresponding from value (which stores user.id) into the partial?
thanks,
mike

So far i have understood that you have a from column in your messages table which stores the user_id. So if you call message.from it returns the user_id. Then you can do the following
class Message < AR::Base
belongs_to :user, :foreign_key=>'from'
end
In your controller
#messages = Message.all.includes(:user)
#It will load messages and will associated users eager loaded.
Now in your view you can call message.user.name to get the user's name directly. You dont need these following two lines in your view at all
<% user_id = message.from %>
<% user = User.find(user_id) %>
Let me know if ive missed anything.
UPDATE:
Here is what Ive tried with
User Model
class User < ActiveRecord::Base
attr_accessible :age, :name
has_many :sent_messages, :class_name=>'Message', :foreign_key => 'from'
has_many :received_messages,:class_name=>'Message', :foreign_key => 'user_id'
end
#Contains Data:
#<User id: 1, name: "p1", age: 27, ... ...>,
#<User id: 2, name: "p2", age: 25, ... ...
Message Model
class Message < ActiveRecord::Base
attr_accessible :body, :sender, :title, :receipent
belongs_to :sender, :class_name => 'User', :foreign_key => 'from'
belongs_to :receipent, :class_name => 'User', :foreign_key => 'user_id'
end
Now say p1 is sending a message to p2
p1.sent_messages.create(title: "test title", body: "test body", receipent: p2)
#Result:
#<Message id: 1, title: "test title", body: "test body", user_id: 2, from: 1 ... ... >
Now for a message, you can get both sender and receipent directly like
#messages = Message.includes(:sender, :receipent) # You can load only sender or only receipent
#In View
message.sender.name
#OR
message.receipent.name

Related

Ruby on Rails: SQL query is wrong while limiting user endorsements a month

I wanted to make that the user can endorse each user once a month and 3 users in total a month.
First of all, got a method in user model:
# Returns true if the current user is endorsing the other user.
def endorsing?(other_user)
endorsing.include?(other_user)
end
Want to slightly change it to check if the user already endorsed the user this month.
I belive it needs to look something like this:
def endorsing?(other_user)
endorsing.where(:created_at => (Time.zone.now.beginning_of_month..Time.zone.now)).include?(other_user)
end
But that is obviously wrong cause it gives me following SQL query:
SELECT "users".* FROM "users" INNER JOIN "endorsements"
ON "users"."id" = "endorsements"."endorsed_id"
WHERE "endorsements"."endorser_id" = ? AND ("users"."created_at" BETWEEN '2016-01-01 00:00:00.000000' AND '2016-01-26 17:15:53.700307') [["endorser_id", 1]]
the "users"."created_at" should be "endorsements"."created_at"
How do I do that?
Same counts for the limit problem I've got:
def endorsement_count_within_limit?
if endorser.endorsing.where(:created_at => (Time.zone.now.beginning_of_month..Time.zone.now)).count >= 3
errors.add(:base, "Exceeded endorse limit (3) this month")
end
end
Which gives:
SELECT COUNT(*) FROM "users" INNER JOIN "endorsements"
ON "users"."id" = "endorsements"."endorsed_id"
WHERE "endorsements"."endorser_id" = ? AND ("users"."created_at" BETWEEN '2016-01-01 00:00:00.000000' AND '2016-01-26 17:15:53.708638' [["endorser_id", 1]]
Same problem, users.created should be endorsements.created
I have no idea how to fix that, And bright ideas?
Below i'll paste my controllers, models and partials that use the methods to render forms for endorsing:
Endorsements model:
class Endorsement < ActiveRecord::Base
belongs_to :endorser, class_name: "User"
belongs_to :endorsed, class_name: "User"
validates :endorser_id, presence: true
validates :endorsed_id, presence: true
validates :comment, presence: true, length: { maximum: 140}
validate :endorsement_count_within_limit?, :on => :create
def endorsement_count_within_limit?
if endorser.endorsing.where(:created_at => (Time.zone.now.beginning_of_month..Time.zone.now)).count >= 3
errors.add(:base, "Exceeded endorse limit (3) this month")
end
end
end
Endorsements controller:
class EndorsementsController < ApplicationController
before_action :logged_in_user
def new
end
def create
#user = User.find(params[:endorsed_id])
comment = params[:endorsement][:comment]
current_user.endorse(#user, comment)
respond_to do |format|
format.html { redirect_to #user }
format.js
end
end
def destroy
#user = Endorsement.find(params[:id]).endorsed
current_user.unendorse(#user)
respond_to do |format|
format.html { redirect_to #user }
format.js
end
end
end
User model:
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
has_many :active_endorsements, class_name: "Endorsement",
foreign_key: "endorser_id",
dependent: :destroy
has_many :passive_endorsements, class_name: "Endorsement",
foreign_key: "endorsed_id",
dependent: :destroy
has_many :endorsing, through: :active_endorsements, source: :endorsed
has_many :endorsers, through: :passive_endorsements, source: :endorser
.
.
.
# Endorses a user.
def endorse(other_user, comment)
active_endorsements.create(endorsed_id: other_user.id, comment: comment)
end
# Unendorses a user.
def unendorse(other_user)
active_endorsements.find_by(endorsed_id: other_user.id).destroy
end
# Returns true if the current user is endorsing the other user.
def endorsing?(other_user)
endorsing.where(:created_at => (Time.zone.now.beginning_of_month..Time.zone.now)).include?(other_user)
end
private
.
.
.
end
Partials:
_endorse_form:
<% unless current_user?(#user) %>
<div id="endorse_form_<%= #user.id %>">
<% if current_user.endorsing?(#user) %>
<%= render partial: 'shared/unendorse' %>
<% else %>
<%= render partial: 'shared/endorse'%>
<% end %>
</div>
<% end %>
_endorse:
<%= form_for(current_user.active_endorsements.build, remote: true) do |f| %>
<% if f.object.endorsement_count_within_limit? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(f.object.errors.count, "error") %>.
</div>
<ul>
<% f.object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% else %>
<div><%= hidden_field_tag :endorsed_id, #user.id %></div>
<%= f.submit "Endorse", class: "btn btn-primary" %>
<%= f.text_field :comment, class: 'form-control' %>
<% end %>
<% end %>
_unendorse:
<%= form_for(current_user.active_endorsements.find_by(endorsed_id: #user.id),
html: { method: :delete },
remote: true) do |f| %>
<%= f.submit "Remove endorse", class: "btn" %>
<% end %>
If you need to look at other files it's available here with some parts missing:https://bitbucket.org/kramarz/pracainzynierska
try explicitly giving the columns for endorsments in your where statement
endorsing.where(%{
endorsments.created_at BETWEEN
'#{Time.zone.now.beginning_of_month.to_s(:db)}'
AND
'#{Time.zone.now.to_s(:db)}'
})
At first glance it looks like you need to specify active_endorsements or passive_endorsements where your examples are using endorsing, which references the endorsed user through the Endorsement.
This would all be a little less convoluted if the names were changed to better reflect what each object is. Of course, naming is much more difficult than it seems.
You might have better luck restructuring your relations like so:
class User < ActiveRecord::Base
has_many :outbound_endorsements, class_name: "Endorsement", foreign_key: "endorser_id", dependent: :destroy
has_many :inbound_endorsements, class_name: "Endorsement", foreign_key: "endorsed_id", dependent: :destroy
has_many :endorsed_users, through: :outbound_endorsements, source: :endorsed_user
has_many :endorsing_users, through: :inbound_endorsements, source: :endorsing_user
end
class Endorsement < ActiveRecord::Base
belongs_to :endorsing_user, class_name: "User"
belongs_to :endorsed_user, class_name: "User"
scope :current, -> { where(created_at: (Time.zone.now.beginning_of_month..Time.zone.now) }
end
As for the validation, this is more complex than validating data to be persisted by any single model. In these cases, it's really useful to put this logic in another object entirely:
class ValidatedEndorsement
attr_reader :endorsing_user, :endorsed_user
def initialize(endorsing_user, endorsed_user)
#endorsing_user, #endorsed_user = endorsing_user, endorsed_user
end
def valid?
# note: User -> Endorsement -> Scope -> Count = violation of the
# law of demeter; if this works well, refactor appropriately ;)
if #endorsing_user.outbound_endorsements.current.count >= 3
# no dice
return false
end
true
end
end
If this object is going to be used in response to a user request, it might be worth your while to consider making this object a FormObject that uses the underlying validations of an ActiveRecord model. This is easy to do with virtus, requiring only a few lines to change in the above ValidatedEndorsement.

Rails: multi level nested Forms (accepts nested attributes)

I am creating simple blog level application. below are my models.
class User < ActiveRecord::Base
attr_accessible :name,:posts_count,:posts_attributes , :comments_attributes
has_many :posts
has_many :comments
accepts_nested_attributes_for :posts , :reject_if => proc{|post| post['name'].blank?} , :allow_destroy => true
end
class Post < ActiveRecord::Base
attr_accessible :name, :user_id ,:comments_attributes
belongs_to :user
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
attr_accessible :content, :post_id, :user_id
belongs_to :user
belongs_to :post
end
I am trying to create user,post and comment in one form by using accepts_nested_attributes_for feature of rails. Below is my controller and view code.
Controller-----------
class UsersController < ApplicationController
def new
#user = User.new
#post = #user.posts.build
#post.comments.build
end
def create
#user = User.new(params[:user])
#user.save
end
end
Form----------
<%= form_for #user do |f| %>
<%= f.text_field :name %>
<%= f.fields_for :posts do |users_post| %>
<br>Post
<%= users_post.text_field :name %>
<%= users_post.fields_for :comments do |comment| %>
<%= comment.text_field :content %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
With the above code i am successfully able to create new user,post and comment but the problem is that i am not able to assign newly created user to newly created comment.when i checked the newly created comment into the database i got below result.I am getting user_id field value of "nil".
#<Comment id: 4, user_id: nil, post_id: 14, content: "c", created_at: "2014-05-30 09:51:53", updated_at: "2014-05-30 09:51:53">
So I just want to know how we can assign newly created comment to newly created user???
Thanks,
You will have to explicitly assign user_id for comments! You are nesting comments under posts, so comments would be having post_id assigned by default but though you are nesting comments under user form indirectly, there is no direct nesting of comments under user, so user_id remains blank in comments.
Try writing after create callback in Comment model to set user_id
In comment.rb
after_create{|comment|
comment.user_id = post.user_id
comment.save
}
Hope this helps :)

Limiting how often a user can post on a particular person's profile/wall in Rails

How can I limit a user to only to being able to post once or twice per day on a particular users's wall? I primarily want to do it in order to limit spam. My code for the wall, models, view, and controllers are below. I don't really know how to go about it as I'm new to rails but I know there is something time.now. I'm not exactly sure how to implement such a feature.
Class UsersController < ApplicationController
def show
#user = User.find(params[:id])
#first_name = #user.first_name
#last_name = #user.last_name
#wallpost = WallPost.new(params[:wall_post])
#showwallposts = #user.received_wallposts
end
def create
#wallpost = WallPost.create(params[:wall_post])
end
models
class WallPost < ActiveRecord::Base
attr_accessible :content, :receiver_id, :sender_id
belongs_to :receiver, :class_name => "User", :foreign_key => "receiver_id"
belongs_to :sender, :class_name => "User", :foreign_key => "sender_id"
end
class User < ActiveRecord::Base
has_many :sent_wallposts, :class_name => 'WallPost', :foreign_key => 'sender_id'
has_many :received_wallposts, :class_name =>'WallPost', :foreign_key => 'receiver_id'
in the view
<%= form_for(#wallpost, :url => {:action => 'create'}) do |f| %>
<%= f.hidden_field :receiver_id, :value => #user.id %>
<%= f.hidden_field :sender_id, :value => current_user.id %>
<%= f.text_area :content, :class => 'inputbox' %>
<%= f.submit 'Post', class: 'right btn' %>
<% end %>
You could create a custom validator which assures maximum DAILY_LIMIT posts have been created on that person's wall that day by that user:
class SpamValidator < ActiveModel::Validator
DAILY_LIMIT = 2
def validate(record)
if similar_posts_today(record).count >= DAILY_LIMIT
record.errors[:spam_limit] << 'Too many posts today!'
end
end
def similar_posts_today(record)
WallPost.where(receiver: record.receiver, sender: record.sender)
.where("DATE(created_at) = DATE(:now)", now: Time.now)
end
end
Then add that validation to your WallPost model:
validates_with SpamValidator
Then it will fail with a validation error when trying to create a wall post beyond the limit set in the constant. You need to handle this case in the create action in your controller. A simple (but not optimal in terms of user experience) way of handling this is:
def create
#wallpost = WallPost.new(params[:wall_post])
flash[:error] = "You've reached the daily posting limit on that wall." unless #wallpost.save
redirect_to user_path(#wallpost.receiver)
end
With that, it'll try to save the new wall post, if it is unable to, it'll set flash[:error] to the error message above. You'd need to show this on your show.html.erb page with <%= flash[:error] if flash[:error] %>.

3 model association in rails 3

i'm working on classified portal in which i have following 3 models
User (name,email)
Advertisement (title,body,user_id)
Bid (user_id,ad_id,price)
i have association between User and Advertisement as follow
class User < ActiveRecord::Base
has_many :advertisements
end
class Advertisement < ActiveRecord::Base
belongs_to :user
end
Now i wanna define Bid model , i guess this is how it should look like ,
class Bid < ActiveRecord::Base
belongs_to :user
belongs_to :advertisement
end
and i should add has_many :bids in User and Advertisement model as well(i think so),
Now my question is, say User is logged in and he wants to bid for some advertisement , so on advertisements show page how should i integrate this bid form, and please do let me how should i define routes for this .
Rails 3.2.13
Ruby 2.0.0p0
Thanks
Take a look at business logic of your app, there are two stories you have to implement
1) User can create advertisements (offers) (thats why you added user_id in Advertisement)
2) And user can bid on advertisements (thats simple has_many :through association)
class User < ActiveRecord::Base
has_many :bids
has_many :advertisements, :through => :bids
has_many :offers, class_name: "Advertisement", foreign_key: :user_id
end
class Advertisement < ActiveRecord::Base
belongs_to :author, class_name:"User", foreign_key: :user_id
has_many :bids
has_many :users, :through => :bids
end
class Bid < ActiveRecord::Base
belongs_to :user
belongs_to :advertisement
end
So now, if you want to get Advertisement author you need to call
a = Advertisement.last
a.author
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
=> #<User id: 1, created_at: "2013-05-31 06:13:20", updated_at: "2013-05-31 06:13:20">
a.users
User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "bids" ON "users"."id" = "bids"."user_id" WHERE "bids"."advertisement_id" = 2
=> [#<User id: 1, created_at: "2013-05-31 06:13:20", updated_at: "2013-05-31 06:13:20">, #<User id: 1, created_at: "2013-05-31 06:13:20", updated_at: "2013-05-31 06:13:20">]
See, it makes last select through joining bids, while first one just selecting from users table. Now it works like you expect it to work.
Anyway, answering your question:
Yes, you need to define new route like this:
resources :advertisements do
member do
post 'bid'
end
end
You need to create custom action in your AdvertisementsController which will do that:
def bid
#advertisement = Advertisement.find(params[:id])
current_user.bids << Bid.new(advertisement: #advertisement, value: params[:bid])
# you may also add some value column in bids table, thats up to you
redirect_to #advertisement
end
and form would be like this:
<%= form_tag(bid_advertisement_path(#advertisement), :method => "post",) do %>
<%= label_tag(:bid, "Make a bid:") %>
<%= text_field_tag(:bid) %>
<%= submit_tag("Submit") %>
<% end %>
If I am correct what you may want to have is a nested_form the gem for this can do exactly what you want. I say that you may have to use a nested form because by going by what you are saying a User has_many :bids which is correct. In order for the bids to accept the attributes from the user you will need to have accepts_nested_attributes_for :user on your Bids model. Tried to mock an example together for you
<%= form_for bid do |f| %>
<%= fields_for :user do |user_fields| %>
<%= user_fields.label :name %>
<%= user_fields.text_field :name %>
<% end %>
<%= f.label :product %>
<%= f.text_field :product %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Relevant links: 196 - Railscasts Nested Form

user and group join model validation

I have the models User and Group, and the join table Membership which uses has_many :through method. In my join table's form, I want the user to input a valid group name that an administrator has created to become a member of that group.
I have gotten the case where a valid name is entered to work but now I need some validation if they input a blank text box, or that the inputted group name exists in the database, I'd like some nice error messages. I thought this would be possible through some validates method?
membership partial _form.html.erb
<%= form_for(#membership) do |f| %>
<% if #membership.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#membership.errors.count, "error") %> prohibited this membership from being saved:</h2>
<ul>
<% #membership.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :group %><br />
<%= f.text_field :group %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
membership.rb
class Membership < ActiveRecord::Base
belongs_to :group
belongs_to :user
attr_accessible :user_id, :group_id
validates_uniqueness_of :group_id, :message => "can be only joined once", :scope => 'user_id'
validates_presence_of :group, :user
end
group.rb
class Group < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :users, :through => :subscriptions
validates :name, :presence => true, :uniqueness => true
attr_accessible :name, :expiry
end
So need some direction as how the validation happens because the above validation in the membership and group models doesn't work, I get the error for both empty text box or name not in the database...
Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
Edit: Added controller code below
def create
#group = Group.find_by_name(params[:membership][:group])
#membership = current_user.memberships.build(:group_id => #group.id)
respond_to do |format|
if #membership.save
format.html { redirect_to membership_url, :notice => 'Membership was successfully created.' }
format.json { render :json => #membership, :status => :created, :location => #membership }
else
format.html { render :action => "new" }
format.json { render :json=> #membership.errors, :status => :unprocessable_entity }
end
end
end
Add the following to your group model:
validate_uniqueness_of :name, :message => "a group already exists with that name"
validate_presence_of :name
The validations are called for all tables you are inserting to.
Edit:
Change your controller like this:
#membership = current_user.memberships.build(:group => #group)
You won't get the id called on nil error anymore. And if #group is nil, the validation in your membership will pick it up on the save attempt.
I solved this question by changing the text input box to a select field, and will add a password field to the model to solve my issue of unwanted users joining the group.
def create
#groups = Group.current
#group = Group.find_by_name(params[:membership][:group])
#membership = current_user.memberships.build(:group_id => #group.id)
After which my model validation works now below
validates_uniqueness_of :group_id, :message => "can be only be joined once", :scope => 'user_id'