Ruby on Rails Active Record Query Interface - ruby-on-rails

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.

Related

Avoid explicit reloading of association

I have the following models:
class Model < ApplicationRecord
has_many :states
has_one :state, -> { order(created_at: :desc) }, class_name: 'State'
def update_state!(state)
states.create!(state: state)
end
end
class State < ApplicationRecord
belongs_to :model
end
If code is kept like this, then this is the behaviour:
m = Model.new(states: State.new(state: 'initial'))
m.save!
m.state.state # => "initial"
m.update_state!("final")
m.state.state # => "initial"
One workaround would be changing the Model#update_state! to:
def update_state!(state)
states.create!(state: state)
reload
end
Which kind of sucks.
Is there anyway to handle this without having to refresh the record? Something tells me I am missing something...
In this scenario, I might understand that Rails might not know how to relate :state with :states... However, I even tried making #state a plain method instead of an association, but I am still facing the same issue.
class Model < ApplicationRecord
has_many :states
def state
states.max_by(&:created_at)
end
def update_state!(state)
states.create!(state: state)
end
end
class State < ApplicationRecord
belongs_to :model
end
I would add a foreign key column to models as a "shortcut" to the latest record:
class AddStateToModels < ActiveRecord::Migration[6.0]
change_table :models do |t|
t.references :state
end
end
class Model < ApplicationRecord
has_many :states,
after_add: ->(state){ update_columns(state_id: state.id) }
belongs_to :state, class_name: 'State'
def update_state!(state)
states.create!(state: state)
end
end
This is a good read optimization if you are displaying a list of Model instances and there current state as it lets you do a very effective query:
#models = Model.eager_load(:current_state)
.all

Rails - How to model object privacy settings and associations

I have Forms created by Users. Every form is only visible to the creator. I would like to grant permission to other users to see a specific form. One could say I want to whitelist other users for a specific form.
Here's what I tried by creating a third model called "SharedForm".
app/models/form.rb
Class Form < ApplicationRecord
belongs_to :user
...
end
app/models/user.rb
Class User < ApplicationRecord
has_many :forms
has_many :forms, through: :sharedforms
...
end
app/models/shared_form.rb
Class SharedForm < ApplicationRecord
belongs_to :user
belongs_to :form
...
end
migration
class CreateSharedForms < ActiveRecord::Migration[5.0]
def change
create_table :shared_forms do |t|
t.integer :form_id, index: true
t.integer :user_id, index: true
t.timestamps
end
add_foreign_key :shared_forms, :users, column: :user_id
add_foreign_key :shared_forms, :forms, column: :form_id
end
end
In order to present both user forms and forms shared with the user I defined the index as:
app/controllers/forms_controller.rb
Class FormsController < ApplicationController
def index
#forms = Form.where(user_id: current_user.id)
shared = SharedForm.where(user_id: current_user.id)
#sharedforms = Form.where(id: shared)
end
end
This doesn't work.
Is there a way to access the records I need by user.forms and user.sharedforms respectively?
You can't use the same name for two associations as the latter will overwrite the former:
class User < ApplicationRecord
has_many :forms
# this overwrites the previous line!
has_many :forms, through: :sharedforms
...
end
Instead you need to give each association a unique name:
class User < ApplicationRecord
has_many :forms
has_many :shared_forms
has_many :forms_shared_with_me, through: :shared_forms
end
Note that the through option for has_many should point to an association on the model!
This would let you use:
class FormsController < ApplicationController
def index
#forms = current_user.forms
#shared = current_user.forms_shared_with_me
end
end

Rails Model: How to do the relation

