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

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] %>.

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.

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'

Querying for a relationship in ruby on rails and update results via Ajax

I have a ROR app that has many players, and many proposed games. The games display on a feed and a player can decide to hide them from this feed. The hidden function works like this:
in player.rb:
has_many :hides, :foreign_key=> "hider_id",
:dependent => :destroy
has_many :hidees, :through => :hides
def hidden?(hidee)
hides.find_by_hidee_id(hidee)
end
def hide!(hidee)
hides.create!(:hidee_id => hidee.id)
end
def unhide!(hidee)
hides.find_by_hidee_id(hidee).destroy
end
hides_controller.rb
class HidesController < ApplicationController
def create
#game = Game.find(params[:hide][:hidee_id])
current_profile.hide!(#game)
redirect_to :back
end
def destroy
#game = Hide.find(params[:id]).hidee
current_profile.unhide!(#game)
redirect_to :back
end
end
hide.rb
class Hide < ActiveRecord::Base
attr_accessible :hidee_id
belongs_to :hider, :class_name => "Player"
belongs_to :hidee, :class_name => "Game"
validates :hider_id, :presence => true
validates :hidee_id, :presence => true
end
game.rb
has_many :reverse_hides, :foreign_key => "hidee_id",
:class_name => "Hide",
:dependent => :destroy
has_many :hiders, :through => :reverse_hides
routes.rb
resources :games do
member do
post :publish
post :unpublish
get :view
get :hidees, :hiders
end
I'm trying to do two things: 1. Write a function that would allow me to hide a game from the feed if a relationship between hidden relationship between game and player exits, and 2. write a "show hidden" button that would allow me to return all projects that were "hidden" by the player.
So far with part 1. I have the following code in the view, and while this does the trick in terms of setting up the relationships, it does not "hide" the game from the feed--I'm guessing I would need ajax for that??
- if current_profile.hidden?(game)
= form_for current_profile.hides.find_by_hidee_id(game), :html => { :method => :delete } do |f|
= f.submit "Unhide", :title => "Unhide this game."
- else
= form_for current_profile.hides.build(:hidee_id => game.id) do |f|
= f.hidden_field :hidee_id
= f.submit "Hide", :title => "Hide this game"
Thank you so much for viewing this, I know it's quite long, but I would appreciate any help you could offer. Also, thank you for you time.

How to capture a comment's author then display in tooltip?

I have a form attached to profiles where short comments can be submitted. I want to capture the author's name though so I can display it in a tooltip when hovering over the comment's body.
In my create method in the controller I have:
def create
#comment = Comment.new(params[:comment])
#comment.save!
redirect_to profile_path(#comment.profile)
end
Inside my migration:
t.timestamps
t.integer :profile_id
t.string :author_id
t.string :body
Profile model:
belongs_to :user
accepts_nested_attributes_for :user
has_many :comments
Comment model:
belongs_to :profile
ProfilesController:
def show
#user = User.find(params[:id])
#profile = user.profile
#superlative = #profile.superlatives.new
end
And my form:
<%= form_for #comment do |f| %>
<%= f.hidden_field :profile_id, :value => #profile.id %>
<%= f.hidden_field :author_id, :value => "#{current_user.profile.first_name} #{current_user.profile.last_name}" %>
<%= f.text_field :body %>
<%= f.submit 'Add new' %>
<% end %>
I was thinking of linking the :author_id to current_user.profile.id and using that association to display :first_name and :last_name which are attributes of the profile. Or is there a simpler, better way?
UPDATE: I got it to display the name though I'm still curious if there's a better way.
Your solution looks fine, but I'd store the User (or whatever class current_user returns) instead of the Profile:
In app/models/comment.rb:
class Comment < ActiveRecord::Base
belongs_to :profile
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
... rest of the code ...
end
You then change your migration to:
t.integer :author_id
and your controller method to:
def create
#comment = Comment.new(params[:comment].merge(:author_id => current_user.id))
#comment.save!
redirect_to profile_path(#comment.profile)
end
In your view (I used the title attribute do create a tooltip, but feel free to use whatever method you like):
<div class="comment" title="<%= #comment.author.profile.first_name %> <%= #comment.author.profile.last_name %>">
<%= #comment.body %>
</div>
I would suggest something like this:
In your routes.rb create a nested resource for comments
resources :users do
resources :comments
end
In your User model
class User
has_many :comments
end
In your Comment model
class Comment
belongs_to :user
end
In your CommentsController in the new and create methods
#comment = User.find(params[:user_id]).comments.new(params[:comment])
So the comment automagically gets created as belonging to that User and you don't have to pass anything around.
Then, in your Comment view, you could just call its owners name
#comment.user.first_name

rails3 is not adding the 2nd id to my table

the problem I'm having is company_id is not been save to the details table
I know the company_id is there I check it using <%= debug(params[:id])%> on the form before adding all company details information but for some reason is saving everything else but the company_id
##user.rb
has_one :company
##company.rb
belongs_to :user
has_one :detail
##detail.rb
belongs_to :user
##details controller
def new
#detail = Detail.new
user_id = session[:user_id]
company_id = params[:id]
end
def create
#detail = Detail.new(params[:detail])
#detail.user_id = session[:user_id]
#detail.company_id = params[:id]
end
###settings.html.erb
### this is where i make sure that company_id gets pass with the url
link_to 'New Detail', {:controller => 'details', :action =>'new', :id => company.id }, :class => 'plus'
#####routes
resources :details
resources :companies
resources :users
resources :sessions
I know this may not look pretty or proper if you know a better way please let me know...thanks in advance.
I notice something immediately. You may need to fix your associations first. Assuming, a user has one company and a company has one detail.
##user.rb
has_one :company
##company.rb
belongs_to :user
has_one :detail
##detail.rb
belongs_to :user
Should be:
##user.rb
has_one :company
##company.rb
belongs_to :user
has_one :detail
##detail.rb
belongs_to :company
Although, depending on your domain requirements. I would normally have it as: User has_many Companies. And since details is 1:1 with company, I would include all the details inside company.
I didn't realize this until later but I need it pass values to from on the view to the from like so
<% #companies.each do |company| %>
<%= link_to 'New Detail', {:controller =>'details', :action => 'new', :company_id => company.id}, :class => 'plus' %>
<% end %>
and I need it to collect that value on the from...like so
<%= form_for(:detail, :url =>{:action => 'create', :company_id => #company.id}) do |f| %>
...(values)
<% end %>