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
Only allow teachers who attend workshops to print workshop certificates #32678
Conversation
const enrolledWorkshopsTable = shallow( | ||
<EnrolledWorkshopsTable workshops={workshops} /> | ||
); | ||
|
||
// Click the "Print Certificate" button | ||
enrolledWorkshopsTable | ||
.find('tr') | ||
.last() | ||
.find('tbody tr') |
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.
Changed this find
call because there is a header row that had me banging my head against the wall for a bit.
Going to get to a review of what you've got here, but first: What you've got here is a good client-side improvement for the user experience when viewing a workshop you didn't attend, but it doesn't actually prevent someone from going to |
@@ -78,6 +78,7 @@ class EnrolledWorkshopsTable extends React.Component { | |||
<Button | |||
onClick={() => this.openCertificate(workshop)} | |||
style={styles.button} | |||
disabled={!workshop.attended} |
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.
👍 The way you've serialized out the relevant data makes this line, where we eventually use it to make a decision, so nice. Good choice.
state: 'Not Started' | ||
state: 'Not Started', | ||
user_id: 123, | ||
attended: false |
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.
Not required right now, but seeing this large fixture again, and how many places you had to edit it, reminded me that we have a better way to generate test data now: Rosie.js, the JavaScript equivalent to FactoryBot. Here's an example of cleaning up a large test fixture with Rosie.
.find('Button') | ||
.first(); | ||
|
||
expect(printCertificateButton.prop('disabled')).to.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.
The fact that disabled
wasn't respected on this simulated button click may be by design - in particular, we're shallow-rendering here, and using a bootstrap Button
component, so in theory we're not even rendering the DOM element that would normally enforce disabled
as a behavior.
I think your approach here, checking the prop itself, is fine. Just make sure to update the comment on line 146 since we're not actually clicking the button anymore.
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.
Great first pass! I have a few comments to address before we move forward here.
@islemaster Can you think of an example you could share of how we gracefully handle this in other places? |
One good example of this pattern of catching business logic edge cases in the controller is the route for cancelling a workshop enrollment. It takes an enrollment code as part of the URL. GET /pd/workshop_enrollment/<code>/cancel This route handles two special edge cases. One, perhaps we can't find an enrollment for the code you provided at all. In that case, we render this view saying the registration was not found. @enrollment = Pd::Enrollment.find_by_code params[:code]
if @enrollment.nil?
render :not_found Two, maybe we find the enrollment, but you already attended the workshop, so it's too late to cancel! In that case, we render a view that says "You have already attended this workshop." elsif @enrollment.attendances.any?
render :attended Finally, if neither of the above happens, we fall through and render the cancellation form. I think you could take a similar approach, or since we wouldn't expect someone to open this URL unless they were actually trying to bypass our UI, we could simply return a 404 if they didn't attend the workshop. After all, they didn't earn a certificate yet, so return head :not_found unless enrollment.attendances.any? |
Hold on review, need to fix some tests. |
|
||
get :generate_certificate, params: {enrollment_code: @enrollment.code} | ||
assert_response :missing | ||
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.
🎉 Awesome.
@@ -6,6 +6,8 @@ class Pd::WorkshopCertificateController < ApplicationController | |||
find_by: :code, id_param: :enrollment_code | |||
|
|||
def generate_certificate | |||
return render_404 unless @enrollment&.attendances&.any? |
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 the first safe-navigation operator isn't needed here because the load_resource
line above that populates @enrollment
should cause a 404 if it's not found before we reach this code; and the second safe-navigation operator shouldn't be necessary because if you have an Enrollment
with no attendances, its attendances
association will give an empty array, not nil
. But use tests to check these cases before taking my word for it!
return render_404 unless @enrollment&.attendances&.any? | |
return render_404 unless @enrollment.attendances.any? |
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.
Makes sense -- we do have a test that an error is raised if a nonsense enrollment code is provided, so I think your first assumption is sound. I tested manually in console that an enrollment with no attendances returns false from enrollment.attendances.any?
, not an error. Also tested the generate_workshop_certificate
page with this code change and an enrollment with no attendance got the 404 I'd expect.
@@ -63,6 +63,10 @@ def enrollment_code | |||
@scope.try(:[], :enrollment_code) | |||
end | |||
|
|||
def attended | |||
object.enrollments.find_by(code: @scope.try(:[], :enrollment_code))&.attendances&.any? |
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.
Whereas here we need the two safe-navigation operators because find_by
might have returned 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.
Great work!
PLC-687: Prevents teachers who did not attend workshops from printing certificates of completion.
Testing story
Added a test for the new behavior. I was trying to test by actually generating a click event, but I ran into this bug (I think). In order to get feedback I added a less great test, but happy to adjust if there are ideas about how to work around this issue.
Also added/modified testing related to new server side logic to prevent rendering workshop certificates for teaches who haven't attended a workshop.
Reviewer Checklist: