Sort by latest created comment - ruby-on-rails

What i have created is a "active" field in my topics table which i can use to display the active topics, which will contain at first the time the topic was created and when someone comments it will use the comment.created_at time and put it in the active field in the topics table, like any other forum system.
I found i similar question here
How to order by the date of the last comment and sort by last created otherwise?
But it wont work for me, im not sure why it wouldn't. And i also don't understand if i need to use counter_cache in this case or not. Im using a polymorphic association for my comments, so therefore im not sure how i would use counter_cache. It works fine in my topic table to copy the created_at time to the active field. But it wont work when i create a comment.
Error:
NoMethodError in CommentsController#create
undefined method `topic' for
Topic.rb
class Topic < ActiveRecord::Base
attr_accessible :body, :forum_id, :title
before_create :init_sort_column
belongs_to :user
belongs_to :forum
validates :forum_id, :body, :title, presence: true
has_many :comments, :as => :commentable
default_scope order: 'topics.created_at DESC'
private
def init_sort_column
self.active = self.created_at || Time.now
end
end
Comment.rb
class Comment < ActiveRecord::Base
attr_accessible :body, :commentable_id, :commentable_type, :user_id
belongs_to :user
belongs_to :commentable, :polymorphic => true
before_create :update_parent_sort_column
private
def update_parent_sort_column
self.topic.active = self.created_at if self.topic
end
end

Didn't realise you were using a polymorphic association. Use the following:
def update_parent_sort_column
commentable.active = created_at if commentable.is_a?(Topic)
commentable.save!
end
Should do the trick.

Related

Prevent from raising ActiveRecord::RecordInvalid or adding twice on has_many association

I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.

ActiveAdmin / Formtastic sortable has_many through relationship

I may be missing something fundamental here, but I can't seem to get ActiveAdmin to work with a sortable has_many through relationship, with the ability to create new records.
So given the following models
class User < ActiveRecord::Base
has_many :user_videos
has_many :videos, through: :user_videos
accepts_nested_attributes_for :user_videos
accepts_nested_attributes_for :videos
...
end
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
accepts_nested_attributes_for :video
end
class Video < ActiveRecord::Base
has_many :user_videos
has_many :users, through: :user_videos
...
end
(I admit I'm throwing accepts_nested_attributes_for around somewhat in the hopes that something may work)
And Active Admin setup goes something like this (WIP of course):
f.inputs "User" do
f.has_many :user_videos, heading: 'Videos', sortable: :order, allow_destroy: true, new_record: 'New Record' do |v|
v.inputs for: :video do |video|
video.input :video_url
end
end
f.has_many :videos, heading: 'Videos', new_record: 'New Video' do |v|
v.input :video_url
end
end
f.actions
The first has_many on the :user_videos association does not seem to render any inputs. If there are records there, I can see that video.input :video_url is actually returning an li tag with label and input, however nothing gets rendered to the page. For new records the whole v.inputs bit does not get run (do I need to create the child records somehow there first?).
The second has_many will work in that you'll be able to add records, and update existing records, however it's impossible to sort as the order column is on the UserVideos model. I include this more as illustration than anything.
If anyone has any pointers for this, they would be most appreciated. :)
WHOA! I know I am late to the party, but this is a perfect opportunity to utilize the :delegate method!
Your UserVideo class would look something like this
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
validates_with VideoValidator
delegate :video_url, :video_url=, to: :video
end
Best of luck!
Since nobody seemed interested in tackling this, I took another approach - rather than trying to get ActiveAdmin / Formtastic to work with the existing model structure, I added getters and setters for the necessary field on the intersection model.
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
validates_with VideoValidator
def video_url
self.video = Video.create if video.nil?
self.video.video_url
end
def video_url=(video_url)
self.video = Video.create if video.nil?
self.video.video_url = video_url
# Video url is set via Active Admin, AA will not call save on the video as it does not realise it's changed
self.video.save! if video.present? and video.valid?
end
end
Doing this meant that Active Admin did not need to know about the Video model, and could just operate on the UserVideo model:
f.has_many :user_videos, heading: 'Videos', sortable: :order, allow_destroy: true, new_record: 'New Record' do |v|
v.input :video_url, :hint => (v.object.video.embed_code unless v.object.nil? or v.object.video.nil?)
end
If anyone has an actual solution rather than a work around, I'd love to hear it, but otherwise this is a possible solution for anyone searching for an answer to the same problem.

Rails: includes with polymorphic association

I read this interesting article about Using Polymorphism to Make a Better Activity Feed in Rails.
We end up with something like
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
end
Now, if two of those subjects are for example:
class Event < ActiveRecord::Base
has_many :guests
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
class Image < ActiveRecord::Base
has_many :tags
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
With create_activities defined as
def create_activities
Activity.create(subject: self)
end
And with guests and tags defined as:
class Guest < ActiveRecord::Base
belongs_to :event
end
class Tag < ActiveRecord::Base
belongs_to :image
end
If we query the last 20 activities logged, we can do:
Activity.order(created_at: :desc).limit(20)
We have a first N+1 query issue that we can solve with:
Activity.includes(:subject).order(created_at: :desc).limit(20)
But then, when we call guests or tags, we have another N+1 query problem.
What's the proper way to solve that in order to be able to use pagination ?
Edit 2: I'm now using rails 4.2 and eager loading polymorphism is now a feature :)
Edit: This seemed to work in the console, but for some reason, my suggestion of use with the partials below still generates N+1 Query Stack warnings with the bullet gem. I need to investigate...
Ok, I found the solution ([edit] or did I ?), but it assumes that you know all subjects types.
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
belongs_to :event, -> { includes(:activities).where(activities: { subject_type: 'Event' }) }, foreign_key: :subject_id
belongs_to :image, -> { includes(:activities).where(activities: { subject_type: 'Image' }) }, foreign_key: :subject_id
end
And now you can do
Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)
But for eager loading to work, you must use for example
activity.event.guests.first
and not
activity.part.guests.first
So you can probably define a method to use instead of subject
def eager_loaded_subject
public_send(subject.class.to_s.underscore)
end
So now you can have a view with
render partial: :subject, collection: activity
A partial with
# _activity.html.erb
render :partial => 'activities/' + activity.subject_type.underscore, object: activity.eager_loaded_subject
And two (dummy) partials
# _event.html.erb
<p><%= event.guests.map(&:name).join(', ') %></p>
# _image.html.erb
<p><%= image.tags.first.map(&:name).join(', ') %></p>
This will hopefully be fixed in rails 5.0. There is already an issue and a pull request for it.
https://github.com/rails/rails/pull/17479
https://github.com/rails/rails/issues/8005
I have forked rails and applied the patch to 4.2-stable and it works for me. Feel free to use my fork, even though I cannot guarantee to sync with upstream on a regular basis.
https://github.com/ttosch/rails/tree/4-2-stable
You can use ActiveRecord::Associations::Preloader to preload guests and tags linked, respectively, to each of the event and image objects that are associated as a subject with the collection of activities.
class ActivitiesController < ApplicationController
def index
activities = current_user.activities.page(:page)
#activities = Activities::PreloadForIndex.new(activities).run
end
end
class Activities::PreloadForIndex
def initialize(activities)
#activities = activities
end
def run
preload_for event(activities), subject: :guests
preload_for image(activities), subject: :tags
activities
end
private
def preload_for(activities, associations)
ActiveRecord::Associations::Preloader.new.preload(activities, associations)
end
def event(activities)
activities.select &:event?
end
def image(activities)
activities.select &:image?
end
end
image_activities = Activity.where(:subject_type => 'Image').includes(:subject => :tags).order(created_at: :desc).limit(20)
event_activities = Activity.where(:subject_type => 'Event').includes(:subject => :guests).order(created_at: :desc).limit(20)
activities = (image_activities + event_activities).sort_by(&:created_at).reverse.first(20)
I would suggest adding the polymorphic association to your Event and Guest models.
polymorphic doc
class Event < ActiveRecord::Base
has_many :guests
has_many :subjects
after_create :create_activities
end
class Image < ActiveRecord::Base
has_many :tags
has_many :subjects
after_create :create_activities
end
and then try doing
Activity.includes(:subject => [:event, :guest]).order(created_at: :desc).limit(20)
Does this generate a valid SQL query or does it fail because events can't be JOINed with tags and images can't be JOINed with guests?
class Activity < ActiveRecord::Base
self.per_page = 10
def self.feed
includes(subject: [:guests, :tags]).order(created_at: :desc)
end
end
# in the controller
Activity.feed.paginate(page: params[:page])
This would use will_paginate.

Validate before Posting a text or an image

I am working on a self-learning Rails application (the source code can be found here. I want to validate the presence of the content before posting a text or an image:
.
Those are my models or look below:
class Post < ActiveRecord::Base
belongs_to :user
default_scope { order ("created_at DESC")}
belongs_to :content, polymorphic: true
has_reputation :votes, source: :user, aggregated_by: :sum
end
class PhotoPost < ActiveRecord::Base
has_attached_file :image, styles: {
post: "200x200>"
}
end
class TextPost < ActiveRecord::Base
attr_accessible :body
end
Here are my controllers in case they have a relation with this. Any other files can be found in my Github account. I am sure it will be messy to copy the whole project (that is why I am giving links for the controllers and for my project).
So what I have tried so far. (I tried those on the Posts Model)
=> Using validates_associated
validates_associated :content, :text_post
and getting an error "undefined method `text_post' for #Post:0x517c848>"
=> Used validates
validates :content, :presence => true
and getting no error however a post is created with no text.
validates :body, :presence => true
and getting an error "undefined method `body' for #Post:0x513e4a8>"
If you need any other information please let me know and I will provide it asap.
Thank you.
It would seem you have quite a confusing model setup with some key missing relation rules. E.g. Polymorphic rule which is not being utilised and a has_many relation between User and Post with no sign a of a user_id value in the Post model. Here is how I would set it up:
User.rb
def User << ActiveRecord::Base
has_many :text_posts
has_many :photo_posts
end
TextPost.rb
def TextPost << ActiveRecord::Base
attr_accessible :body, :user_id
belongs_to :user
validates :body, :presence => true
end
PhotoPost.rb
def PhotoPost << ActiveRecord::Base
attr_accessible :image, :user_id
belongs_to :user
validates :file, :presence => true, :format => {
:with => %r{\.(gif|png|jpg)$}i,
:message => "must be a URL for GIF, JPG or PNG image."
}
end
Then in your view you would need to do:
<%= form_for #text_post do |f| %>
# ...
<% end %>
And in your controller you can modify the create method to include the current_user from devise and assign it to the new text post record (user_id attribute):
text_posts_controller.rb
def create
#text_post = current_user.text_posts.new(params[:text_post])
end
This adheres more to the DRY principle which Ruby on Rails excels at - you shouldn't be writing alot of code to just create a new record.
I would advise on reading up on some Ruby on Rails standard and best practises. You shouldn't need to create a method in the Dashboard Model in order to create a new TextPost or PhotoPost record. This is a very confusing way of going about it; instead you should be utilising the power of ActiveRecord relation.
I would advise checking out Railscasts. They have alot of fulfilling content.

