I have a custom Interval class that I would like to use inside a few different ActiveRecord models. Currently I am storing the interval as a string with a specific syntax (w/ a custom validator to enforce formatting) and just creating a new object anytime I need to access Interval methods.
What do I need to add to the ActiveRecord models/ Interval class to be able to use the interval as an object instead of a string, while still storing it in the database using the specific syntax?
Hopefully that makes sense but if not hopefully the following example clears things up.
The ActiveRecord class currently looks something like:
class MyClass < ActiveRecord::Base
validates :interval, allow_blank: true, interval: true # custom validator
...
And to do anything useful I create a new interval:
def some_helper
...
interval_object = Interval.new(#my_class.interval) # #my_class.interval is just a string with specific syntax
if interval_object.useful? # 'useful' method
...
But I would like to do:
def some_helper
...
if #my_class.interval.useful? # 'useful' method
...
Interval syntax that initialize expects:
3:day # represents every 3 days
1:week # represents every week
This seems like it should have a simple solution but I can't seem to find the right phrasing.
You should be able to override the accessor and mutator methods to do what needs to be done:
def interval
Interval.new(super)
end
def interval=(i)
# Or whatever needs to be done to convert `i` back to a string,
# keep in mind that `i` might be a string already.
super(i.to_s)
end
Then you can say things like:
#my_class.interval.useful?
#my_class.interval = some_interval_object
#my_class.interval = some_string_that_looks_right
#my_class.update_attributes(:interval => some_interval_object)
#my_class.update_attributes(:interval => some_string_that_looks_right)
and The Right Things should happen.
Related
I'm in the process of learning Ruby/Rails. I'm currently learning to create a model/classes.
I have an existing class that has these methods
def duration
(start_on..end_on).count
end
def items
space.available_items
end
def available_items
duration >= 365 ? items.not_including_insurance : items
end
In the class above, I have a method called available_items that checks if the duration is more than 365 days, then item will not be included in the array.
Rather than coupling the duration logic to the existing class, I think it's better to encapsulate it in another class which could take an array of rules to apply to the items.
So instead, in the above class, in the available_items method I can do something like:
policy.apply(items)
which will return all of the items which satisfy the rules.
And in the future, I can append more rules and keeps it flexible.
After includeing you module you still can define available_items method with custom rules (and have a generic one upstream), this is the easy way.
As for "passing" parameters to a strategy - it can be accomplished in a Concern like:
module SomePolicy
extend ActiveSupport::Concern
module ClassMethods
def my_policiable(param)
has_many :prices, class_name: 'Pricing::SimplePrice', as: :priceable
# ...
define_method(:available_items) {
if param == :foo
# ...
end
}
end
end
end
include SomePolicy
my_policiable(:foo)
trick is that the method is run in class context, there based on params you can define methods differently (but note that in example above if param.. will run for each resulting method invocation)
So the goal is to turn for instance "ProductCustomer", which comes from the class, into "product customer".
I used to have this:
notification.notifiable.model_name.human.downcase
It didn't work out of course, since if the notifiable is nil it breaks. I don't
want to use try or something similar since it can be solved with using notifiable_type.
So now I changed to this:
notification.notifiable_type.split(/(?=[A-Z])/).join(' ').downcase
But this is way too complex to use every time in the view. So either I would like to define this as a view helper or using some ruby formatting method if there is a simple one.
Can somebody tell me what the Rails convention is in this case? If it's a helper, how does the method looks like and where should I put it?
Options:
Initializer
/your_app/config/initializers/my_initializer.rb
module MyModule
def human_model_name
self.class.to_s.tableize.singularize.humanize.downcase
end
end
ActiveRecord::Base.send(:include, MyModule)
Including MyModule in ActiveRecord::Base will add human_model_name in all ActiveRecord instances. So, you will be able to do...
user.human_model_name #=> user
notification.human_model_name #=> notification
notification.notifiable.human_model_name #=> product customer
any_active_record_instance.human_model_name #=> etc.
To avoid exceptions when notifiable is nil, you can use try method.
notification.try(:notifiable).try(:human_model_name)
A cleaner way can be use delegate
class Notification < ActiveRecord::Base
delegate :human_model_name, to: :notifiable, prefix: true, allow_nil: true
end
Then, you can do:
notification.notifiable_human_model_name # if notifiable is nil will return nil as result instead of an exception
A simple method in your Notification model
class Notification < ActiveRecord::Base
def human_notifable_name
return unless self.notifiable # to avoid exception with nil notifiable
self.notifiable.class.to_s.tableize.singularize.humanize.downcase
end
end
Then...
notification.human_notifable_name
View Helper (If you think this is a view related method only)
module ApplicationHelper # or NotificationHelper
def human_model_name(instance)
return unless instance # to avoid exception with nil instance
instance.class.to_s.tableize.singularize.humanize.downcase
end
end
Then, in your view...
<%= human_model_name(notification.notifiable) %>
Either option is fine. I would use one or the other depending on the case. In this case, I would use the first option. I think you are adding behaviour that can be useful in any model. I mean your method is not directly related with something about notifications. In a more generic way you want a method to return the class name of an ActiveRecord's instance. Today you want the model name of the notifiable ActiveRecord's instance. But, tomorrow you may want the model name of any ActiveRecord model.
To answer the question "Where should I put a method?" I suggest to break (without fear) a little bit the MVC pattern and read about:
Decorators, presenters and delegators
Services
(a little bit old, but you can get the idea)
"ProductCustomer".tableize.singularize.humanize.downcase
I have a model called Coupon, which can either be set to have a money_off or percent_off attributes (it can only have one set at a time).
Also depending on whether the Coupon is money_off or percent_off changes which methods are used.
Im wondering if i should be using Single table inheritance to eseentially sub class Coupon and have a sub class that deals with percent off coupons and another dealing with money off coupons?
I would then like to know how a user would be able to select this from the view.
Here's an example that illustrates the usage of strategies (about which Yam posted a more detailed answer):
class Coupon < Struct.new(:original_price, :amount_off, :type)
def price_after_discount
discount_strategy.call(self)
end
private
def discount_strategy
# note: no hardcoding here
klass = type.to_s.camelize # :money_off to 'MoneyOff'
"Coupon::#{klass}".constantize.new
end
class MoneyOff
def call(coupon)
coupon.original_price - coupon.amount_off
end
end
class PercentOff
def call(coupon)
coupon.original_price * (1.0 - coupon.amount_off / 100.0)
end
end
end
Coupon.new(150, 10, :money_off).price_after_discount # => 140
Coupon.new(150, 10, :percent_off).price_after_discount # => 135.0
Now, instead of creating a strategy internally, we can accept it in constructor, thus making the strategy "injectable".
The best way is to determine which functionality you require for each class. If you only need a small amount of changes, then stick to a single class with an enum:
#app/models/coupon.rb
class Coupon < ActiveRecord::Base
enum type: [:percent, :money]
def value
if type.percent?
# ...
elsif type.money?
# ...
end
end
end
This will allow you to use the type in your instance methods, which shouldn't cause such a problem if you didn't have a lot of changes to make within the class.
This would allow you to call:
#coupon = Coupon.find x
#coupon.value #-> returns value based on the type
--
The alternative (STI) would be more of a structured change, and would only work if you were referencing each class explicitly:
#app/models/coupon.rb
class Coupon < ActiveRecord::Base
end
#app/models/percent.rb
class Percent < Coupon
def amount
# ...
end
end
#app/models/money.rb
class Money < Coupon
def takeout
# ...
end
end
An important factor here is how you call these.
For the above classes, you have to reference the subclassed classes on their own:
#percentage_coupon = Percent.find x
#money_coupon = Money.find y
This will obviously be more cumbersome, and may even cause problems with your routes & controllers etc.
.... so it may be best going with the single class :)
What you can do is maintain the strategy internally, and provide methods such as price, discounted?, discounted_price. In addition, whether or not the admin chose to enter percentages or fixed units, you can still supply both methods: discount_pct, discount_units which would internally realize how to compute their return values.
This way the original class still supports the concept (same as the data model), yet is also flexible enough to allow various ways of providing it the necessary input. Whether you wish to show customers the pct off, or the fixed price units, you can do so independently of the admin's preferred method of input.
Even internal methods can use these abstractions. And if it turns out you're if/elsing all over the place internally, you can create nested classes for strategies and instantiate the right one once you get the record from the DB.
I use the readonly? function to mark my Invoice as immutable after they've been sent; for by InvoiceLines, I simply proxy the readonly? function to the Invoice.
A simplified example:
class Invoice < ActiveRecord::Base
has_many :invoice_lines
def readonly?; self.invoice_sent? end
end
def InvoiceLine < ActiveRecord::Base
def readonly?; self.invoice.readonly? end
end
This works great, except that in one specific scenario I want to update an InvoiceLine regardless of the readonly? attribute.
Is there are way to do this?
I tried using save(validate: false), but this has no effect. I looked at persistence.rb in the AR source, and that seems to just do:
def create_or_update
raise ReadOnlyRecord if readonly?
...
end
Is there an obvious way to avoid this?
A (somewhat dirty) workaround that I might do in Python:
original = line.readonly?
line.readonly? = lambda: false
line.save()
line.readonly? = original
But this doesn't work in Ruby, since functions aren't first-class objects ...
You can very easily redefine a method in an instantiated object, but the syntax is definition rather than assignment. E.g. when making changes to a schema that required a tweak to an otherwise read-only object, I have been known to use this form:
line = InvoiceLine.last
def line.readonly?; false; end
Et voila, status overridden! What's actually happening is a definition of the readonly? method in the object's eigenclass, not its class. This is really grubbing around inside the guts of the object, though; outside of a schema change it's a serious code smell.
One crude alternative is forcing Rails to write an updated column directly to the database:
line.update_columns(description: "Compliments cost nothing", amount: 0)
and it's mass-destruction equivalent:
InvoiceLine.where(description: "Free Stuff Tuesday").update_all(amount: 0)
but again, neither should appear in production code outside of migrations and, very occasionally, some carefully written framework code. These two bypass all validation and other logic and risk leaving objects in inconsistent/invalid states. It's better to convey the need and behaviour explicitly in your model code & interactions somehow. You could write this:
class InvoiceLine < ActiveRecord::Base
attr_accessor :force_writeable
def readonly?
invoice.readonly? unless force_writeable
end
end
because then client code can say
line.force_writable = true
line.update(description: "new narrative line")
I still don't really like it because it still allows external code to dictate an internal behaviour, and it leaves the object with a state change that other code might trip over. Here's a slightly safer and more rubyish variant:
class InvoiceLine < ActiveRecord::Base
def force_update(&block)
saved_force_update = #_force_update
#_force_update = true
result = yield
#_force_update = saved_force_update
result
end
def readonly?
invoice.readonly? unless #_force_update
end
end
Client code can then write:
line.force_update do
line.update(description: "new description")
end
Finally, and this is probably the most precision mechanism, you can allow just certain attributes to change. You could do that in a before_save callback and throw an exception, but I quite like using this validation that relies on the ActiveRecord dirty attributes module:
class InvoiceLine < ActiveRecord::Base
validate :readonly_policy
def readonly_policy
if invoice.readonly?
(changed - ["description", "amount"]).each do |attr|
errors.add(attr, "is a read-only attribute")
end
end
end
end
I like this a lot; it puts all the domain knowledge in the model, it uses supported and built-in mechanisms, doesn't require any monkey-patching or metaprogramming, doesn't avoid other validations, and gives you nice error messages that can propagate all the way back to the view.
I ran into a similar problem with a single readonly field and worked around it using update_all.
It needs to be an ActiveRecord::Relation so it would be something like this...
Invoice.where(id: id).update_all("field1 = 'value1', field2 = 'value2'")
Here is an answer, but I don't like it. I would recommend to think twice about the design: If you make this data immutable, and you do need to mutate it, then there may be a design issue. Let aside any headache if the ORM and the datastore "differ".
One way is to use the meta programming facilities. Say you want to change the item_num of invoice_line1 to 123, you can proceed with:
invoice_line1.instance_variable_set(:#item_num, 123)
Note that the above will not work directly with ActiveRecord models' attributes, so it would need be adapted. But well, I would really advice to reconsider the design rather than falling for black magic.
Here's an elegant solution how to disallow modification generally but allow it if it is specifically requested:
In your model, add the following two methods:
def readonly?
return false if #bypass_readonly
return true # Replace true by your criteria if necessary
end
def bypass_readonly
#bypass_readonly=true
yield
#bypass_readonly=false
end
Under normal circumstances, your object is still readonly, so no risk of accidentally writing to a readonly object:
mymodel.save! # This raises a readonly error
However in privileged places where you are sure that you want to ignore the readonlyness, you can use:
mymodel.bypass_readonly do
# Set fields as you like
mymodel.save!
end
Everything inside the bypass_readonly block is now allowed despite readonly. Have fun!
This overrides the #readonly? method for one particular only, not affecting anything else:
line.define_singleton_method(:readonly?) { false }
readonly_attrs = described_class.readonly_attributes.dup
described_class.readonly_attributes.clear
# restore readonly rails constraint
described_class.readonly_attributes.merge(readonly_attrs)
This worked for us with Rails 7.
What is the best way to store Time in milliseconds when using Rails+Mysql?
I am about to use a decimal and composed_of in order to be able to manipulate this value as a Ruby Time. Does anyone have a better idea?!
Several years have passed since this was asked. Here's an updated solution:
https://gist.github.com/MarkMurphy/93adca601b05acffb8b5601df09f66df
I'm not sure I fully understand what you're trying to do, but have you considered simply overriding the reader/writer methods in your model?. If this works for you, it might be preferred over your proposed solution since it's arguably more readable.
MyClass < ActiveRecord::Base
# Override reader method
def my_attribute
super().from_milis
end
# Override writer method
def my_attribute=(value)
super(value.to_milis)
end
end
Posted a solution to store millisecond precision in MySql using composed_of
http://ternarylabs.com/2011/09/26/millisecond-precision-timestamp-in-rails-with-mysql/
1) Store it as a :decimal with ample precision for your purposes.
2) Create a helper method yourself. Something like this:
# app/helpers/application_helper.rb
module ApplicationHelper
def time_with_ms(time)
minutes = (time % 1.minute).floor
seconds = time % 1.minute
"%02d:%05.2f" % [minutes, seconds]
end
end
My approach was to:
open the time class and implement the methods :from_milis and :to_milis :
class Time
def self.from_milis(milis)
self.at(milis.to_f/1000)
end
def to_milis
self.to_f*1000
end
end
migrate the column from timestamp to :decimal,:precision=>17
then, in the AR class in which i am using this column as attribute:
composed_of :ts,
:class_name=>"Time",
:mapping=>%w(ts to_milis),
:constructor=>:from_milis,
:converter=>:from_milis
I just had gochas when using this attribute in arel queries, where I had to explicitly call to_milis in order to get the intended value in the comparision.