I've got STI like this:
class Post
end
class Post::Confirmed < Post
end
class Post::Draft < Post
def confirm!
becomes Post::Confirmed
end
end
...# somewhere in controller
# POST /posts/1/confirm
# POST /posts/1/confirm.json
def confirm
#post = Post::Draft.first
#post = #post.confirm! # this is the only way I can reload #post with Post::Confrmed
end
Is it somehow possible to make:
#post.confirm! # I want this #post(Post::Draft) to become Post::Confirmed without reassigning
Or is it just nor RoR way?
Thanks in advance!
The pattern I've found that works best here is having a datetime type field that records when the record was flagged.
For example:
def confirm!
self.confirmed_at = DateTime.now
self.save!
end
Then you can tell when something was confirmed. This comes in especially handy for when you have a situation where something will be flagged but isn't yet, such as setting a publishing date in the future.
Although it might seem a little annoying to not have your STI bag of tricks available, STI is not always the appropriate tool. Generally STI is to differentiate between similar but different models that have a lot of commonality or are used in a common context. It's not supposed to be used to handle different states of a singular model.
What you want in that case is a state-machine type pattern.
Related
I'm using mongoid for an app where the user is the parent document, and pretty much all other information is embedded in the user. So for instance, my controller #new action for a Relationship belonging to the user looks something like:
def new
#relationship = current_user.relationships.new(friend_id: params[:fid])
#relationship.validate
end
Because I run validations on the relationship that will show up in the view and some of those validations need to be able to reference the parent, I can't just call #relationship = Relationship.new(friend_id: params[:fid]), but having instantiated this relationship in the user's relationship array, it's now hanging out in there, even if the user decides they don't want to make a new relationship after all and they go to another part of the site. If they go to the relationship index page, they'll see it in the list unless I filter it out.
If the relationship is valid and they do something elsewhere that causes the user to save, that dummy relationship is now a real one. If it's not valid, the save is going to fail for unknown reasons.
I have a number of models I intend to embed in the user, so I will have this issue with every one of them.
I know I can call current_user.reload to clear the junk out, but it feels ridiculous to me that I would have to hit the database every time I wanted to do this. I could also orphan the relationship after validating, but that feels hacky.
It seems to me that this is a problem people should run into all the time with embedded documents, so I would think there'd be some kind of built in solution, but I can't seem to find it anywhere. I saw this question, which is similar to mine, but I want something more extensible, so that I don't have to put it everywhere.
I'm about to make a module that will add a clear_unsaved_#{relation} method to the class for each embedded relation, but the idea frustrates me, so I wanted to see if anyone has a better idea of how to do it, and also where is best to call it.
I ended up making a monkey patch that overrides Mongoid's embeds_many and embeds_one class methods to also define an instance method for clearing unsaved documents for that relation. This felt like the most straightforward way to me because it's very little code and it means I don't have to remember to include it places.
# config/initializers/patches/dirty_tracking_embedded.rb
module DirtyTrackingEmbedded
# override the embedding methods to also make dirty-tracking
def embeds_many(name, options= {}, &block)
define_method "clear_unsaved_#{name}" do
# remove_child removes it from the array without hitting the database
send(name).each {|o| remove_child(o) unless o.persisted?}
end
super
end
def embeds_one(name, options={}, &block)
define_method "clear_unsaved_#{name}" do
dirty = send(name)
remove_child(dirty) unless dirty.persisted?
end
super
end
end
module Mongoid
module Association
module Macros
module ClassMethods
prepend DirtyTrackingEmbedded
end
end
end
end
Then in my controller I resorted to an after_action:
# app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
after_action :clear_unsaved, only: [:new]
def new
#relationship = current_user.relationships.new(friend_id: params[:fid])
#relationship.validate
end
private
def clear_unsaved
current_user.clear_unsaved_relationships
end
end
Other Possibilities
Different monkey patch
You could monkey patch the setup_instance_methods! methods in Mongoid::Association::Embedded::EmbedsMany and Mongoid::Association::Embedded::EmbedsOne to include setting up an instance method to clear unsaved. You can find an example of how the Mongoid folks do that sort of thing by looking at Mongoid::Association::Accessors#self.define_ids_setter!. I'd recommend doing your patching with a prepend like in the solution I went with, so you can inherit the rest of the method.
Combo monkey patch and inheritance
Mongoid chooses which class to use to instantiate an association from a constant called MACRO_MAPPING in Mongoid::Association, so you could make classes that inherit from EmbedsMany and EmbedsOne with just setup_instance_methods! overridden to add the needed instance method, then you would only have to monkey patch MACRO_MAPPING to map to your new classes.
Concern
If you're anti-monkey patching, you could use the code from my DirtyTrackingEmbedded module to make an ActiveSupport::Concern that does the same thing. You'll want to put the overridden methods in the class_methods block, and then just make sure you include this module after you include Mongoid::Document in any model class you want it in.
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.
I have the following classes:
class AwardBase
class AwardOne < AwardBase
class Post < ActiveRecord::Base
The Post is an ActiveRecord, and the Award has a can_award? class method which takes a post object and checks to see if it meets some criteria. If yes, it updates post.owner.awards.
I know I can do this using an Observer pattern (I tested it and the code works fine). However, that requires me to add additional code to the model. I'd like not to touch the model at all if possible. What I'd like to do is run the Award checks like this (the trigger will be invoked at class load time):
class AwardOne < AwardBase
trigger :post, :after_save
def self.can_award?(post)
...
end
end
The intention with the above code is that it should automatically add AwardOne.can_award? to Post's after_save method
So essentially what I'm trying to do is to get the trigger call be equivalent to:
class Post < ActiveRecord::Base
after_save AwardOne.can_award?(self)
...
end
which is basically:
class Post < ActiveRecord::Base
after_save :check_award
def check_award
AwardOne.can_award?(self)
end
end
How can I do this without modifying the Post class?
Here's what I've done (which does not appear to work):
class AwardBase
def self.trigger (klass, active_record_event)
model_class = klass.to_class
this = self
model_class.instance_eval do
def award_callback
this.can_award?(self)
end
end
model_class.class_eval do
self.send(active_record_event, :award_callback)
end
end
def self.can_award? (model)
raise NotImplementedError
end
end
The above code fails with the error:
NameError (undefined local variable or method `award_callback' for #<Post:0x002b57c04d52e0>):
You should think about why you want to do it this way. I would argue it is even worse than using the observer pattern. You are violating the principle of least surprise (also called principle of least astonishment).
Imagine that this is a larger project and I come as a new developer to this project. I am debugging an issue where a Post does not save correctly.
Naturally, I will first go through the code of the model. I might even go through the code of the posts controller. Doing that there will be no indication that there is a second class involved in saving the Post. It would be much harder for me to figure out what the issue is since I would have no idea that the code from AwardOne is even involved.
In this case it would actually be most preferable to do this in the controller. It is the place that is easiest to debug and understand (since models have enough responsibilities already and are generally larger).
This is a common issue with metaprogramming. Most of the time it is better to avoid it precisely because of principle of least surprise. You will be glad you didn't use it a year from now when you get back to this code because of some issue you need to debug. You will forget what "clever" thing you have done. If you don't have a hell-of-a-good reason then just stick to the established conventions, they are there for a reason.
If nothing else then at least figure out a way to do this elegantly by declaring something in the Post model. For example by registering an awardable class method on ActiveRecord::Base. But the best approach would probably be doing it in the controller or via a service object. It is not the responsibility of AwardOne to handle how Post should be saved!
Because you are adding award_callback as class method. I bet it will be registered if you grep class methods.
So change your code like below. It should work fine.
model_class.class_eval do ## Changed to class_eval
def award_callback
this.can_award?(self)
end
end
Let me give a detailed example if it sounds confusing.
class Test
end
Test.instance_eval do
def class_fun
p "from class method "
end
end
Test.class_eval do
def instance_fun
p "from instance method "
end
end
Test.methods.grep /class_fun/
# => [:class_fun]
Test.instance_methods.grep /instance_fun/
# => [:instance_fun]
Test.class_fun
# => "from class method "
Test.new.instance_fun
# => "from instance method "
I'd like to override the setter for an association, but write_attribute() isn't working - probably because that method only works for database columns.
I have tried super(), but that doesn't work either (didn't think it would... but it was worth a guess).
How do I override the setter? Here is what I am trying to do:
def parent=(value)
# this line needs to be changed
write_attribute(:parent, value)
if value.subject.start_with?('Re:')
self.subject = "#{value.subject}"
else
self.subject = "Re: #{value.subject}"
end
self.receivers << value.sender
end
What worked for me is the following:
def parent=(new_parent)
# do stuff before setting the new parent...
association(:parent).writer(new_parent)
end
I found one way to do it, but I am disturbed by it:
alias_method :old_parent=, :parent=
def parent=(value)
self.old_parent = value
if value.subject.start_with?('Re:')
self.subject = "#{value.subject}"
else
self.subject = "Re: #{value.subject}"
end
self.receivers << value.sender
end
One thing I don't necessarily like about Rails is that whenever you want to do something that is out of the norm just a bit - but not unreasonable by any means - the "how" is very different than what your intuition would come up with.
It's not a problem when you know the exceptions, but when you're learning, this sort of irregularity and inconsistency on how to do things makes it harder to learn - not easier.
Java might be initially harder to learn, but it's way more consistent. Your intuition can take you a lot further once you think in Java. This is not true once you think in Rails. Rails is about memorization of methods to call and memorization on how to do things. In java, you can reason it out a lot more... and intellisense fills in the rest.
I'm just disappointed. This is a reoccurring pattern for me - I want do something that is just "a little more complex" than the framework examples... and the "how" is inconsistent and takes 30 minutes or maybe even hours to locate and find the answer for it.
In Rails 4.2.1 doc:
# Association methods are generated in a module that is included into the model class,
# which allows you to easily override with your own methods and call the original
# generated method with +super+. For example:
#
# class Car < ActiveRecord::Base
# belongs_to :owner
# belongs_to :old_owner
# def owner=(new_owner)
# self.old_owner = self.owner
# super
# end
# end
Instead of
def parent=(value)
write_attribute(:parent, value)
end
Couldn't you just do:
def parent=(parent)
parent_id = parent.id
end
In my rails app I would like to track who changes my model and update a field on the model's table to reflect.
So, for example we have:
class Foo < ActiveRecord::Base
before_create :set_creator
belongs_to :creator, :class_name => "User"
protected
def set_creator
# no access to session[:user_id] here...
end
end
What's a good testable way for me to get at the user_id from my model? Should I be wacking this data in Thread.current ?
Is it a better practice to hand this information from the controller?
Best practice in MVC is to have your Models be stateless, the controller gets to handle state. If you want the information to get to your models, you need to pass it from the controller. Using a creation hook here isn't really the right way to go, because you are trying to add stateful data, and those hooks are really for stateless behavior.
You can pass the info in from the controller:
Foo.new(params[:foo].merge {:creator_id => current_user.id})
Or you can create methods on User to handle these operations:
class User
def create_foo(params)
Foo.new(params.merge! {:creator_id => self.id})
end
end
If you find yourself writing a lot of permissions code in the controller, I'd go with option 2, since it will let you refactor that code to the model. Otherwise option 1 is cleaner.
Omar points out that it's trickier to automate, but it can still be done. Here's one way, using the create_something instance method on user:
def method_missing(method_sym, *arguments, &block)
meth = method_sym.to_s
if meth[0..6] == "create_"
obj = meth[7..-1].classify.constantize.new(*arguments)
obj.creator_id = self.id
else
super
end
end
You could also override the constructor to require user_ids on construction, or create a method inside ApplicationController that wraps new.
There's probably a more elegant way to do things, but I definitely don't like trying to read state from inside Model code, it breaks MVC encapsulation. I much prefer to pass it in explicitly, one way or another.
Yeah, something like that would work, or having a class variable on your User model
cattr_accessor :current_user
Then in your controller you could have something like:
User.current_user = current_user
inside a before filter (assuming current_user is the logged in user).
You could then extend AR:Base's create/update methods to check for the existence of a created_by/updated_by field on models and set the value to User.current_user.
I'd create new save, update, etc methods that take the user_id from everything that calls them (mainly the controller).
I'd probably extend ActiveRecord:Base into a new class that handles this for all the models that need this behaviour.
I wouldn't trust Thread.current, seems a bit hackish. I would always call a custom method which takes an argument:
def create_with_creator(creator, attributes={})
r = new(attributes)
r.creator = creator
r.save
end
As it follows the MVC pattern. The obviously inherient problem with this is that you're going to be calling create_with_creator everywhere.
You might find PaperTrail useful.
Probably you could check out usertamp plugins, found two in github
http://github.com/delynn/userstamp/tree/master
http://github.com/jnunemaker/user_stamp/tree/master