How to check if associated model has entries in Rails 5? - ruby-on-rails

I have a model RegularOpeningHour(dayOfWeek: integer) that is associated to a model OpeningTime(opens: time, closes: time). RegularOpeningHour has an 1:n relation to OpeningTime, so that a specific day can have many opening times.
(I know that I simply could have one entry with 'opens' and 'closes' included in RegularOpeningHour but for other reasons I need this splitting)
Now I want a open?-Method, that returns whether the business is opened or not. I tried the following in my model file regular_opening_hour.rb:
def open?
RegularOpeningHour.where(dayOfWeek: Time.zone.now.wday).any? { |opening_hour| opening_hour.opening_times.where('? BETWEEN opens AND closes', Time.zone.now).any? }
end
Unforutnately, that doesn't work. Any ideas to solve this?

How about this:
def open?
joins(:opening_times)
.where(dayOfWeek: Time.current.wday)
.where("opens <= :time AND closes >= :time", time: Time.current)
.any?
end
EDIT: Missing ':' in the join

You could create some scopes to make selecting open OpeningTimes and open RegularOpeningHours less clunky. This makes creating the given selection much easier.
class OpeningTime < ApplicationRecord
# ...
belongs_to :regular_opening_hour
def self.open
time = Time.current
where(arel_table[:opens].lteq(time).and(arel_table[:closes].gteq(time)))
end
# ...
end
class RegularOpeningHour < ApplicationRecord
# ...
has_many :opening_times
def self.open
where(
dayOfWeek: Time.current.wday,
id: OpeningTime.select(:regular_opening_hour_id).open,
)
end
# ...
end
def open?
RegularOpeningHour.open.any?
end

Since you have has_many association of RegularOpeningHour to OpeningTime you can use join query like below.:
RegularOpeningHour.joins(:opening_times).where(dayOfWeek: Time.zone.now.wday).where('? BETWEEN opening_times.opens AND opening_times.closes', Time.zone.now).any?

Related

Prevent duplicate has_many records in Rails 5

