Looking for a cleaner way to set a default value if attribute is not set yet or has been deleted, and returns nil.
class Category < ActiveRecord::Base
has_and_belongs_to_many :restaurants
belongs_to :picture
def set_picture
if self.picture.nil?
Picture.default_pic
else
self.picture
end
end
end
class Picture < ActiveRecord::Base
belongs_to :review
def self.default_pic
Picture.new(url: "/assets/default.jpg")
end
end
# index.html.erb
<%= image_tag category.set_picture.url %>
categories has many restaurants, and restaurants has many reviews. Reviews has one to one picture. category should be allowed to select from one of its associated pictures, or defaults to image in assets folder.
The #set_picture needs to get refactored out. Hopefully to a callback of some type:
class Category < ActiveRecord::Base
belongs_to :picture, defaults_to: Picture.default_pic
end
Is there a callback that does the above? Can I create one? Or is my framework wrong?
I think you could just override the accessor and call super. If this returns nil then you could return your default picture:
class Category < ActiveRecord::Base
belongs_to :picture
def picture
super || Picture.default_pic
end
end
Related
I have an Article model and Ckeditor + Paperclip installed. When I upload pictures into Article body, everything works fine. However, I want to make these pictures accessible via #article.pictures without creating a separate Picture model. I've already created a regular association between Article and Ckeditor::Picture. But when I'm uploading a picture, Ckeditor not surprisingly requires an Article id. Where and how am I supposed to pass it?
class CreateCkeditorAssets < ActiveRecord::Migration[5.2]
t.references :article, foreign_key: true
end
class Article < ApplicationRecord
has_many :pictures, class_name: 'Ckeditor::Picture'
end
class Ckeditor::Picture < Ckeditor::Asset
belongs_to :article
end
You can't pass an article ID, because at the time when you upload pictures your article isn't persisted (unless you're editing an already saved article).
So what you can do is to build an article with some unique token, then after uploading pictures and saving the article, update article_id in all pictures that have the same token.
Like this: (pseudocode, not tested)
class Article < ApplicationRecord
has_many :pictures, class_name: 'Ckeditor::Picture'
after_save :assign_pictures
private
def assign_pictures
Ckeditor::Picture.where(token: picture_token).update_all(article_id: id)
end
end
-
class Ckeditor::Picture < Ckeditor::Asset
belongs_to :article, optional: true
end
-
class Ckeditor::PicturesController
def create
#picture = Ckeditor::Picture.new
#picture.token = params[:picture_token] # pass this param via javascript, see: https://github.com/galetahub/ckeditor/blob/dc2cef2c2c3358124ebd86ca2ef2335cc898b41f/app/assets/javascripts/ckeditor/filebrowser/javascripts/fileuploader.js#L251-L256
super
end
end
-
class ArticlesController < ApplicationController
def new
#article = Article.new(picture_token: SecureRandom.hex)
end
end
Obviosly you need to add picture_token field to your Article model and token field to Ckeditor::Picture. Hope that helps.
I'm getting the strangest error I've seen in Rails so far. In my view, I can print out the email associated with a painting if I find the record directly (e.g. Painting.find(15). But if I try to use an instance variable it errors (e.g #painting).
views/paintings/show.html.erb
<%= Painting.find(15).artist.user.email %> # works
<%= #painting.artist.user.email %> # Error: "undefined method 'user' for nil:NilClass"
controllers/paintings_controller.rb
def show
#painting = Painting.find(15)
end
Models: "users", "artists", "paintings".
A user can be an artist. So a user has_one artist.
An artist has_many paintings.
I think you should add associations. This how they should look like:
class User < ActiveRecord::Base
has_one :artist # it's ok
end
class Artist < ActiveRecord::Base
belongs_to :user # add this
has_many :paintings
end
class Painting < ActiveRecord::Base
belongs_to :artist
end
For me both cases works with this associations.
Use
def show
#painting = Painting.find(15).last
end
Currently the second one is returning a 1 element array, but in order to call a dependent method, you must specify 1 item.
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.
Example:
I have the following:
class Person < ActiveRecord::Base
has_many :educations
end
class Education < ActiveRecord::Base
belongs_to :school
belongs_to :degree
belongs_to :major
end
class School < ActiveRecord::Base
has_many :educations
# has a :name
end
I want to be able to return all people who went to a specific school so in my PeopleController#index I have
#search = Person.search do
keywords params[:query]
end
#people = #search.results
How do I create the searchable method on the Person model to reach down into school? Do I do something like this:
searchable do
text :school_names do
educations.map { |e| e.school.name }
end
end
which I would eventually have to do with each attribute on education (degree etc) or can I make a searchable method on Education and somehow "call" that from Person.searchable?
Thanks
It would be best if you keep the declaration of all the indexed fields for an specific model in the same place.
Also, you were doing a good job indexing :school_names, just do the same thing for the rest of the associations fields' that you want to index.
I've been looking for this kind of functionality in AR, but don't seem able to find it. The Dirty implementation of AR states that an already persisted instance is deemed only dirty if one of its direct attributes has changed. So, let's say:
class Picture < ActiveRecord::Base
belongs_to :gallery
has_one :frame
end
in this case, I can do something like:
p = Picture.new(:gallery => Gallery.new, :frame => Frame.new)
p.save #=> it will save the three instances
p.gallery = Gallery.new
p.save #=> will not save the new gallery
p.gallery_id_will_change!
p.gallery = Gallery.new
p.save #=> this will save the new Gallery
but now I can't do something similar for the has_one association, since the Picture implementation doesn't own an attribute referring to it. So, it seems such dirty markups are impossible. Or aren't they?
Best thing I can figure out to do is:
class Picture < ActiveRecord::Base
belongs_to :gallery
has_one :frame
after_save :force_save_frame
def force_save_frame
frame.save! if frame.changed?
end
end
Like weexpectedTHIS said Dirty flags are set for attributes on the model itself, not for related objects. It only works for the belongs_to because there is a foreign key on the model.
But you can make a trick like that
module FrameDirtyStateTracking
def frame=(frame)
attribute_will_change!('frame')
super
end
end
class Picture < ActiveRecord::Base
prepend FrameDirtyStateTracking
belongs_to :gallery
has_one :frame
end
picture = Picture.new
picture.frame = Frame.new
picture.changed? # => true
picture.changed # => ['frame']