-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add resume upload #265
add resume upload #265
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left some comments and thoughts. Let me know what you think and if what I said makes sense.
app/commands/profiles/update.rb
Outdated
@@ -0,0 +1,52 @@ | |||
module Profiles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So tbh I am not a big fan of this type of abstraction for the most part. To solve one of my problems, the call!
should either return nothing or raise an error. Alternatively, you could make it call
which can return true/false, but I much prefer raising custom errors specific to what went wrong.
(Another solution would be to pass in the controller in via self).
Now, given that we will be having a resume upload needed in the user_mentee_application
I think tailoring to that specifically, using a service object is acceptable. That being said, what should be passed in here is most likely a user
and a resume
.
There might be some complications with figuring out how to properly redirect, but we can figure that out later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you have some time, I'd be curious to hear a bit more about the time using this type of command/service object abstraction to encapsulate domain logic ended up causing an issue for you.
I'm also unclear about why you prefer call!
to return nothing or raising an error? I can understand wanting to bubble up an error, though as long as that error gets handled and reported (we probably could look into rollbar, honeybadger, or something similar), seems like gracefully recovering from it is important.
I have historically referenced the stitchfix post on anatomy of a service object as rough guidelines and specifically like returning rich objects. In this case, I chose to use symbols over a more complex object to simplify the implementation so I could solicit feedback more quickly. Additionally, it does mirror other libraries that follow similar patterns, like rectify. However, one thing I like about the symbols is that they they provide more context about what the method did rather than silently passing, raising an error, or true/false.
I actually do like failing noisily, so I don't mind rescuing and reraising if the Error is not what I expect it to be, but am wary of it until it's clear that we have a solid error reporting system in place that doesn't completely disrupt service to the end users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever i've done service objects I generally follow the call!
pattern (cause I like to fail noisily). (There are times when i'd have different methods if different places need different data from the same source).
My thought process goes like this, the !
means it will raise some error on failure and it is your job as the caller to catch it and handle appropriately. If it returns nothing, that means the service succeeded in what it was doing (this can also mean the service didn't need to do anything because making them idempotent is nice).
As for reporting, yeah we should prob implement that at some point. I generally tend to use Sentry and have had good results.
Sidenote:
Now we could include Dry Structs/ Dry types or ways to returned typed data to keep things consistent but I still don't see much value adding more levels of complexity.
app/forms/profile_form.rb
Outdated
@@ -0,0 +1,35 @@ | |||
class ProfileForm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given what I said above about extracting the resume to it's own service object, I don't think we really need this. Most the complications came because of that, and everything else is just simple rails form stuff which should be in the model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you typically approach handling form data when it's not associated with a particular resource on the unrelated resource's controller? Do you namespace it differently in the form? Do you create a separate params
method that requires those data, e.g.
def resume_params
params.require(:profile).permit(:resume, :resume_name, :current_resume_id)
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have too much of an opinion. separate params would be totally fine by me though
Responding to @toyhammered 's comments, I've now updated the PR to remove the form object. One outstanding concern I have is handling validation errors on the resume side of things, since that's not technically covered by the profile errors. I've also moved the business logic associating with updating the current profile and current resume into the profile controller and wrapped all of it in a single transaction block. |
…ion gets rolled back
@@ -307,6 +307,8 @@ Rails/HasAndBelongsToMany: | |||
Enabled: false | |||
Rails/TimeZone: | |||
Enabled: false | |||
Rails/HelperInstanceVariable: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rubocop incorrectly identified the commands
directory as a helper, even after I excluded using the following:
Rails/HelperInstanceVariable:
Exclude:
- "app/commands/**/**/*"
I've disabled for now.
@@ -44,11 +44,11 @@ | |||
end | |||
|
|||
transient do | |||
attached_picture { false } | |||
picture { nil } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I discovered when I investigated further that the transient
attribute is expecting the name of an attribute that is actually defined on the model. Since Profile
defines picture
, but not attached_picture
, I've updated the factory and spec to reflect that change.
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #265 +/- ##
==========================================
+ Coverage 97.84% 97.89% +0.05%
==========================================
Files 165 167 +2
Lines 2131 2190 +59
==========================================
+ Hits 2085 2144 +59
Misses 46 46
☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good, just couple small things!
app/commands/resumes/update.rb
Outdated
module Resumes | ||
class Update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this into a services
instead of commands.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure
app/commands/resumes/update.rb
Outdated
ActiveRecord::Base.transaction do | ||
user.current_resume.update!(current: false) | ||
|
||
if attributes[:resume] | ||
user.resumes.create!(attributes) | ||
next | ||
end | ||
|
||
user.resumes.find_by(id: current_resume_id).update!(current: true) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can optimize this quickly a tiny bit. We should be able to next
if attributes[:resume].blank?
&& current_resume.id == current_resume id.
Other than that this makes sense what is happening. Might be worth adding a small comment explaining that they can chose a different resume to make as current (took me a min to figure that out), and not need to upload one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some comments might clarify the intention, as the current way its written makes the logic flow read confusingly.
next
is not really a correct word for the context, but rubocop is irritating and i thin, in this case, wrong (Rails/TransactionExitStatement) rubocop/rubocop-rails#642. I can update to ignore rubocop in this context.
It should be this:
ActiveRecord::Base.transaction do
user.current_resume.update!(current: false)
return user.resumes.create!(attributes) if attributes[:resume]
user.resumes.find_by(id: current_resume_id).update!(current: true)
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what rubocop is saying is correct. I understand the next
and return
difference. It was more asking if we could include a guard clause because user.current_resume.update!(current: false)
and user.resumes.find_by(id: current_resume_id).update!(current: true)
might be the exact same record which could already be true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gotcha. updated.
if update_records | ||
redirect_to @profile, success: 'Profile successfully updated!' | ||
else | ||
# using this now misses validations and errors on the resume part of the form. | ||
flash.now[:form_errors] = @profile.errors.full_messages | ||
render :edit, status: :unprocessable_entity | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like it should always just redirect and the else
can just happen if there is an error (i.e: rescue).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify this, please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this service isn't going to be returning anything. Wouldn't it just be nil
or raising some type of error which you'd need to capture in the controller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it. yeah, fair enough.
What's the change?
This PR enables users to upload and update their resumes. This PR also enables direct upload, such that files get stored directly on a storage service and then the attachments are created via signed_id rather than an io to the filename.
In this PR, given that the Resume lives on the user, but the resource is a Profile (and they are only related via User), I encapsulated the form using the form object pattern for validations/collecting attributes, and passed it to a service/command object to process the relevant business logic.
Finally, I reorganized the form so have a bit more logical groupings, making it easier to skim and parse. Though I don't think it's in scope here, I think we should probably redesign the profile page form.
Highlights / Surprises / Risks / Cleanup
Demo / Screenshots
https://www.veed.io/embed/0667b780-b21b-4211-be27-8e58a82503e9
Issue ticket number and link
#179
Checklist before requesting a review
Please delete items that are not relevant.