summaryrefslogtreecommitdiff
path: root/tracdb
diff options
context:
space:
mode:
authorBaptiste Mispelon <bmispelon@gmail.com>2025-02-01 01:03:15 +0100
committerBaptiste Mispelon <bmispelon@gmail.com>2025-02-01 01:03:53 +0100
commit9b4e170aa38f4a26ffe579b622ab8a9f76301a29 (patch)
tree7079fdb5a478c682d269d83dbd7fa0f03b6efa75 /tracdb
parentcf7ed27a898fd15495f81018b0a43ab4b193becc (diff)
Revert temporary upgrade to Django 5.2a1
This reverts commits cf7ed27a898fd15495f81018b0a43ab4b193becc. and fabb4bc163858e945f5c29074afae5a54f1f12cd.
Diffstat (limited to 'tracdb')
-rw-r--r--tracdb/models.py46
-rw-r--r--tracdb/testutils.py59
2 files changed, 97 insertions, 8 deletions
diff --git a/tracdb/models.py b/tracdb/models.py
index 6dc81934..67a91990 100644
--- a/tracdb/models.py
+++ b/tracdb/models.py
@@ -9,6 +9,38 @@ A few notes on tables that're left out and why:
* All the session and permission tables: they're just not needed.
* Enum: I don't know what this is or what it's for.
* NodeChange: Ditto.
+
+These models are far from perfect but are Good Enough(tm) to get some useful data out.
+
+One important mismatch between these models and the Trac database has to do with
+composite primary keys. Trac uses them for several tables, but Django does not support
+them yet (ticket #373).
+
+These are the tables that use them:
+
+ * ticket_custom (model TicketCustom)
+ * ticket_change (model TicketChange)
+ * wiki (model Wiki)
+ * attachment (model Attachment)
+
+To make these work with Django (for some definition of the word "work") we mark only
+one of their field as being the primary key (primary_key=True).
+This is obviously incorrect but — somewhat suprisingly — it doesn't break **everything**
+and the little that does actually work is good enough for what we're trying to do:
+
+ * Model.objects.create(...) correctly creates the object in the db
+ * Most queryset/manager methods work (in particular filter(), exclude(), all()
+ and count())
+
+On the other hand, here's what absolutely DOES NOT work (the list is sadly not
+exhaustive):
+
+ * Updating a model instance with save() will update ALL ROWS that happen to share
+ the value for the field used as the "fake" primary key if they exist (resulting
+ in a DBError)
+ * The admin won't work (the "pk" field shortcut can't be used reliably since it can
+ return multiple rows)
+
"""
from datetime import date
@@ -129,11 +161,11 @@ class Ticket(models.Model):
class TicketCustom(models.Model):
- pk = models.CompositePrimaryKey("ticket", "name")
ticket = models.ForeignKey(
Ticket,
related_name="custom_fields",
db_column="ticket",
+ primary_key=True, # XXX See note at the top about composite pk
on_delete=models.DO_NOTHING,
)
name = models.TextField()
@@ -148,11 +180,11 @@ class TicketCustom(models.Model):
class TicketChange(models.Model):
- pk = models.CompositePrimaryKey("ticket", "_time", "field")
ticket = models.ForeignKey(
Ticket,
related_name="changes",
db_column="ticket",
+ primary_key=True, # XXX See note at the top about composite pk
on_delete=models.DO_NOTHING,
)
author = models.TextField()
@@ -260,8 +292,9 @@ class Revision(models.Model):
class Wiki(models.Model):
- pk = models.CompositePrimaryKey("name", "version")
- name = models.TextField()
+ name = models.TextField(
+ primary_key=True
+ ) # XXX See note at the top about composite pk
version = models.IntegerField()
_time = models.BigIntegerField(db_column="time")
time = time_property("_time")
@@ -279,9 +312,10 @@ class Wiki(models.Model):
class Attachment(models.Model):
- pk = models.CompositePrimaryKey("type", "id", "filename")
type = models.TextField()
- id = models.TextField()
+ id = models.TextField(
+ primary_key=True
+ ) # XXX See note at the top about composite pk
filename = models.TextField()
size = models.IntegerField()
_time = models.BigIntegerField(db_column="time")
diff --git a/tracdb/testutils.py b/tracdb/testutils.py
index 13a31e41..8ac6192e 100644
--- a/tracdb/testutils.py
+++ b/tracdb/testutils.py
@@ -1,5 +1,60 @@
+from copy import deepcopy
+
from django.apps import apps
-from django.db import connections
+from django.db import connections, models
+
+# There's more models with a fake composite pk, but we don't test them at the moment.
+_MODELS_WITH_FAKE_COMPOSITE_PK = {"ticketcustom"}
+
+
+def _get_pk_field(model):
+ """
+ Return the pk field for the given model.
+ Raise ValueError if none or more than one is found.
+ """
+ pks = [field for field in model._meta.get_fields() if field.primary_key]
+ if len(pks) == 0:
+ raise ValueError(f"No primary key field found for model {model._meta.label}")
+ elif len(pks) > 1:
+ raise ValueError(
+ f"Found more than one primary key field for model {model._meta.label}"
+ )
+ else:
+ return pks[0]
+
+
+def _replace_primary_key_field_with_autofield(model, schema_editor):
+ """
+ See section about composite pks in the docstring for models.py to get some context
+ for this.
+
+ In short, some models define a field as `primary_key=True` but that field is not
+ actually a primary key. In particular that field is not supposed to be unique, which
+ interferes with our tests.
+
+ For those models, we remove the `primary_key` flag from the field, and we add a
+ new `testid` autofield. This makes the models easier to manipulate in the tests.
+ """
+ old_pk_field = _get_pk_field(model)
+ del old_pk_field.unique
+ new_pk_field = deepcopy(old_pk_field)
+ new_pk_field.primary_key = False
+ schema_editor.alter_field(
+ model=model, old_field=old_pk_field, new_field=new_pk_field
+ )
+
+ autofield = models.AutoField(primary_key=True)
+ autofield.set_attributes_from_name("testid")
+ schema_editor.add_field(model=model, field=autofield)
+
+
+def _create_db_table_for_model(model, schema_editor):
+ """
+ Use the schema editor API to create the db table for the given (unmanaged) model.
+ """
+ schema_editor.create_model(model)
+ if model._meta.model_name in _MODELS_WITH_FAKE_COMPOSITE_PK:
+ _replace_primary_key_field_with_autofield(model, schema_editor)
def create_db_tables_for_unmanaged_models(schema_editor):
@@ -10,7 +65,7 @@ def create_db_tables_for_unmanaged_models(schema_editor):
for model in appconfig.get_models():
if model._meta.managed:
continue
- schema_editor.create_model(model)
+ _create_db_table_for_model(model, schema_editor)
def destroy_db_tables_for_unmanaged_models(schema_editor):