How to unscope multiple models in rails? - ruby-on-rails

I am trying to unscope multiple model as below
User Model which has acts_as_paranoid
class User
acts_as_paranoid
has_one :category
has_one :brand
has_one :item
INDEXED_FIELDS = {
only: [:name],
include: {
category: { only: [:name] },
item: { only:[:name] },
brand: { only: [:name]},
}
}
def custom_json
Category.unscoped do
Item.unscoped do
Brand.unscoped do
self.as_json(INDEXED_FIELDS)
end
end
end
end
end
User model has following association which also has acts_as_paranoid
Sample Category model, Brand and Item model have same code
class Category
acts_as_paranoid
belongs_to :user
end
Can I do this dynamically with 'N' number of models, like iterating over array as below
def custom_json
[Category, Item, Brand].each do
# do unscoping
end
end
Association looks like

I think the approach you may have is to unscope the class manually, by setting default_scopes to [], and then putting it back.
classes_to_unscope = [Category, Item, Brand]
# remove default_scopes, saving them in previous_scopes
previous_scopes = classes_to_unscope.map do |klazz|
scopes = klazz.default_scopes
klazz.default_scopes = []
scopes
end
self.as_json(INDEXED_FIELDS)
# put default_scopes back
classes_to_unscope.each_with_index do |klazz, i|
klazz.default_scopes = previous_scopes[i]
end