Order by field in related model with ActiveRecord condition

I am trying to order by a field in a related model in Rails. All of the solutions I have researched have not addressed if the related model is filtered by another parameter?
Item model
class Item < ActiveRecord::Base
has_many :priorities
Related Model:
class Priority < ActiveRecord::Base
belongs_to :item
validates :item_id, presence: true
validates :company_id, presence: true
validates :position, presence: true
end
I am retrieving Items using a where clause:
#items = Item.where('company_id = ? and approved = ?', #company.id, true).all
I need to order by the 'Position' column in the related table. The trouble has been that in the Priority model, an item could be listed for multiple companies. So the positions are dependent on which company_id they have. When I display the items, it is for one company, ordered by position within the company. What is the proper way to accomplish this? Any help is appreciated.
PS - I am aware of acts_as_list however found it did not quite suit my setup here, so I am manually handling saving the sorting while still using jquery ui sortable.
You could use the includes method to include the build association then order by it. You just make sure you disambiguate the field you are ordering on and there are some things you should read up on here on eager loading. So it could be something like:
#items = Item.includes(:priorities).where('company_id = ? and approved = ?', #company.id, true).order("priorities.position ASC")
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
end
class Priority < ActiveRecord::Base
belongs_to :item
end
class Company < ActiveRecord::Base
has_many :items
end
#company = Company.find(params[:company_id])
#items = #company.items.joins(:priorities).approved.order(priorities: :position)
If I've understood your question, that's how I'd do it. It doesn't really need much explanation but lemme know if you're not sure.
If you wanted to push more of it into the model, if it's a common requirement, you could scope the order:
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
def self.order_by_priority_position
joins(:priorities).order(priorities: :position)
end
end
and just use: #company.items.approved.order_by_priority_position

Resources