Updating extra column in a has_many, :through table with Coffeescript - ruby-on-rails-3

I've got a rather simple setup here: A Doc model, a Publication model, and an Article model.
doc.rb
class Doc < ActiveRecord::Base
attr_accessible :article_ids,:created_at, :updated_at, :title
has_many :publications, dependent: :destroy
has_many :articles, :through => :publications, :order => 'publications.position'
accepts_nested_attributes_for :articles, allow_destroy: false
accepts_nested_attributes_for :publications, allow_destroy: true
end
publication.rb
class Publication < ActiveRecord::Base
attr_accessible :doc_id, :article_id, :position
belongs_to :doc
belongs_to :article
acts_as_list
end
article.rb
class Article < ActiveRecord::Base
attr_accessible :body, :issue, :name, :page, :image, :article_print, :video, :id
has_many :publications
has_many :docs, :through => :publications
end
The Doc form lets users select and order a number of articles:
...
<% #articles.each do |article| %>
<span class="handle">[drag]</span>
<%= check_box_tag("doc[article_ids][]", article.id, #doc.articles.include?(article), :class => "article_chooser" ) %>
<a id="<%= article.id %>" class="name"><%= article.name %></a>
<% end %>
...
This Coffeescript saves the order on drag:
jQuery ->
$('#sort_articles').sortable(
axis: 'y'
handle: '.handle'
update: ->
$.post($(this).data('update-url'), $(this).sortable('serialize'))
);
And this is the sort method in docs_controller.rb:
def sort
Article.all.each_with_index do |id, index|
Publication.update_all({position: index + 1}, {id: id})
end
render nothing: true
end
I've followed this sortable lists Rails cast and it all works well until I update a Doc record because re-ordering isn't saved on update. I've come to the conclusion that it's because my sortable field is on the association table (i.e., publications), but because of the way the app works it has to be.
I've been doing some research here and found this question whose answer comes close, but because I've got a Coffeescript action saving the record in the first place it doesn't work.
Any help would be excellent, I'm really stuck.

Because I struggled for a long time with this I'm putting my stupidly simple solution here. Hopefully it helps someone else.
It's as simple as deleting the old join table records on update before the changes are saved, i.e.:
docs_controller.rb
def update
#doc = Doc.find(params[:id])
#doc.publications.destroy_all
respond_to do |format|
if #doc.update_attributes(params[:doc])
format.html { redirect_to share_url(#doc) }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #doc.errors, status: :unprocessable_entity }
end
end
end
Boom.

Related

How to create new records with nested attributes in a ruby on rails "has many through" relationship?

I need some advice on building a has many through relationship between USER, THING and EXTRA models.
My USER model is slightly modified inside Devise gem and is noted as Creator whereas other models belonging to USER receive :created_things form.
In my app, USERS create THINGS can later add EXTRAS to their THINGS.
I chose has many through because I want to have unique data on all three models and be able to call both THINGS and EXTRAS from the USER "CREATOR" model.
I have built this many different ways and after 10 years of solving my problems by reading stackoverflow, I am finally submitting this request for support! Thank you for your help.
I have tried creating user and extra references on the THING model and declaring nested attributes in the USER and THING model. I have tried several examples from stackoverflow inside the create and new methods but nothing seems to work.
class User < ApplicationRecord
has_many :created_things, class_name: Thing, foreign_key:
:creator_id, :dependent => :destroy
has_many :extras, through: :created_things
accepts_nested_attributes_for :extras, :reject_if => :all_blank,
allow_destroy: true
class Thing < ApplicationRecord
belongs_to :creator, class_name: User
has_many :extras
accepts_nested_attributes_for :extras, :reject_if => :all_blank,
allow_destroy: true
class Extra < ApplicationRecord
belongs_to :creator, class_name: User, inverse_of: :thing
belongs_to :created_things
Members Index.html.erb
<% if thing.extras.exists? %>
<% thing.extras.each do |extra| %>
<%= extra.title %> <%= link_to "[+]", edit_extra_path(extra) %>
<% end %>
<% else if thing.extras.empty? %>
<%= link_to "+1 EXTRA", new_extra_path(current_user) %>
<% end %>
<% end %>
class MembersController < ApplicationController
before_action :authenticate_user!
def index
#user = current_user
#created_extras = #user.extras
#created_things = #user.created_things
end
class ExtrasController < ApplicationController
def new
#extra = Extra.new
end
def create
#extra = current_user.extras.build(extra_params)
if #extra.save
I am able to create a new EXTRA but the :thing_id remains nul as it does not display when called on the show extra view. Therefore I am not surprised that when I return to the member index page that my thing.extras.exists? call is returning false and the created extra never displays under the THING view. My attempts to modify the extra controller have failed and I some of my reading sugested the extras controller is not necessary in this relationship so I am really at a loss on how this is built. I'm assuming I am missing something in new and create methods maybe in things or user controller? Perhaps I'm missing something in routes resources? Any advice is greatly appreciated. Thank you.
Ok, I figured it out. I really didn't need has many through for this model and I did a lot of testing of the syntax on each model.rb and in the end was able to figure it out from this stackoverflow . . .
[Passing parent model's id to child's new and create action on rails
Here are my the various parts of setting up a has many and belongs to relationship with nested attributes.
class Thing < ApplicationRecord
belongs_to :creator, class_name: User
has_many :extras, inverse_of: :thing, :dependent => :destroy
accepts_nested_attributes_for :extras, allow_destroy: true
class Extra < ApplicationRecord
belongs_to :thing, inverse_of: :extras
extras_controller.rb
class ExtrasController < ApplicationController
def new
#extra = Extra.new(thing_id: params[:thing_id])
end
def create
#user = current_user
#extra = Extra.new(extra_params)
#extra.user_id = #user.id
if #extra.save
flash[:success] = "You have added a new Extra!"
redirect_to #extra #extras_path later
else
flash[:danger] = "The form contains errors"
render :new
end
end
edit.html.erb things
<% if #thing.extras.exists? %>
<p>current extras associated with <%= #thing.title %>: </p>
<% #thing.extras.each do |extra| %>
<p><%= extra.title %> <%= link_to "[+]", edit_extra_path(extra) %>
/ <%= link_to "[-]", extra_path(extra), method: :delete %> </p>
<% end %>
<% end %>
<%= link_to "+1 EXTRA", new_extra_path(thing_id: #thing.id) %>
<%= render 'things/form' %>

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.

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

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.

2 models in a form in rails 3

I am very new to rails development.
I am creating a simple backend for my portfolio site.
I am not sure about the title of this question. A previous question I asked maybe too convoluted. So I am simplifying it.
Im using 3 models: Post, Attachment, Attachment_Category
I have a form that I use to:
Draft the post with a title, content and a category.
Display attachment categories in a drop down (slideshow, image, video)
Upload the attachment(s).
I have implemented steps 1 and 2.
For step 3: I want it so that when I finally hit submit on the form, the attachment_category_id is saved to the attachment table.
I have the following relationships:
Post.rb
class Post < ActiveRecord::Base
has_many :attachment_categories, :through => :attachments
has_many :attachments,:dependent => :destroy
accepts_nested_attributes_for :attachments
validates_presence_of :title, :content, :category
end
Attachment.rb
class Attachment < ActiveRecord::Base
belongs_to :post
belongs_to :attachment_category
#paperclip
has_attached_file :photo, :styles =>{
:thumb => "100x100#",
:small => "400x400>"
}
end
Attachment_category.rb
class AttachmentCategory < ActiveRecord::Base
has_many :posts , :through => :attachments
has_many :attachments
validates :category_name, :presence =>true
end
So I have accomplished Steps 1, parts of step 2 and step 3.
With my solution, I am able to upload just one attachment.
But it works: The attachment gets saved to the Attachments table with the post_id and the attachment_category_id.
The following code is from _form.html.erb which gets sent to post_controller.rb.
Truncated code:
.....
<%= f.fields_for :attachments do |attach| %> <br>
<%= attach.collection_select :attachment_category_id, AttachmentCategory.all, :id, :category_name %>
<%= attach.file_field :photo %> <br>
<% end %>
.....