As extra method:
def unscope_all(*models, &block)
# the order does not matter, but preserve it
blocks = [block] + models.reverse.map do |model|
proc do |inner_block|
model.unscoped { inner_block.call }
end
end
blocks.inject do |inner, outer|
proc { outer.call(inner) }
end.call
end
Then you would use it:
unscope_all(Category, Item, Brand) do
# do unscoping
end
unscoped pitfall: when leaving the block you loose the "unscopability", so make sure you don't return a relation (it won't be unscoped). Instead you have to resolve it in the block (e.g. by returning an array where(...).to_a.

Related

How to merge different keys?

I have two Model classes
class Book
# attributes
# title
# status
# author_id
belongs_to :author
enum status: %w[pending published progress]
end
class Author
# attributes
has_many :books
end
I have an activerecord that return a list of books
The list
[<#Book><#Book><#Book>]
I use this function to group them
def group_by_gender_and_status
books.group_by { |book| book.author.gender }
.transform_values { |books| books.group_by(&:status).transform_values(&:count) }
end
and the outcome is
{
"female"=>{"progress"=>2, "pending"=>1, "published"=>2},
"male"=>{"published"=>3, "pending"=>4, "progress"=>4}
}
How do I merge progress and pending and name the key pending? so it would look like this
{
"female"=>{"pending"=>3, "published"=>2 },
"male"=>{"pending"=>8, "published"=>3, }
}
I prefer to use the group_by method vs the group by SQL for a reason. Thanks
def group_by_gender_and_status
books.group_by { |book| book.author.gender }.
transform_values do |books|
books.group_by { |book| book.status == 'progress' ? 'pending' : book.status }.
transform_values(&:count)
end
end

Destroy deep associated object on parent save, using assign_attributes

I want to delete an deep associated record inside assign_attributes.
Screen is the only object I need to save, but the deep associated NoteMember object should get deleted on save of Screen object, if params[:delete] is true for that particular NoteMember object.
Following is the table structure:
MODELS
class Screen < ActiveRecord::Base
has_many :alerts
accepts_nested_attributes_for :alerts
end
class Alert < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :notes
end
class Note < ActiveRecord::Base
has_many :note_members
accepts_nested_attributes_for :note_members
end
class NoteMember < ActiveRecord::Base
end
CONTROLLER
s = Screen.where(id: <some_id>).first
alert_attrs = []
params[:alerts].each do |a|
notes_attrs = []
params[:notes].each do |n|
note_member_attrs = []
params[:note_members].each do |nm|
# if nm[:delete] = true, I need to delete the note member on saving Screen object
note_member_attrs.push({
id: nm[:id],
visibility: nm[:visibility]
})
end
notes_attrs.push({
id: n[:id],
description: n[:description],
note_members_attributes: note_member_attrs
})
end
alert_attrs.push({
id: a[:id],
name: a[:name]
notes_attributes: notes_attrs
})
end
s.assign_attributes(
alerts_attributes: alert_attrs
)
s.save!
How can this be achieved?
You can use rails built-in destroy functionality:
accepts_nested_attributes_for :note_members, allow_destroy: true
and pass
note_members_attributes: [ { _destroy: true, id: 123 }]
for note_members that need to be deleted.

change the name of nested keys received as parameter

Hi in rails the nested parameters are passed with attributes appended to the key's and then its is passed to permit
If I receive normal hash and want to append attribute to each nested key before permit is called
How to do that ?
"project":{
"project_name":"test",
"tentative_start_date":"2018-12-12",
"tentative_end_date":"2019-12-12",
"project_roles":[
{
"role_id":1,
"project_role_skills":[
{
"skill":{
"skill_type":"C++",
"id":2
}
}
],
"project_role_users":[
],
"role_end_date":"2018-12-12",
"role_start_date":"2018-12-12"
}
]
}
}
This is the request I received and I want to append attribute to project_roles,project_role_skill and so on so that rails accept that
Please anyone help
Have you tried this? Rails 4 - Strong Parameters - Nested Objects
If it doesn't work for you, can you please specify what is your form currently and what is your current method using permit?
TL;DR:
def params_with_deep_appended_nested_attributes(_params)
modified_params = _params.clone
_params.each do |k, v|
if v.is_a?(Array)
modified_params["#{k}_attributes"] = []
v.each_with_index do |vv, index|
if vv.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"][index] = params_with_deep_appended_nested_attributes(vv)
end
end
modified_params.delete(k)
elsif v.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"] = params_with_deep_appended_nested_attributes(v)
modified_params.delete(k)
end
end
modified_params
end
Usage Example:
# rails console
example_json_hash_request = {
"project": {
"project_name":"test",
"tentative_start_date":"2018-12-12",
"tentative_end_date":"2019-12-12",
"project_roles": [
{
"role_id":1,
"project_role_skills":[
{
"skill":{
"skill_type":"C++",
"id":2
}
}
],
"project_role_users":[
],
"role_end_date":"2018-12-12",
"role_start_date":"2018-12-12"
}
]
}
}
# simulate `params` value in the controller
params = ActionController::Parameters.new(example_json_hash_request)
modified_params = params_with_deep_appended_nested_attributes(params)
pp modified_params.permit!.to_hash
# {"project_attributes"=>
# {"project_name"=>"test",
# "tentative_start_date"=>"2018-12-12",
# "tentative_end_date"=>"2019-12-12",
# "project_roles_attributes"=>
# [{"role_id"=>1,
# "role_end_date"=>"2018-12-12",
# "role_start_date"=>"2018-12-12",
# "project_role_skills_attributes"=>
# [{"skill_attributes"=>{"skill_type"=>"C++", "id"=>2}}],
# "project_role_users_attributes"=>[]}]}}
Solution:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
# I added `= params` to default to the `params` value here in the controller
def params_with_deep_appended_nested_attributes(_params = params)
modified_params = _params.clone
_params.each do |k, v|
if v.is_a?(Array)
modified_params["#{k}_attributes"] = []
v.each_with_index do |vv, index|
if vv.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"][index] = params_with_deep_appended_nested_attributes(vv)
end
end
modified_params.delete(k)
elsif v.is_a?(ActionController::Parameters)
modified_params["#{k}_attributes"] = params_with_deep_appended_nested_attributes(v)
modified_params.delete(k)
end
end
modified_params
end
# ...
end
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def create
#project = Project.new(project_params)
if #project.save
# ...
else
# ...
end
end
def project_params
params_with_deep_appended_nested_attributes.require(:project_attributes).permit(
:project_name, :tentative_start_date, :tentative_end_date,
project_roles_attributes: [
:role_id, :role_end_date, :role_start_date,
project_role_skills_attributes: [
skill_attributes: [
:skill_type, :id
]
],
project_role_users_attributes: []
]
)
end
end
Don't forget to define the "nested attributes" in the models:
# app/models/project.rb
class Project < ApplicationRecord
has_many :project_roles
accepts_nested_attributes_for :project_roles
end
# app/models/project_role.rb
class ProjectRole < ApplicationRecord
belongs_to :project
has_many :project_role_skills
has_many :project_role_users
accepts_nested_attributes_for :project_role_skills
accepts_nested_attributes_for :project_role_users
end
# app/models/project_role_skill.rb
class ProjectRoleSkill < ApplicationRecord
belongs_to :project_role
belongs_to :skill
accepts_nested_attributes_for :skill
end
TODOs:
add cahing of the params_with_deep_appended_nested_attributes return value, to avoid running the code each time params_with_deep_appended_nested_attributes is going to be called.
This question interests me. I might find use to this code of mine in the future.

How to convert a method into a scope.

Hello I am trying to convert the method self.liked_by(user) into a scope. I am not entirely sure what my instructor is asking for so any interpretations on the question are greatly appreciated.
this is the method in question that I am supposed to turn into a scope.
def self.liked_by(user)
joins(:likes).where(likes: { user_id: user.id })
end
this is where the method appears in the model
class Bookmark < ActiveRecord::Base
belongs_to :user
belongs_to :topic
has_many :likes, dependent: :destroy
before_validation :httpset
validates :url, format: { with: /\Ahttp:\/\/.*(com|org|net|gov)/i,
message: "only allows valid URLs." }
def self.liked_by(user)
joins(:likes).where(likes: { user_id: user.id })
end
def httpset
if self.url =~ /\Ahttp:\/\/|\Ahttps:\/\//i
else
if self.url.present?
self.url = "http://"+ self.url
else
self.url = nil
end
end
end
end
And this is where the method is called in the controller
class UsersController < ApplicationController
def show
user = User.find(params[:id])
#bookmarks = user.bookmarks
#liked_bookmarks = Bookmark.liked_by(user)
end
end
Thanks for looking at my problem and have a good day.
#liked_bookmarks = Bookmark.liked_by(user)
In this line, in the same way you send the user parameter to a method, the same way you can send it to a scope.
class Bookmark < ActiveRecord::Base
---------
---------
scope :liked_by, ->(user) { joins(:likes).where(likes: { user_id: user.id }) }
---------
---------
end
the parameter you sent from the scope call can be accessed using the (user{or any name) in the scope
reference of scopes
As Owen suggested, read the docs to understand what scopes are. It is just another syntax to define your model's class methods (just like the one you already have).
scope :liked_by, ->(user) { joins(:likes).where(likes: { user_id: user.id }) }

Jbuilder not wrapping single object in collection

I have a index view in my rails application that allows filtering via search params. When a group op articles are returned its is wropped in an articles colllection like {"articles":[{"article":{"id":341,"updated":"2015-08-18T13:05:08.427Z","title":". But if only a single object is found the articles level is missing, {"article":{"id":398,"updated":"2015-08-07T11:37:26.200Z","title":. How can I fix it so that a single object behaves like multiple?
_articles.list.json.jbuilder
require 'uri'
require 'publish_on'
json.cache! ['v1', articles] do
json.articles articles do |article|
json.cache! ['v1', article] do
json.article do
json.id article.id
json.updated as_ns_date(article.updated_at)
json.title article.label
json.numberOfViews article.view_mappings.count
json.numberOfFavorites article.favorite_mappings.count
json.imageURLs article.images if article.images.any?
json.youtubeURL article.youtube unless article.youtube.blank?
json.tags article.categories.map(&:label)
json.isFeatured article.featured
json.isPublished article.is_published
json.published as_ns_date(article.publish_on)
end
end
end
end
index.json.jbuilder
json.partial! 'articles/articles_list', articles: #articles
articles_controller.rb
def index
#articles = SearchArticlesCommand.new(params).execute
render :index
end
search_articles_command.rb
class SearchArticlesCommand
def initialize(params = {})
#since = params[:since_date]
#keys = params[:search_query]
#category = params[:category]
end
def execute
Article.unscoped do
query = if #since.present?
Article.article.since_date(#since)
else
Article.published_article
end
query = query.search_by_keywords(#keys) if #keys.present?
query = query.search_by_category(#category) if #category.present?
query.select(:id, :updated_at, :label, :is_published, :featured, :slug, :created_at).order(created_at: :desc)
end
end
end
article.rb
class Article < Comfy::Cms::Page
include PgSearch
include ActionView::Helpers::SanitizeHelper
HOSTNAME = ENV['HOSTNAME'] || Socket.gethostname
has_many :view_mappings, dependent: :destroy
has_many :favorite_mappings, dependent: :destroy
pg_search_scope :search_by_keywords, against: [:content_cache, :label], using: { tsearch: { any_word: true, prefix: true } }
pg_search_scope :search_by_category, associated_against: {
categories: [:label]
}
scope :since_date, -> (date) { where('created_at > ? OR updated_at > ? ', date, date) if date.present? }
scope :folder, -> { where.not(layout_id: ENV['ARTICLE_LAYOUT_ID']) }
scope :published_article, -> { published.article }
scope :article, -> { where(layout_id: ENV['ARTICLE_LAYOUT_ID']) }
It is what i suspected. If you want the same behavior your query should return the same type of object when it finds one or many articles. The problem is that either you are returning an ActiveRecordRelation or a Article object depending on your params.
#articles = Article.all # => ActiveRecordRelation, an array per se
#articles = Article.find(1) # => Article object
When it comes to jbuilder to construct the JSON it checks if it is an array of objects and then wrap the json with a { keyword => array }. WHen it is a single object, it defaults to a single object {article: {}}.
The solution is simple, you can tweak your SearchArticlesCommand to always return an ActiveRecordRelation, even if it finds only one object.

Resources