How to get list of ActiveStorage attributes (attachment names)? - ruby-on-rails

For example I have model
class User < ApplicationRecord
has_one_attached :avatar
has_one_attached :diploma
has_many_attached :photos
has_many_attached :files
end
How to get lists of attachments names for some model (separately for has_one_attached and has_many_attached)?
[:avatar, :diploma] and [:photos, :files] in this case.

A solution that doesn't depend on naming conventions and will give you exactly what you need based on Rails own internals:
for has_one_attached
User
.reflect_on_all_attachments
.filter { |association| association.instance_of? ActiveStorage::Reflection::HasOneAttachedReflection }
.map(&:name)
for has_many_attached
User
.reflect_on_all_attachments
.filter { |association| association.instance_of? ActiveStorage::Reflection::HasManyAttachedReflection }
.map(&:name)

I don't know if there is a straightforward way, bud this should workaround (for already stored records):
ActiveStorage::Attachment.distinct.pluck(:record_type).map(&:underscore)
Starting from a model, this is a raw idea to be refined:
User.reflect_on_all_associations(:has_many).map { |e| e.name.to_s.split("_") }.select { |e| e.last == "attachments" }
User.reflect_on_all_associations(:has_one).map { |e| e.name.to_s.split("_") }.select { |e| e.last == "attachment" }
Note == "attachments" and == "attachment"

#iGian gave a great idea, but there is a problem in it.
If the attachment name contains the underscore(s), it will lead to an incorrect result.
So my solution is:
for has_many_attached
User.
reflect_on_all_associations(:has_many).
map { |reflection| reflection.name.to_s }.
select { |name| name.match?(/_attachments/) }.
map { |name| name.chomp('_attachments').to_sym }
#=> [:photos, :files]
for has_one_attached
User.
reflect_on_all_associations(:has_one).
map { |reflection| reflection.name.to_s }.
select { |name| name.match?(/_attachment/) }.
map { |name| name.chomp('_attachment').to_sym }
#=> [:avatar, :diploma]

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

How to unscope multiple models in 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.

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.

write an Rspec test to test multiple nested_attibutes

