claims to hold them The scaffolding is down. 203 subject, 2,275 disease and 4,281 keyword tags, and 25,356 links, deleted — backed up first to a 1.9MB JSON of replayable rows, because "we can always put it back" should be true rather than said. The 16 system rows stay: categories point at them. With them go the things that only existed to feed them — the classify_questions task, its snapshot helpers, POST /tags/classify and its status poll — and the three Taxonomy tabs that would now always read zero. A tab showing 0 forever teaches people the page is broken. The organ-system filter in the session builder moved onto categories with the rest, including everything beneath a matched topic, so it groups the way the analysis does. Registration: `settings:registration_enabled` was set to false, and there was no switch anywhere on the site to set it back. The API had always accepted it; the Site policy page had never shown it. So the site could be closed to new members with the admin looking at three switches, all correct, and no way to see the one that was actually refusing them. It is now the first switch on that page, and says plainly that the ones below it have nothing to act on while it is off. The SSO-only flag was hidden the same way and is shown when SSO is configured. Deleting a topic no longer silently unfiles its questions. It asks where they go, and says how many are waiting, unless the topic is empty — the same rule promotion now follows. Its extra category links move too, minus any that would duplicate a pair the destination already has. Back links: Trash, Extraction jobs, Taxonomy and the Handbook had none at all, and Access pointed at the wrong section. They are one component now, each returning one step to the section it was opened from. Editorial has its own entry in the section bar, so its Tools card is gone rather than being a second door to the same room. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
93 lines
4.3 KiB
Python
93 lines
4.3 KiB
Python
"""Exercise Alembic's real offline path; never connect to an external database."""
|
|
import io
|
|
import os
|
|
from pathlib import Path
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
|
|
import test_quiz_builder as fixtures
|
|
from app.database import Base # Initialize the app engine with disposable SQLite before offline URL override.
|
|
from app.models.question import Question
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
from alembic.script import ScriptDirectory
|
|
|
|
|
|
class CategoryMigrationTests(unittest.TestCase):
|
|
def test_graph_upgrade_and_downgrade(self):
|
|
backend = Path(__file__).resolve().parents[1]
|
|
output = io.StringIO()
|
|
config = Config(output_buffer=output)
|
|
config.set_main_option("script_location", str(backend / "alembic"))
|
|
scripts = ScriptDirectory.from_config(config)
|
|
self.assertEqual(len(scripts.get_heads()), 1)
|
|
self.assertEqual(scripts.get_revision("c82d19e4a601").down_revision, "5f8c1c2a9d40")
|
|
list(scripts.walk_revisions())
|
|
# sql=True uses no engine/connection; the URL selects PostgreSQL DDL only.
|
|
with patch.dict(os.environ, {"DATABASE_URL": "postgresql://unused@127.0.0.1/offline_only"}):
|
|
command.upgrade(config, "5f8c1c2a9d40:c82d19e4a601", sql=True)
|
|
sql = output.getvalue()
|
|
self.assertIn("ADD COLUMN IF NOT EXISTS parent_id INTEGER", sql)
|
|
self.assertIn("WHERE conrelid = 'question_categories'::regclass", sql)
|
|
self.assertIn("FOREIGN KEY (parent_id) REFERENCES question_categories(id) ON DELETE RESTRICT", sql)
|
|
self.assertNotIn("UPDATE question_categories", sql)
|
|
output.seek(0)
|
|
output.truncate()
|
|
command.downgrade(config, "c82d19e4a601:5f8c1c2a9d40", sql=True)
|
|
self.assertIn("DROP CONSTRAINT IF EXISTS fk_question_categories_parent", output.getvalue())
|
|
self.assertIn("DROP COLUMN IF EXISTS parent_id", output.getvalue())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
|
|
|
|
class DeletingATopicTests(unittest.TestCase):
|
|
"""Where the questions go has to be said.
|
|
|
|
Deleting a topic used to leave them filed nowhere — no discipline, no
|
|
organ system, no relevance, no row on any tab of the analysis. In the bank
|
|
and invisible to every page that counts them.
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.bank = fixtures.BuilderTests()
|
|
self.bank.setUp()
|
|
self.client = self.bank.client
|
|
self.db = self.bank.db
|
|
self.bank.user = self.bank.mod
|
|
|
|
def tearDown(self):
|
|
self.bank.tearDown()
|
|
|
|
def test_a_topic_holding_questions_will_not_go_quietly(self):
|
|
# Category 3 holds question 3 in the fixture bank.
|
|
response = self.client.delete('/question-categories/3')
|
|
self.assertEqual(response.status_code, 400, response.text)
|
|
self.assertIn('Choose a topic to move them to', response.json()['detail'])
|
|
self.assertIsNotNone(self.db.get(Question, 3).question_category_id)
|
|
|
|
def test_naming_a_destination_moves_them_and_deletes_the_topic(self):
|
|
from app.models.question_category import QuestionCategory
|
|
response = self.client.delete('/question-categories/3', params={'move_to': 2})
|
|
self.assertEqual(response.status_code, 204, response.text)
|
|
self.assertEqual(self.db.get(Question, 3).question_category_id, 2)
|
|
self.assertIsNone(self.db.get(QuestionCategory, 3))
|
|
|
|
def test_an_empty_topic_goes_without_being_asked(self):
|
|
from app.models.question_category import QuestionCategory
|
|
# Category 4 holds nothing.
|
|
self.assertEqual(self.client.delete('/question-categories/4').status_code, 204)
|
|
self.assertIsNone(self.db.get(QuestionCategory, 4))
|
|
|
|
def test_an_extra_link_moves_too_and_does_not_duplicate(self):
|
|
from app.models.question_category import QuestionCategoryLink
|
|
# Question 1 is filed under category 1 and also linked to 3.
|
|
self.db.add(QuestionCategoryLink(question_id=1, category_id=3))
|
|
self.db.add(QuestionCategoryLink(question_id=1, category_id=2))
|
|
self.db.commit()
|
|
self.assertEqual(
|
|
self.client.delete('/question-categories/3', params={'move_to': 2}).status_code, 204)
|
|
pairs = self.db.query(QuestionCategoryLink).filter_by(question_id=1, category_id=2).count()
|
|
self.assertEqual(pairs, 1)
|