Skip to content
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

Allow deletion of Assignment's with no groups #6880

Merged
merged 30 commits into from
Feb 17, 2024

Conversation

mimischly7
Copy link
Contributor

@mimischly7 mimischly7 commented Dec 24, 2023

Motivation and Context

An authorized instructor should be able to delete an assignment if and only if the assignment does not have any groups.
Although routes.rb already generated the endpoint for destroying an assignment, there was no #destroy action in controllers/api/assignments_controller.rb.

button-enabled button-disabled

Your Changes

Description:
I implemented a #destroy action in controllers/api/assignments_controller.rb which deletes the Assignment corresponding to the given id if this assignment does not have any groups. The logic for checking whether an assignment has any groups is performed at the model level using the dependent: restrict_with_exception attribute of rails associations. The controller catches the potential exception and responds to the request accordingly.

Type of change (select all that apply):

  • New feature (non-breaking change which adds functionality)

Testing

Wrote several rspec tests in spec/controller/api/assignments_controller_spec.rb aiming to capture the various scenarios and contexts under which one may attempt to delete an assignment.

Checklist

  • I have performed a self-review of my own code.
  • I have verified that the pre-commit.ci checks have passed.
  • I have verified that the CI tests have passed.
  • I have reviewed the test coverage changes reported on Coveralls.
  • I have added tests for my changes.
  • I have updated the Changelog.md file.
  • I have made changes to the documentation and linked to that pull request below.

Pull request to make documentation changes (if applicable)

MarkUsProject/Wiki#201

Although routes.rb already generated the endpoint for destroying an assignment, there was no #destroy action in controllers/api/assignments_controller.rb. An authorized instructor should be able to delete an assignment if and only if the assignment does not have any groups. I implemented this #destroy action and wrote corresponding rspec tests in spec/controller/api/assignments_controller_spec.rb.
Added informative en.yml messages for the different possible scenarios that can arise when attempting to delete an assignment (using those in the api controller instead of the former hardcoded messages) and improved the style of the expectations in the corresponding rspec tests.
Corrected a I18 style problem and updated the Changelog (though this will change again to get the PR number).
@mimischly7 mimischly7 marked this pull request as draft December 24, 2023 20:29
@coveralls
Copy link
Collaborator

coveralls commented Dec 24, 2023

Pull Request Test Coverage Report for Build 7932654664

Details

  • 0 of 69 (100.0%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.01%) to 91.733%

Totals Coverage Status
Change from base Build 7925404029: 0.01%
Covered Lines: 38935
Relevant Lines: 41822

💛 - Coveralls

Instead of having the logic that checks if the assignment has any
groups inside the api-assignments-controller, this is now taking place in the model itself, simply by including 'dependent: restrict_with_exception' in the association definition. Now, the api-assignments controller has a begin-rescue block to catch this exception.
@mimischly7 mimischly7 marked this pull request as ready for review January 6, 2024 22:34
Copy link
Collaborator