Given the following models:
class Client < ApplicationRecord
has_many :preferences
validates_associated :preferences
accepts_nested_attributes_for :preferences
end
class Preference < ApplicationRecord
belongs_to :client
validates_uniqueness_of :block, scope: [:day, :client_id]
end
I'm still able to create preferences with duplicate days* when creating a batch of preferences during client creation. This is (seemingly) because the client_id foreign key isn't available when the validates_uniqueness_of validation is run. (*I have an index in place which prevents the duplicate from being saved, but I'd like to catch the error, and return a user friendly error message, before it hits the database.)
Is there any way to prevent this from happening via ActiveRecord validations?
EDIT: This appears to be a known issue.
There's not a super clean way to do this with AR validations when you're batch inserting, but you can do it manually with the following steps.
Make a single query to the database using a Postgresql VALUES list to load any potentially duplicate records.
Compare the records you are about to batch create and pull out any duplicates
Manually generate and return your error message
Step 1 looks a little like this
# Build array of uniq attribute pairs we want to check for
uniq_attrs = new_collection.map do |record|
[
record.day,
record.client_id,
]
end
# santize the values and create a tuple like ('Monday', 5)
values = uniq_attrs.map do |attrs|
safe = attrs.map {|v| ActiveRecord::Base.connection.quote(v)}
"( #{safe.join(",")} )"
end
existing = Preference.where(%{
(day, client_id) in
(#{values.join(",")})
})
# SQL Looks like
# select * from preferences where (day, client_id) in (('Monday',5), ('Tuesday', 3) ....)
Then you can take the collection existing and use it in steps 2 and 3 to pull out your duplicates and generate your error messages.
When I've needed this functionality, I've generally made it a self method off my class, so something like
class Preference < ApplicationRecord
def self.filter_duplicates(collection)
# blah blah blah from above
non_duplicates = collection.reject do |record|
existing.find do |exist|
exist.duplicate?(record)
end
end
[non_duplicates, existing]
end
def duplicate?(record)
record.day == self.day &&
record.client_id = self.client_id
end
end

What's the right way to list all paper_trail versions including associations?

Question regarding the paper_trail gem.
When only associations change, a version record won't be created for the main model. So what's the right way to list all versions for a certain record including its associations?
Should I query something like this? (The bad point is this SQL query might be long and low performance.)
f = "(item_type = 'Place' AND item_id = ?) OR (item_type = 'PlaceName' AND item_id IN (?))"
PaperTrail::Version.where(f, #place.id, #place.names.map { |n| n.id })
Or should I create a version record when only associations changed? I think #DavidHam tried the same thing and asked a similar question but nobody has answered it yet.
So, I sort of found a way to do this, but it's not exactly pretty and it doesn't create a new version everytime an association is changed. It does, however, give you an efficient way to retrieve the versions chronologically so you can see what the version looked like before/after association changes.
First, I retrieve all the ids for for the asscoiation versions given the id of that model:
def associations_version_ids(item_id=nil)
if !item_id.nil?
ids = PaperTrail::VersionAssociation.where(foreign_key_id: item_id, foreign_key_name: 'item_id').select(:version_id)
return ids
else
ids = PaperTrail::VersionAssociation.where(foreign_key_name: 'item_id').select(:version_id)
return ids
end
end
Then I get all versions together using the VersionAssociation ids from this function. It will return a chronological array of PaperTrail::Version's. So the information is useful for an activity log, etc. And it's pretty simple to piece back together a version and its associations this way:
def all_versions
if !#item_id.nil?
association_version_ids = associations_version_ids(#item_id)
all_versions = PaperTrail::Version
.where("(item_type = ? AND item_id = ?) OR id IN (?)", 'Item', #item_id, association_version_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
else
assocation_ids = associations_version_ids
all_versions = PaperTrail::Version
.where("item_type = ? OR id IN (?)", 'Item', association_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
end
end
Again, not a perfect answer since there isn't a new version everytime there's a change, but it's manageable.
This is more of an approach than a specific answer, but here goes.
In my case, I needed a version history such that any time anyone changed a Child, they also changed a flag on the `Parent'. But I needed a way to show an audit trail that would show the initial values for all the children, and an audit line for the parent whenever anyone changed a child.
So, much simplified, it's like this:
class Parent < ActiveRecord::Base
has_paper_trail
has_many :children
end
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent
end
So, whenever there's a change on a Child we need to create a version on the Parent.
First, try changing Child as follows:
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent, touch: true
end
This should (should! have not tested) create a timestamp on the Parent whenever the Child changes.
Then, to get the state of the :children at each version of Parent, you search the Child's versions for the one where the transaction_id matches the Parent.
# loop through the parent versions
#parent.versions.each do |version|
#parent.children.versions.each do |child|
# Then loop through the children and get the version of the Child where the transaction_id matches the given Parent version
child_version = child.versions.find_by transaction_id: version.transaction_id
if child_version # it will only exist if this Child changed in this Parent's version
# do stuff with the child's version
end
This worked in my situation, hope something in here is useful for you.
[UPDATED]
I found a better way. You need to update associations inside transaction to make this code work.
class Place < ActiveRecord::Base
has_paper_trail
before_update :check_update
def check_update
return if changed_notably?
tracking_has_many_associations = [ ... ]
tracking_has_has_one_associations = [ ... ]
tracking_has_many_associations.each do |a|
send(a).each do |r|
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
tracking_has_one_associations.each do |a|
r = send(a)
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
end
class Version < PaperTrail::Version
def associated_versions
Version.where(transaction_id: transaction_id) if transaction_id
end
end
[Original Answer]
This is the best way I've found so far. (#JohnSchaum's answer helps me a lot, thanks!)
Before starting, I've added polymorphic_type column to the versions table.
class AddPolymorphicTypeToVersions < ActiveRecord::Migration
def change
add_column :versions, :polymorphic_type, :string
end
end
And setup models like this:
# app/models/version.rb
class Version < PaperTrail::Version
has_many :associations, class_name: 'PaperTrail::VersionAssociation'
end
# app/models/link.rb
class Link < ActiveRecord::Base
has_paper_trail meta: { polymorphic_type: :linkable_type }
belongs_to :linkable, polymorphic: true
end
# app/models/place.rb
class Place < ActiveRecord::Base
has_paper_trail
has_many :links, as: :linkable
def all_versions
f = '(item_type = "Place" AND item_id = ?) OR ' +
'(foreign_key_name = "place_id" AND foreign_key_id = ?) OR ' +
'(polymorphic_type = "Place" AND foreign_key_id = ?)'
Version.includes(:associations).references(:associations).where(f, id, id, id)
end
end
And we can now get versions including associations like following:
#place.all_versions.order('created_at DESC')

Rails replace collection instead of adding to it from a has_many nested attributes form

I have these models (simplified for readability):
class Place < ActiveRecord::Base
has_many :business_hours, dependent: :destroy
accepts_nested_attributes_for :business_hours
end
class BusinessHour < ActiveRecord::Base
belongs_to :place
end
And this controller:
class Admin::PlacesController < Admin::BaseController
def update
#place = Place.find(params[:id])
if #place.update_attributes(place_params)
# Redirect to OK page
else
# Show errors
end
end
private
def place_params
params.require(:place)
.permit(
business_hours_attributes: [:day_of_week, :opening_time, :closing_time]
)
end
end
I have a somewhat dynamic form which is rendered through javascript where the user can add new opening hours. When submitting these opening hours I would like to always replace the old ones (if they exist). Currently if I send the values via params (e.g.):
place[business_hours][0][day_of_week]: 1
place[business_hours][0][opening_time]: 10:00 am
place[business_hours][0][closing_time]: 5:00 pm
place[business_hours][1][day_of_week]: 2
place[business_hours][1][opening_time]: 10:00 am
place[business_hours][1][closing_time]: 5:00 pm
... and so forth
These new business hours get added to the existing ones. Is there a way to tell rails to always replace the business hours or do I manually have to empty the collection in the controller every time?
Bit optimizing the solution proposed #robertokl, to reduce the number of database queries:
def business_hours_attributes=(*args)
self.business_hours.clear
super(*args)
end
This is the best I could get:
def business_hours_attributes=(*attrs)
self.business_hours = []
super(*attrs)
end
Hopefully is not too late.
You miss id of business_hours try:
def place_params
params.require(:place)
.permit(
business_hours_attributes: [:id, :day_of_week, :opening_time, :closing_time]
)
end
That's why the form is adding a new record instead of updating it.

Mongoid: Querying from two collections and sorting by date

I currently have the following controller method in a Rails app:
def index
#entries = []
#entries << QuickPost.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries << Infographic.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries.flatten!.sort!{ |a,b| b.created_at <=> a.created_at }
#entries = Kaminari.paginate_array(#entries).page(params[:page]).per(10)
end
I realise this is terribly inefficient so I'm looking for a better way to achieve the same goal but I'm new to MongoDB and wondering what the best solution would be.
Is there a way to make a sorted limit() query or a MapReduce function in MongoDB across two collections? I'm guessing there isn't but it would certainly save a lot of effort in this case!
I'm currently thinking I have two options:
Create a master 'StreamEntry' type model and have both Infographic and QuickPost inherit from that so that both data types are stored on the same collection. The issue with this is that I have existing data and I don't know how to move it from the old collections to the new.
Create a separate Stream/ActivityStream model using something like Streama (https://github.com/christospappas/streama). The issues I can see here is that it would require a fair bit of upfront work and due to privacy settings and editing/removal of items the stream would need to be rebuilt often.
Are there options I have overlooked? Am I over-engineering with the above options? What sort of best practices are there for this type of situation?
Any info would be greatly appreciated, I'm really liking MongoDB so far and want to avoid falling into pitfalls like this in the future. Thanks.
The inherit solution is fine, but when the inherited models are close.
For example :
class Post < BasePost
field :body, type: String
end
class QuickPost < BasePost
end
class BasePost
field :title, type: String
field :created_at, type: Time
end
But when the models grows, or are too different, your second solution is better.
class Activity
include Mongoid::Document
paginates_per 20
field :occurred_at, :type => Time, :default => nil
validates_presence_of :occurred_at
belongs_to :user
belongs_to :quick_post
belongs_to :infographic
default_scope desc(:occurred_at)
end
and for example :
class QuickPost
include Mongoid::Document
has_one :activity, :dependent => :destroy
end
The dependant destroy make the activity destroyed when the QuickPost is destroyed. You can use has_many and adapt.
And to create the activities, you can create an observer :
class ActivityObserver < Mongoid::Observer
observe :quick_post, :infographic
def after_save(record)
if record.is_a? QuickPost
if record.new_record?
activity = record.build_activity
activity.user = record.user
# stuff when it is new
else
activity = record.activity
end
activity.occurred_at = record.occurred_at
# common stuff
activity.save
end
end
end

Only one record true, all others false, in rails

I have the following situation
class RecordA
has_many :recordbs
end
class RecordB
belongs_to :recorda
end
RecordA has many recordbs but only one of them may be an active recordb. I need something like myRecordA.active_recordb
If I add a new column like is_active to RecordB, then I have the potential problem of setting two records to is_active = true at the same time.
Which design pattern can I use?
Thanks!
Let's change your example. There's a LectureRoom, with many People and only one Person can be the instructor.
It'd be much easier to just have an attribute in the LectureRoom to indicate which Person is the instructor. That way you don't need to change multiple People records in order to swap the instructor. You just need to update the LectureRoom record.
I would use a named scope to find the active lecturer.
class Person
named_scope :currently_speaking, :conditions => {:active => true}
end
Then I would call that a lecturer in ClassRoom:
class ClassRoom
def lecturer
people.currently_speaking.first
end
end
The real problem is making sure that when you activate someone else, they become the only active person. I might do that like this:
class Person
belongs_to :class_room
before_save :ensure_one_lecturer
def activate!
self.active = true
save
end
def ensure_one_lecturer
if self.active && changed.has_key?(:active)
class_room.lecturer.update_attribute(:active, false)
end
end
end
This way everything is done in a transaction, it's only done if you've changed the active state, and should be pretty easily tested (I have not tested this).
You can define a class method on RecordB for this:
class RecordB < ActiveRecord::Base
def self.active
first(:conditions => { :active => true }
end
end

Resources