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:
<div class="field-unit__label"> <%= f.label field.attribute %> #=> Give superpowers? </div> <div class="field-unit__field" style="width: 2em;"> <%= f.check_box field.attribute %> #=> User.add_role superpowered </div> <%= f.date_field "superpower_expiration", value: (Time.now + 1.year).strftime("%Y-%m-%d"), style: "width: 25%;" %> <div style="margin-left: 1em;"> <%= f.label "Send email telling them they have it?" %> </div> <div style="margin-left: 1em;"> <%= check_box_tag "superpower_email" %> </div>
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...
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
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 is synonymous and an alias of using both
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!