@david-yz-liu david-yz-liu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mimischly7 good! So in addition to this change, I want you to add a button to delete the assignment from the web interface (and more specifically from the assignment edit form.

Add the button and give it a red background using one of the built-in colour constants, so that it's very clearly distinguished from other buttons. Make sure the button is disabled if there are any groups, and that there is a confirmation dialog box. This will need a parallel assignments controller route on the non-API controller.

Changelog.md Outdated
@@ -7,6 +7,7 @@
- Bug Fix: Prevent assigning inactive graders to a group (#6801)
- Added new API routes for the `create` and `destroy` actions of SectionsController (#6873)
- Display error message in real-time when running automated tests, rather than waiting for page refresh (#6878)
- - Added `destroy` action to `controllers/api/assignments_controller` (#6880)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an extra - here. Also, please do a pull from master; we've made a new minor release, so you'll need to add this to the new [unreleased] section

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you missed this

Added a destroy action to the non-API Assignments controller - and the corresponding route in routes.rb, to be used by the UI (this non-API route will be used for deletion of assignments from the UI).

Recall that an assignment should be deleted only if it has no groups. This logic is handled in the model through an appropriate association feature. Nevertheless, this method must NOT be called on an assignment with groups. Its precondition is that the Assignment object with which it is called has no groups. This should be reasonable; for example, the delete button for an assignment should be visible only if the assignment has no groups.
All elements in the assignment-edit page are found inside the _form.html.erb partial. There, added a new button to delete assignments using the button_to helper, and the button makes a DELETE request to the destroy action of the non-API assignments controller.

The button is conditionally rendered based on whether the assignment has any groups (using ERB's templating capabilities). It is visible only if the corresponding assignment has no groups.
CSS class for making the assignment-delete button red, with bold font. This is just temporary and subject to change. Its style does not really align with the general style of Markus' UI.
The red button should only be rendered if (1) you are in the assignment-edit page and (2) the current asisgnment has no groups. Previously, only (2) was being checked (i.e. that the assignment has no groups). So if, for example, you went to create a new assignment, the condition of the if statement did not fail and rails attempted to render the button, leading to error because in most cases @assignment is undefined (nil). I added the condition that we are in the context of the edit action of the assignment controller to face this issue (only in that case will the delete button be rendered).
Utilised the external class FlashResponder class to automatically place the appropriate success message in the ActionDispatch:Flash object. This strategy already takes place in the Assignments controller in the #create and #update actions. In particular, given the line <respond_with @assignment ...>, the FlashResponder checks the status of the @assignment --that is, if it was just created, updated, or destroyed-- and places the appropriate message in the flash object (which should be defined in the appropriate format in a en.yml file). Then, by definition of how the flash object works, the next (redirected-to) action (and thus the view rendered by the next action) has access to the data stored in flash. In this particular case, the assignment_content layout view renders the _flash_message.html.erb partial, and it is this partial that used the flash object (doing flash[:...]).
Handling the case where the action is called with an Assignment that still has groups. This is impossible from the UI (since the delete button only shows if the assignment does not have any groups), but it possible from direct HTTP requests.
Testing behaviour when request is not authorized (fail to destroy), when the request is authorized but the assignment still has groups (fail to destroy), and when the request is authorized and the assignment has no groups (succeeds to destroy).
Firstly, instead of completely hiding the assignment-delete button for assignments with groups (in their 'edit' pages), now it is visible but disabled, and when the mouse is over it a tooltip appears saying 'Assignment has groupings'. Also, in the case where the button is enabled (i.e. for assignments with no groups), I improved the style by using Markus' predefined constants (found in assets/stylesheets/common/_constants.scss) and defining a new style for the id #assignment-delete (targeting specifically that delete button); the button also is of class .button, utilising predefined styling. The only two files modified for these changes are the partial app/views/assignments/_form.html.erb and the stylesheet app/assets/stylesheets/common/_markus.scss.
@mimischly7 mimischly7 removed the request for review from david-yz-liu February 7, 2024 06:23
@mimischly7 mimischly7 self-assigned this Feb 7, 2024
change belonged to unreleased section
@mimischly7 mimischly7 removed their assignment Feb 7, 2024
Copy link
Collaborator

@david-yz-liu david-yz-liu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mimischly7 great work! Sorry for the delay on reviewing this. I left a few more comments for you to polish this PR.

Changelog.md Outdated
@@ -7,6 +7,7 @@
- Bug Fix: Prevent assigning inactive graders to a group (#6801)
- Added new API routes for the `create` and `destroy` actions of SectionsController (#6873)
- Display error message in real-time when running automated tests, rather than waiting for page refresh (#6878)
- - Added `destroy` action to `controllers/api/assignments_controller` (#6880)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you missed this

@@ -301,6 +301,15 @@ button,
min-width: 70px;
padding: 0.25em 0.5em;
}

&.delete-button {
background: #f00;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For colours, you should always use existing colour constants (start by looking in _constants.scss)

@@ -301,6 +301,15 @@ button,
min-width: 70px;
padding: 0.25em 0.5em;
}

&.delete-button {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more appropriate class name is danger-button (connoting a "dangerous" action, which might not just be limited to deletion in the future)

app/assets/stylesheets/common/_markus.scss Show resolved Hide resolved
Comment on lines 303 to 308
assignment_id = params[:id]
assignment = Assignment.find_by(id: assignment_id)
if assignment.nil?
render 'shared/http_status', locals: { code: '404', message:
I18n.t('assignments.assignment_not_found',
invalid_id: assignment_id) }, status: :not_found
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is not necessary; look at other methods in this class. In particular, the record method will give you the Assignment instance corresponding to params[:id]. There's existing code that computes this and handles missing objects more generally across all controllers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given our discussion on Friday on whether we should check in the controller whether the requested record is actually valid, even though the SessionHandler class method check_record (as a before_action method) already checks that, I decided to keep the extra check. Let me know if you want me to remove it.


<!--Conditional Rendering-->
<% if (@assignment.groups.length === 0) and (action_name=="edit")%>
<%= button_to "Delete",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember that no string literals should appear in your code. Always define strings in locale files.

form_class: 'display-inline-block',
title: t('helpers.submit.delete', model: Assignment.model_name.human),
remote: true %>
<% end %>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is an extra space here

class: 'delete-button',
form_class: 'display-inline-block',
title: t('helpers.submit.delete', model: Assignment.model_name.human),
remote: true %>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the remote: true is not necessary (I encourage you to look up this option to understand why)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see why. If I understand correctly, by setting remote:true we allow the generated form to be submitted asynchronously (with AJAX), but this requireds that we handle it appropriately in the respond_to block in our action with a case for javascript; since we are not doing this in the action, there is no point.

title: t('helpers.submit.delete', model: Assignment.model_name.human),
remote: true %>
<% end %>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete this line

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line hasn't been deleted

expect(assignment.groups).to be_empty
delete :destroy, params: { id: assignment.id, course_id: course.id }
expect(response).to have_http_status(200)
expect(Assignment.all).to eq([])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this assertion. The next line is more specific to what you're actually testing

Most notably, simplifying the erb code in the _form partial for assignments -- for when to show the delete button (and when it should be disabled). Includes also other minor changes.
title: t('helpers.submit.delete', model: Assignment.model_name.human),
remote: true %>
<% end %>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line hasn't been deleted

@@ -303,6 +303,12 @@ button,
}
}

#assignment-delete {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay this is better, but (1) make it a class, and (2) call the class danger-button, which is more general.

@@ -299,6 +299,24 @@ def submit_file
upload_file(grouping, only_required_files: assignment.only_required_files)
end

def destroy
assignment = record
if assignment.nil?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't necessary, you can remove it. The check_record helper is sufficient to check for this

@@ -657,6 +657,17 @@ def lti_settings
render layout: 'assignment_content'
end

def destroy
@assignment = @record
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use record (as with the other methods)

@@ -489,4 +489,22 @@
data: { disable_with: t('working') } %>
</p>
<% end %>

<% if (action_name=="edit") %>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If statements don't need parentheses in Ruby. Also, put spaces around ==

<%= button_to t(:delete),
course_assignment_path(@current_course, @assignment),
{
data: { confirm: "Are you sure you want to delete this assignment?" },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This string also needs to be internationalized

data: { confirm: "Are you sure you want to delete this assignment?" },
method: 'delete',
class: has_groups ? nil : 'button',
id: has_groups ? nil : 'assignment-delete',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following my above comment, don't use an id here, instead conditionally add a second class to 'button' above

disabled: has_groups
}
%>
<%end %>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a space missing after the <%

@@ -502,6 +507,35 @@
expect(response).to have_http_status(403)
end
end
context 'DELETE assignment' do
it 'should successfully delete assignment because the assignment has no groups' do
assignment # since lazy let is used for creating an assignment, I invoke it here to trigger its execution
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary, as assignment is immediately used on the next line

expect(Assignment.exists?(assignment.id)).to eq(false)
end
it 'fails to delete assignment because assignment has groups' do
assignment # since lazy let is used for creating an assignment, I invoke it here to trigger its execution
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

Includes defining strings in locale file, improving the css, and small formatting fixes.
@david-yz-liu david-yz-liu merged commit a9a4504 into MarkUsProject:master Feb 17, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants