Conversation
This commit changes the behaviour of LessonsController#destroy from an archive (soft-delete) operation to a hard destroy operation. Due to the possibility of having existing lessons in the database that were soft-deleted, we retain the `Lesson.unarchived` scope and only return Lessons where `archived_at` is null. We also remove the Archive and Unarchive operations, since these are no longer needed.
42330f6 to
d4170e8
Compare
| has_one :project, dependent: :destroy | ||
| accepts_nested_attributes_for :project | ||
|
|
||
| before_validation :assign_school_from_school_class |
There was a problem hiding this comment.
If there's nothing using the archived_at column now, it would be good to add it to the ignored column list (docs)
As well as signifying to us that the column isn't used anymore, this will mean that active record will stop including the column in generated SQL, making it easier for us to delete later if we wanted to.
There was a problem hiding this comment.
Pull request overview
This PR updates the Lessons REST API and model so that deleting a Lesson performs a hard delete (and cascades to its associated Project), removing the prior “archive/unarchive” soft-delete behavior.
Changes:
- Switch
Lessonfrom non-destroyable/archivable to destroyable, and cascade-delete its associatedProject. - Simplify
LessonsController#destroyto delete the record (removing undo/unarchive behavior) and adjust lesson listing to always exclude previously-archived lessons. - Remove archive/unarchive concept operations and related specs; update request/model specs to assert hard-deletion behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
app/controllers/api/lessons_controller.rb |
Replace archive/unarchive destroy logic with hard delete; adjust lesson filtering to always use unarchived. |
app/models/lesson.rb |
Change project association to dependent: :destroy; remove archive-related callbacks/methods/scopes. |
app/views/api/lessons/_lesson.json.jbuilder |
Stop including archived_at in lesson JSON output. |
app/dashboards/lesson_dashboard.rb |
Remove archived_at from Administrate dashboard fields. |
spec/models/lesson_spec.rb |
Update model specs to assert lessons are destroyable and cascade-delete the project; remove archive-related specs. |
spec/features/lesson/listing_lessons_spec.rb |
Update listing behavior expectations around previously archived lessons; remove include-archived behavior tests. |
spec/features/lesson/destroying_a_lesson_spec.rb |
Update request specs to assert hard deletion of lesson and associated project. |
lib/concepts/lesson/operations/archive.rb |
Removed (no longer archiving). |
lib/concepts/lesson/operations/unarchive.rb |
Removed (no longer unarchiving). |
spec/concepts/lesson/archive_spec.rb |
Removed (archive operation no longer exists). |
spec/concepts/lesson/unarchive_spec.rb |
Removed (unarchive operation no longer exists). |
| rescue StandardError => e | ||
| Sentry.capture_exception(e) | ||
| render json: { error: "Error destroying lesson: #{e}" }, status: :unprocessable_entity |
There was a problem hiding this comment.
ApiController already has rescue_from StandardError that reports to Sentry and renders a 500 with a consistent { error: "ExceptionClass: message" } shape. This per-action rescue StandardError changes failures during destroy into a 422, double-reports to Sentry, and can mask real server errors (e.g. NoMethodError, FK violations) as client errors. Prefer removing this rescue entirely, or rescuing only specific expected exceptions (e.g. ActiveRecord::RecordNotDestroyed) and letting everything else bubble up to the global handler.
| rescue StandardError => e | |
| Sentry.capture_exception(e) | |
| render json: { error: "Error destroying lesson: #{e}" }, status: :unprocessable_entity |
There was a problem hiding this comment.
Mostly agree with Copilot
Was there a specific kind of error you were worried about (such as ActiveRecord::RecordNotDestroyed)- if you are, I would make this more specific.
And then would this error be expected or not? I.e. is there validation on the records that would prevent them from being destroyed?
If it's expected, then there's no need to re-raise. If it's unexpected then maybe we don't need this case at all as if it happens the error will bubble up and we'll be notified about it.
| def filtered_lessons_scope | ||
| archive_scope = params[:include_archived] == 'true' ? Lesson : Lesson.unarchived | ||
| scope = params[:school_class_id] ? archive_scope.where(school_class_id: params[:school_class_id]) : archive_scope | ||
| scope = params[:school_class_id] ? Lesson.unarchived.where(school_class_id: params[:school_class_id]) : Lesson.unarchived | ||
| scope = scope.joins(:project).where(projects: { identifier: params[:project_identifier] }) if params[:project_identifier].present? |
There was a problem hiding this comment.
PR description mentions retaining an “unfiltered” scope/default to handle soft-deleted lessons, but the controller now hard-codes Lesson.unarchived and removes the previous include_archived=true behavior entirely. Either update the PR description to match the behavior, or reintroduce an explicit unfiltered/include-archived path if clients still rely on it.
zetter-rpf
left a comment
There was a problem hiding this comment.
Thanks for taking the time to tidy up archiving, it's great to be able to remove something that isn't being used.
I've left a couple of comments/suggestions
This PR changes the behaviour of
LessonsController#destroyfrom soft-delete (aka "archived") to hard delete.During the development of the front end features for this issue, in RaspberryPiFoundation/editor-standalone#720, @zetter-rpf asked whether we should be doing soft- or hard-delete. I had initially built the front-end UI on the basis that the existing back-end implementation existed for a reason.
@kylec-rpf after discussion with @zetter-rpf said that we should implement this as a hard-delete feature.
Status
Points for consideration:
This PR changes
Lesson's relation withProjectfrom:nullifyto:destroy:Lesson.rb:
has_one :project, dependent: :nullifybecomeshas_one :project, dependent: :destroyThis means that the Project associated with the Lesson will be destroyed when the lesson is destroyed. This seems to me the right thing to do, but I am open to other interpretations of the logic in this area.
What's changed?
LessonsController#destroyto destroy the lesson.Lessons.unfilteredscope as the default scope, just in case there are soft-deleted projects in the database.archived_atcolumn on Lessons for similar reasons.Steps to perform after deploying to production
None should be required, unless we want to delete all the soft-deleted projects from the database at some point in the future.
Merge and deploy RaspberryPiFoundation/editor-standalone#720