I would like to do an app, but I am stuck on how to design the model. I have these models so far: User, Post, Tag, Like, Comment. I created for each a model, controller, view and migration file. So far so good, BUT now my question is should I just do the relations between them or create these models too: PostTag, PostLike, PostComment. You know, they would just have the relation between those, so when somebody deletes a tag in a post, he actually just deletes the relation and not the tag itself because the tag is maybe used in another post.
You don't need separate models for relations. Just add has_many and belongs_to to existing models and add needed fields (post_id, user_id etc.) to your database tables.
And tags won't disappear when you delete it from a post in case you won't write a code for a tag destroy. Nothing happens by itself.
P.S. I recommend to use acts-as-taggable-on gem for tags
You will only need to add a Tagging model, and use a polymorphic association to handle Likes. So, you need a User, Post, Comment, Like, Tag, and Tagging
You will not need a controller for Tags or Taggings or Likes
If you want users to be able to create posts and comments as well as like both posts and comments, then here is one way to configure your models:
User model:
class User < ActiveRecord::Base
has_many :posts
has_many :comments
has_many :likes, as: :likeable, dependent: :destroy
end
Post model:
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :likes, as: :likeable, dependent: :destroy
has_many :taggings
has_many :tags, through: :taggings
def self.tagged_with(name)
Tag.find_by_name!(name).posts
end
def self.tag_counts
Tag.select("tags.*, count(taggings.tag_id) as count").
joins(:taggings).group("taggings.tag_id")
end
def tag_list
tags.map(&:name).join(", ")
end
def tag_list=(names)
self.tags = names.split(",").map do |n|
Tag.where(name: n.strip).first_or_create!
end
end
end
Comment model:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Like model:
class Like < ActiveRecord::Base
belongs_to :likeable, polymorphic: true
end
Tag migration generator:
rails g model tag name
Tag model:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, through: :taggings
end
Tagging migration generator:
rails g model tagging tag:belongs_to post:belongs_to
Tagging model
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
The Post model assumes your tag uses the attribute :name
In order to access your tagged Posts add this route
get 'tags/:tag', to: 'posts#index', as: :tag
And change your index action in your Posts controller to this:
def index
if params[:tag]
#posts = Post.tagged_with(params[:tag])
else
#posts = Post.all
end
end
Finally, add a link to your Post Show view like this:
<%= raw #post.tags.map(&:name).map { |t| link_to t, tag_path(t) }.join(', ') %>

Any shortcut for updating join table when creating one of the models

For example, let us say we have
class User < ActiveRecord::Base
has_many :networks, through: user_networks
has_many :user_networks
end
class Network< ActiveRecord::Base
has_many :users, through: user_networks
has_many :user_networks
end
class UserNetwork < ActiveRecord::Base
belongs_to :user
belongs_to :network
end
Is there a shortcut for doing the following in a controller:
#network = Network.create(params[:network])
UserNetwork.create(user_id: current_user.id, network_id: #network.id)
Just curious and I doubt it.
This should work:
current_user.networks.create(params[:network])
But your code implies you are not using strong_parameters, or checking the validation of your objects. Your controller should contain:
def create
#network = current_user.networks.build(network_params)
if #network.save
# good response
else
# bad response
end
end
private
def network_params
params.require(:network).permit(:list, :of, :safe, :attributes)
end

Identifying the relationship with multiple one-to-one relationships between two models in rails

I have the following two models:
class Project < ActiveRecord::Base
has_one :start_date, :class_name => 'KeyDate', :dependent => :destroy
has_one :end_date, :class_name => 'KeyDate', :dependent => :destroy
and
class KeyDate < ActiveRecord::Base
belongs_to :project
Given a certain key date from the database related to a project:
#key_date = KeyDate.find(:first)
is there a way to introspect the relationship to check if the #key_date is related to the project as start_date or as end_date?
A nice way would be to use single table inheritance for the KeyDate class
class KeyDate < ActiveRecord::Base
belongs_to :project
end
class StartDate < KeyDate
end
class EndDate < KeyDate
end
class Project < ActiveRecord::Base
has_one :start_date, :dependent => :destroy
has_one :end_date, :dependent => :destroy
end
class CreateKeyDatesMigration < ActiveRecord::Migration
def up
create_table :key_dates do |t|
t.date :date
t.string :type #this is the magic column that activates single table inheritance
t.references :project
end
end
…
end
this lets you do
#key_date = KeyDate.find(:first)
#key_date.type # => "StartDate"
One clean way to do what you want is to create STI:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html
See one example I gave here:
Rails devise add fields to registration form when having STI
Just thinking aloud...
class KeyDate < ActiveRecord::Base
belongs_to :project
def start_date?
project.start_date == self
end
def end_date?
project.start_date == self
end
date_type
[:start_date, :end_date].find {|sym| send("#{sym}?") }
end
end
To be honest I can't see why you'd ever need this. Surely you're always going to have a handle on a project anyway?

Resources