Question ids come from a sequence and are never reissued, and fourteen tables point at them — attempts, quiz membership, exam membership, media, article links, notes, favourites, feedback. Deleting the row took all of that with it, so "restore" could only ever have meant typing the text in again as a different question. DELETE now sets deleted_at. The question leaves the bank, the builder, search and every share path at once, because the exclusion lives in general_question_predicate rather than at each call site. Restoring puts back the same id, so everything that pointed at it still does. Erasing for real requires the trash first and a moderator, and the confirmation says what goes with it. The trash page holds questions instead of tests. A test is a selection you can remake in a minute; nobody wanted those back. Used and withdrawn invite codes can be removed — an unused one is still withdrawn rather than deleted, so it stays visible as having been issued and stopped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
71 lines
3 KiB
Python
71 lines
3 KiB
Python
"""Deleting a question hides it; restoring it puts back the same question.
|
|
|
|
The point of the whole arrangement is the id. Ids come from a sequence and are
|
|
never reissued, and fourteen tables point at this one — attempts, quiz
|
|
membership, exam membership, media, notes, favourites, feedback. Erasing the
|
|
row takes all of that with it, so a "restore" that re-inserted the text would
|
|
be a different question wearing the same words.
|
|
"""
|
|
import unittest
|
|
|
|
import test_quiz_builder as fixtures
|
|
from app.models.question import Question
|
|
|
|
|
|
class QuestionTrashTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.bank = fixtures.BuilderTests()
|
|
self.bank.setUp()
|
|
self.client = self.bank.client
|
|
self.bank.user = self.bank.mod
|
|
|
|
def tearDown(self):
|
|
self.bank.tearDown()
|
|
|
|
def row(self, question_id):
|
|
self.bank.db.expire_all()
|
|
return self.bank.db.get(Question, question_id)
|
|
|
|
def test_delete_hides_the_question_but_keeps_the_row(self):
|
|
self.assertEqual(self.client.delete("/questions/1").status_code, 204)
|
|
kept = self.row(1)
|
|
self.assertIsNotNone(kept, "the row must survive or nothing can point at it")
|
|
self.assertIsNotNone(kept.deleted_at)
|
|
|
|
def test_a_deleted_question_leaves_the_bank_and_the_builder(self):
|
|
before = self.client.get("/questions/bank").json()
|
|
before_ids = [q["id"] for q in before["questions"]]
|
|
self.assertIn(1, before_ids)
|
|
|
|
self.client.delete("/questions/1")
|
|
|
|
after = self.client.get("/questions/bank").json()
|
|
self.assertNotIn(1, [q["id"] for q in after["questions"]])
|
|
# Excluded in the shared predicate, so the builder's count moves too
|
|
# rather than each caller having to remember.
|
|
self.assertEqual(self.client.get("/questions/builder/count", params={"state": "all"}).json()["count"],
|
|
len(before_ids) - 1)
|
|
|
|
def test_the_trash_lists_it_and_restore_brings_back_the_same_id(self):
|
|
self.client.delete("/questions/1")
|
|
trash = self.client.get("/questions/trash").json()
|
|
self.assertEqual([row["id"] for row in trash], [1])
|
|
|
|
self.assertEqual(self.client.patch("/questions/1/restore").status_code, 200)
|
|
self.assertIsNone(self.row(1).deleted_at)
|
|
self.assertIn(1, [q["id"] for q in self.client.get("/questions/bank").json()["questions"]])
|
|
self.assertEqual(self.client.get("/questions/trash").json(), [])
|
|
|
|
def test_restoring_something_that_is_not_deleted_is_refused(self):
|
|
self.assertEqual(self.client.patch("/questions/1/restore").status_code, 400)
|
|
|
|
def test_erasing_requires_the_trash_first(self):
|
|
# Straight to permanent would make the trash a formality.
|
|
self.assertEqual(self.client.delete("/questions/1/permanent").status_code, 400)
|
|
self.client.delete("/questions/1")
|
|
self.assertEqual(self.client.delete("/questions/1/permanent").status_code, 204)
|
|
self.assertIsNone(self.row(1))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|