I am trying to put together a test that will test an object with its nested_attributes.
I have a simple setup
class Animal < ActiveRecord::Base
has_many :animal_images
end
class AnimalImage < ActiveRecord::Base
belongs_to :animal
end
From the factory_girl docs you can create an associated object like so
FactoryGirl.define do
factory :animal, class: Animal do
ignore do
images_count 1
end
after(:create) do |animal, evaluator|
create_list(:animal_image, evaluator.images_count, animal: animal)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
so how would i construct a test to look at a custom validation method that validates the number of images..
custom method
def max_num_of_images
if image.size >= 4
errors.add(:base, "Only 3 images allowed")
end
end
But where would i use this, in Animal or AnimalImage model? Am i to assume the AnimalImage model as i have access to the image attribute?
So far i have this
it 'is invalid with 4 images' do
animal = FactoryGirl.create(:animal, images_count: 4)
animal_image = AnimalImage.create!(animal: animal, image: #how do i pass the 4 images i created earlier)
ap(animal)
ap(animal_image)
end
So ap(animal) will return
#<Animal:0x00000001c9bbd8> { :id => 52 }
and ap(animal_image) will return
#<AnimalImage:0x00000005457b30> { :id => 231, :animal_id => 52 }
What i need to do is create 4 animal_images with the same animal_id and have my validation fail it as there are more than 3 images..
Does anyone have any idea on how to go about this please?
Thank You
If i understood right, you can create array of images like:
FactoryGirl.define do
factory :animal do
name 'monkey'
trait :with_image
create_list(:animal_images, 3)
end
trait :with_5_images
create_list(:animal_images, 5)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
describe Animal do
subject(:animal) { build :animal, :with_images }
let(:invalid_animal) { build :animal, :with_5_images }
it { expect(subject.valid?).to be_truthy }
it { expect(invalid_aminal.valid?).to be_falsy }
end

Rails Relationship Questions (categories, products, and projects)

I have 3 models, Categories, Products and Projects.
The goal is to get a list of all Categories that have products (category.products.count >0) and where the Project that that the Product belongs to is not marked as private.
class Category < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :project
belongs_to :user
belongs_to :category
end
class Project < ActiveRecord::Base
belongs_to :user
has_many :products
end
class ApplicationController < ActionController::Base
protected
def set_categories
#categories = Category.joins(:products).where("products.is_product = true or
products.is_loving_this = true")
category = Category.find_by(title: "All")
#categories.unshift(category).uniq!
end
end
views/layouts/_title_bar.html.haml
%nav.navbar-custom1.navbar.navbar-custom1.navbar-default.navbar-static-top{role: "navigation"}
.container
.col-xs-4
%ul.nav.navbar-nav
%ul.dropdown#dropdown_title_bar
%a.dropdown-toggle{"data-toggle" => "dropdown", href: "#", type: "button"}
Products
%b.caret
%ul.dropdown-menu{"aria-labelledby" => "dropdown-menu", role: "menu"}
- #categories.each do |c|
- if c.products.count > 0 || c.title == "All"
%li= link_to c.title, category_path(c)
The code above gives me a dropdown list of all Categories where category.products.count > 0 but I can't figure out how to only get products where the product.project.private == false.
Can anyone guide me on how to add that?
I thought about doing 2 loops, but it seems messy.
- #categories.each do |c|
- if c.products.count > 0 || c.title == "All"
- c.products.each do |product|
- if product.project && product.project.private == false
%li= link_to c.title, category_path(c)
The last catch is that sometimes products don't have a project, which is why I added the first if statement
- if product.project && product.project.private == false
Thanks in advance guys.
I would suggest you come up with some scopes to help out with this query.
app/models/product.rb
# BTW, can you come up with a better name for this? When is a product not a product?
scope :is_product, -> { where(is_product: true )}
scope :is_loving_this, -> { where(is_loving_this: true) }
scope :is_public, -> { include(:project).where(projects: { private: false }) }
Then your query can look something like:
app/models/category.rb
def self.active_categories
category_ids = Products.is_public.is_product.is_loving_this.map(&:category_id)
Category.find(category_ids)
end
Note, this is all untested, so please write some tests to verify... :)
If you want to do it in a scope, you can do it with a subquery (Rails does it in 2 separate queries):
scope :wanted_categories, -> { Category.joins(:products).where("
(products.is_product = true or
products.is_loving_this = true) and products.id in (?)",
Product.joins(:projects).where("projects.private
= false").pluck(:id) }
Rails first runs the subquery and grabs the list of product ids who's project is not private, then uses the list to populate a sql set (note the parenthesis around the question mark) for the main query.
This approach works smoothly when you are looking for values that ARE IN a set, but if instead you need to use it with NOT IN, then there will be a caveat:
If the subquery returns no elements, rails will populate the sql set with NULL, which means nothing will match, products.id in (NULL) and products.id not in (NULL) both return nothing. In that case, you would want to turn the subquery into a scope and make your main scope conditional and make it run only if the subquery returns something. E.g:
class Product
scope :of_public_project, -> { joins(:projects).where("projects.private = false") }
class Category
scope :wanted_categories, -> { joins(:products).where("(products.is_product = true
or products.is_loving_this = true) and products.id not in
(?)", Product.of_public_project.pluck(:id) if Product.of_public_project.any?}
Thanks Jason,
Using your suggestions worked however, loving_this never belongs to a project so I can't chain all the methods together as you suggest.
I have to separate them like so:
def get_categories
category_ids = Product.is_public.product_type.map(&:category_id)
loving_this_ids = Product.loving_this_type.map(&:category_id)
#categories = Category.find(category_ids, loving_this_ids)
end
Do you think this is ok? Or is there a better way?

Resources