Disclaimer: I learned about it yesterday. Woops.
I was building out an email sending feature, and the basic idea was to assign a role to a user that has an expiration date, and also allow the option to send an email letting the user know. The end result is this:
Building out the form was pretty straightforward and easy, especially since most of the work was done for me already with the Administrate gem. There was even a nice wiki that told me how to make a custom form field, and it even includes a command line generator! Great! This is all so easy! Well, like most problems, this one was not as simple as I thought.
The generator creates a view partial, and after a bit of configuration, all I had to do was add a few more fields:
Okay, that's all I have to do right? The form submitted, everything works, bada bing bada boom push that code to production. Well, once the code gets to production, I realize it doesn't work -- actually, Jess realizes it doesn't work, and let's me know I goofed. Lesson learned. Always check your code, and see...
What happens to it after you deploy it.
It's not enough to compile and pass tests and not get paged about it. You should have muscle memories for going to check up on it -- did what you expected to happen, actually happen? Did anything else change?
So, back to the code. Here's what I thought was the offending code:
# Controller
class UsersController < ApplicationController
def update
user = User.find(params[:id])
UserRoleService.new(user).check_for_roles(params[:user])
if user.errors.messages.blank? && user.update(user_params)
# happy path
else
# sad path
end
end
private
def strong_params
accessible = [user, model, attributes]
params.require(:user).permit(accessible)
end
end
# user_role_service.rb, the service that handles new roles
class UserRoleService
def check_for_roles
# some other logic
NotifyMailer.delay.superpowers_awarded_email(@user) if params[:superpower_email] == "1"
end
end
Ahh, I forgot that params[:superpower_email]
is a normal parameter and not permitted. I tried to push
it into the accessible
array, but the :superpower_email param
was still nowhere to be found. Well, since it didn't work, I had to change the checkbox field:
<div style="margin-left: 1em;">
<%= f.check_box "superpower_email" %>
</div>
But that threw a NoMethodError, undefined method for User
. Well, the superpower_email
wasn't supposed to be an actual column in the model, and that would be ridiculous if I had to create a migration and make that column. There had to be a better way. I asked for some help, and Ben said:
So now that you've read so much of this story, the question finally arises: what is a virtual attribute? Well, after some Googling, I figured it out.
Rails models are classes that inherited from ApplicationRecord
, and just like classes we can add attributes to them. Almost always, model attributes link with a database column, since the model is essentially the table you're interacting with. There are some cases though where you wouldn't need to link an attribute to the database, and therefore you can create the attribute just like you would with any Ruby class. This was the big "Aha!" moment for me.
Now, I could have went with the simplest way of creating the class attribute:
class User < ApplicationRecord
def superpower_email; end
end
But that didn't seem quite right. After all, I didn't really need a method to run any logic. So, I turned to this solution instead:
class User < ApplicationRecord
attr_acccessor :superpower_email
end
The reason I needed it to be an accessor
attribute as opposed to only a reader or only a writer was so that the form could both render the attribute properly as well as pass along the data as a parameter. Also, this would make it easier to add future extraneous attributes later on if I ever needed them.
Update: As @kinduff mentioned in the comments, it might be misleading about the difference between accessor
and reader
+ writer
. accessor
is synonymous and an alias of using both attr_reader
and attr_writer
.
So I go ahead and add the attribute into the User model (class), add the new attribute as an allowed strong parameter, check that an email is queued as a job, and it works! Huzzah! Back into production the code goes!
Top comments (6)
ooooh i got a shout out 😍
I don't have all the context of your application but they way I understood, if the role is not updated for some reason, the user will get notified that they have a super power when they not. You should move the check after the role is updated.
Also it's worth the mention that attr_acccessor (Ruby method apidock.com/ruby/Module/attr_accessor) acts as an alias for attr_reader and attr_writer methods, both options will work for this case.
Examples:
Hope this makes it clear, since the line about why you use attr_accessor can be misleading.
Good writing, thanks for sharing.
Gotcha, thanks for that. I updated my article to clarify my point.
And yeah, good point about the email sending even if the role is not properly updated.
Thanks for reading!
There is a typo mistake.
should be
Fixed, thanks!
btw where did you asked for help ? it a rails slack ?