I am making a Instagram-like clone where users upload pics that get voted on.
How can I refactor this code?
How can I default scope it by decreasing percent?
class Picture < ActiveRecord::Base
mount_uploader :picture, PictureUploader
has_and_belongs_to_many :tags
has_many :votes, :dependent => :destroy
def vote_count
return self.votes.count
end
def up_votes
return self.votes.where('up like ?', true).count.to_f
end
def down_votes
return self.votes.where('up like ?', false).count.to_f
end
def percent
return self.up_votes/self.vote_count
end
end
FYI my Vote model just tracks the picture and user id and has up:boolean, :default => false
This all seems less concise than it could be, how would you write this better?
class Picture < ActiveRecord::Base
mount_uploader :picture, PictureUploader
has_and_belongs_to_many :tags
has_many :votes, :dependent => :destroy
def vote_count
return self.votes.count
end
def votes_status(status)
return self.votes.where('up like ?', status).count.to_f
end
def percent
return self.votes_status(true)/self.vote_count
end
end
Related
In my movie model, I have the following code.
class Movie < ApplicationRecord
belongs_to :user
has_many :reviews, dependent: :destroy
has_many :users, through: :reviews
validates :title, :description, :movie_length, :director, :rating, presence: true
end
def self.show_order_desc
self.review.order("created_at DESC")
end
And I'm calling this in my movie controller show action where I have my reviews for that movie.
def show
#reviews = show_order_desc
end
is this the right way? is this a scope? it's working but I have a review coming along and I want to be sure to past this feature.
I think you are trying to fetch all reviews of a movie in descending order of created_at. You can follow bellow codes.
# app/models/movie.rb
class Movie < ApplicationRecord
belongs_to :user
has_many :reviews, dependent: :destroy
has_many :users, through: :reviews
validates :title, :description, :movie_length, :director, :rating, presence: true
end
# app/models/review.rb
class Review < ApplicationRecord
belongs_to :movie
belongs_to :user
scope :created_desc, -> { order(created_at: :desc) }
end
# app/controllers/movies_controller.rb
class MoviesController < ApplicationController
before_action :find_movie, only: [:show] # you can add other actions also
def show
#reviews = #movie.reviews.created_desc
end
# your other actoins
private
def find_movie
#movie = Movie.find(params[:id])
end
end
Here I have added one scope in Review model which will order reviews as per your requirement. Now in the show action I am fetching reviews of a movie first, then using the scope for against those reviews to order in descending order with created_at.
I have the following:
models/like.rb:
class Like
belongs_to :post
end
models/post.rb:
class Post
has_many :likes, dependent: :destroy
def self.popular
Like.group(:post_id).count << ???
end
end
I would like to make a scope that returns the most popular posts: posts with more than 20 likes, but I don't know how to make the conditional.
You can use counter_cache to do this. You will have to create an extra column, but it is more performatic when SELECTing.
models/like.rb
class Like < ActiveRecord::Base
belongs_to :post, counter_cache: true
end
models/post.rb
class Post < ActiveRecord::Base
has_many :likes, dependent: :destroy
def self.popular
where('likes_count > 20').order('likes_count DESC')
end
end
Then create the migration:
class AddLikesToPosts < ActiveRecord::Migration
def change
add_column :posts, :likes_count, :integer, default: 0
end
end
And populate likes_count for your current Posts on rails console (only needed if you already have some created posts):
Post.find_each { |post| Post.reset_counters(post.id, :likes) }
After this, each time you create a new Like, the counter will be automatically incremented.
So the way I did things for these model set ups is a bit different then what you might actually do. How ever I did things like this:
Post Model
class Post < ActiveRecord::Base
belongs_to :blog
belongs_to :user
has_and_belongs_to_many :tags, join_table: 'tags_posts', :dependent => :destroy
has_and_belongs_to_many :categories, join_table: 'categories_posts', :dependent => :destroy
has_many :comments, :dependent => :destroy
validates :title, presence: true
def has_tag?(tag_name)
tags.where(name: tag_name).any?
end
def tag_names
tags.pluck(:name)
end
def tag_names=(names)
self.tags = names.map{ |name| Tag.where(name: name).first }
end
def tag_name=(tag_name)
single_tag = [tag_name]
self.tag_names = single_tag
end
def has_category?(category_name)
categories.where(name: category_name).any?
end
def category_names
categories.pluck(:name)
end
def category_names=(names)
self.categories = names.map{ |name| Category.where(name: name).first }
end
def category_name=(category_name)
single_category_name = [category_name]
self.category_names = single_category_name
end
def user=(id)
user = User.find_by(id: id)
self.user_id = user.id if user
end
end
The above allows us to assign tags and categories and a post to a user (the last part is being refactored out as we speak). You can also get all tags and categories for a post and see if that post has a particular category.
Now what I want to do, in the tags model (for now) is get all the posts that a tag belongs to. But I am not sure how to do that ...
this is my tags model:
Tags Model
class Tag < ActiveRecord::Base
belongs_to :blog
validates :name, presence: true, uniqueness: true
end
How do I accomplish what I want?
I am not sure how to do this with has_and_belong_to_many. However, it would be pretty easy using has many through. By Rails conventions, the same of your join table should be tag_posts or post_tags (the first model is singular).
In your Post model:
has_many :tag_posts
has_many :tags, :through => :tag_posts
Then in your Tag model, a similar setup:
has_many :tag_posts
has_many :posts, :through => :tag_posts
Finally, you would create a TagPost model
belongs_to :tag
belongs_to :post
After that, calling tag.posts should return all posts for a given tag.
I have Product model like this:
class Product < ActiveRecord::Base
has_many :images, :class_name => 'ProductImage', :order => 'position DESC', :dependent => :destroy
def image_thumb
images.first.image.thumb.url
end
def image
images.first.image.url
end
end
ProductImage model:
class ProductImage < ActiveRecord::Base
attr_accessible :image, :position, :product_id, :title
belongs_to :product
default_scope order('position ASC')
mount_uploader :image, ProductImageUploader
end
Uploader model:
class ProductImageUploader < CarrierWave::Uploader::Base
...
def default_url
asset_path([version_name, "default.jpg"].compact.join('_'))
end
end
But if I don't upload any image for product, I will get 'nil' in image_thumb and image methods. How get default_url if no one image uploaded and relation between Product and ProductImage models are one-to-many?
If you don't have access to an uploader, an uploader can't help you, so you have to do it by hand. You're already using a presenter (more or less), so this is extremely easy:
def image_thumb
if images.any?
images.first.image.thumb.url
else
asset_path("default.jpg")
end
end
def image
if images.any?
images.first.image.url
else
asset_path("thumb_default.jpg")
end
end
Similar code works for belongs_to relationships.
What is the value you are getting for default_url?
Maybe the path is not evaluating correctly.
Try something like this (of course correct the path below to what it should be):
class ProductImageUploader < CarrierWave::Uploader::Base
...
def default_url
"/assets/" + [version_name, "default.jpg"].compact.join('_')
end
end
I have a Post model:
class Post < ActiveRecord::Base
attr_accessible :title, :content, :tag_names
has_many :votes, :as => :votable, :dependent => :destroy
has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
attr_writer :tag_names
after_save :assign_tags
def tag_names
#tag_names || tags.map(&:name).join(" ")
end
private
def assign_tags
self.tags = []
return if #tag_names.blank?
#tag_names.split(" ").each do |name|
tag = Tag.find_or_create_by_name(name)
self.tags << tag unless tags.include?(tag)
end
end
end
a Tag model:
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :posts, :through => :taggings
has_many :subscriptions
has_many :subscribed_users, :source => :user, :through => :subscriptions
def tag_posts_count
"#{self.name} (#{self.posts.count})"
end
end
and a Vote model:
class Vote < ActiveRecord::Base
belongs_to :votable, :polymorphic => true
belongs_to :user
before_create :update_total
protected
def update_total
total_average = self.votable.total_votes
self.votable.update_attribute(:total_votes, total_average + self.polarity)
end
end
As you can see in this last model, it updates the :total_votes attribute of a post each time a new instance of Vote is created.
For some reason, this action is triggering the after_save :assign_tags action in the post model. So each time I create a vote for a post this part is called:
def assign_tags
self.tags = [] // I added this so that the previous array of tags are removed before the new ones are added.
and all the tags are removed. I want the assign_tags to only be triggered when a post is being edited. Any suggestions to fix this?
EDIT:
votes_controller:
class VotesController < ApplicationController
def vote_up
#votable = params[:votable_type].constantize.find(params[:id])
if #votable.votes.exists?(:user_id => current_user.id)
#notice = 'You already voted'
else
#vote = #votable.votes.create(:user_id => current_user.id, :polarity => 1)
#votable.reload
end
respond_to do |format|
format.js
end
end
update post without callback inside vote model -
def update_total
self.votable.total_votes += self.polarity
self.votable.send(:update_without_callbacks)
end
OR
you can use `update_column(name, value) it skips validations & callbacks -
def update_total
self.votable.update_column(:total_votes, votable.total_votes + polarity)
end
Here is revised vote model
class Vote < ActiveRecord::Base
belongs_to :votable, :polymorphic => true
belongs_to :user
after_create :update_total
protected
def update_total
if votable && votable.is_a?(Post)
self.votable.update_column(:total_votes, votable.total_votes + self.polarity)
end
end
end
Use after_commit as the callback