From 55a88c5deb300b824fea2260958e30dbdcef9524 Mon Sep 17 00:00:00 2001 From: Madison Swain-Bowden Date: Fri, 10 May 2024 12:59:13 -0700 Subject: [PATCH] Use explicit through table for media/decision many-to-many field --- .../0062_decision_through_tables.py | 54 +++++++++++++++++++ api/api/models/audio.py | 20 ++++++- api/api/models/image.py | 20 ++++++- api/api/models/media.py | 25 ++++++++- api/latest_migrations/api | 2 +- documentation/meta/media_properties/api.md | 52 ++++++++++++------ 6 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 api/api/migrations/0062_decision_through_tables.py diff --git a/api/api/migrations/0062_decision_through_tables.py b/api/api/migrations/0062_decision_through_tables.py new file mode 100644 index 00000000000..6faedd0c552 --- /dev/null +++ b/api/api/migrations/0062_decision_through_tables.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.11 on 2024-05-10 19:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0061_convert_varchar_to_text'), + ] + + operations = [ + migrations.CreateModel( + name='ImageDecisionThrough', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('decision', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.imagedecision')), + ('media_obj', models.ForeignKey(db_column='identifier', on_delete=django.db.models.deletion.CASCADE, to='api.image', to_field='identifier')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AudioDecisionThrough', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('decision', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.audiodecision')), + ('media_obj', models.ForeignKey(db_column='identifier', on_delete=django.db.models.deletion.CASCADE, to='api.audio', to_field='identifier')), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='audiodecision', + name='media_objs', + ), + migrations.AddField( + model_name='audiodecision', + name='media_objs', + field=models.ManyToManyField(help_text='The audio items being moderated.', through='api.AudioDecisionThrough', to='api.audio'), + ), + migrations.RemoveField( + model_name='imagedecision', + name='media_objs', + ), + migrations.AddField( + model_name='imagedecision', + name='media_objs', + field=models.ManyToManyField(help_text='The image items being moderated.', through='api.ImageDecisionThrough', to='api.image'), + ), + ] diff --git a/api/api/models/audio.py b/api/api/models/audio.py index 0ae06fe9d5e..d945e373473 100644 --- a/api/api/models/audio.py +++ b/api/api/models/audio.py @@ -13,6 +13,7 @@ AbstractDeletedMedia, AbstractMedia, AbstractMediaDecision, + AbstractMediaDecisionThrough, AbstractMediaList, AbstractMediaReport, AbstractSensitiveMedia, @@ -342,11 +343,28 @@ class AudioDecision(AbstractMediaDecision): media_objs = models.ManyToManyField( to="Audio", - db_constraint=False, + through="AudioDecisionThrough", help_text="The audio items being moderated.", ) +class AudioDecisionThrough(AbstractMediaDecisionThrough): + """ + Many-to-many reference table for audio decisions. + + This is made explicit (rather than using Django's default) so that the audio can + be referenced by `identifier` rather than an arbitrary `id`. + """ + + media_obj = models.ForeignKey( + Audio, + to_field="identifier", + on_delete=models.CASCADE, + db_column="identifier", + ) + decision = models.ForeignKey(AudioDecision, on_delete=models.CASCADE) + + class AudioList(AbstractMediaList): """A list of audio files. Currently unused.""" diff --git a/api/api/models/image.py b/api/api/models/image.py index 4faef17d0dd..1e9fc3a8e77 100644 --- a/api/api/models/image.py +++ b/api/api/models/image.py @@ -8,6 +8,7 @@ AbstractDeletedMedia, AbstractMedia, AbstractMediaDecision, + AbstractMediaDecisionThrough, AbstractMediaList, AbstractMediaReport, AbstractSensitiveMedia, @@ -145,11 +146,28 @@ class ImageDecision(AbstractMediaDecision): media_objs = models.ManyToManyField( to="Image", - db_constraint=False, + through="ImageDecisionThrough", help_text="The image items being moderated.", ) +class ImageDecisionThrough(AbstractMediaDecisionThrough): + """ + Many-to-many reference table for image decisions. + + This is made explicit (rather than using Django's default) so that the image can + be referenced by `identifier` rather than an arbitrary `id`. + """ + + media_obj = models.ForeignKey( + Image, + to_field="identifier", + on_delete=models.CASCADE, + db_column="identifier", + ) + decision = models.ForeignKey(ImageDecision, on_delete=models.CASCADE) + + class ImageList(AbstractMediaList): """A list of images. Currently unused.""" diff --git a/api/api/models/media.py b/api/api/models/media.py index 7ff10d8665a..dbf8978c687 100644 --- a/api/api/models/media.py +++ b/api/api/models/media.py @@ -305,7 +305,7 @@ class AbstractMediaDecision(OpenLedgerModel): media_objs = models.ManyToManyField( to="AbstractMedia", - db_constraint=False, + through="AbstractMediaDecisionThrough", help_text="The media items being moderated.", ) """ @@ -332,6 +332,29 @@ class Meta: # TODO: Implement ``clean`` and ``save``, if needed. +class AbstractMediaDecisionThrough(models.Model): + """ + Generic model for the many-to-many reference table between media and decisions. + + This is made explicit (rather than using Django's default) so that the media can + be referenced by `identifier` rather than an arbitrary `id`. + """ + + media_class: type[models.Model] = None + """the model class associated with this media type e.g. ``Image`` or ``Audio``""" + + media_obj = models.ForeignKey( + AbstractMedia, + to_field="identifier", + on_delete=models.CASCADE, + db_column="identifier", + ) + decision = models.ForeignKey(AbstractMediaDecision, on_delete=models.CASCADE) + + class Meta: + abstract = True + + class PerformIndexUpdateMixin: @property def indexes(self): diff --git a/api/latest_migrations/api b/api/latest_migrations/api index 56319b5792c..426a19e0915 100644 --- a/api/latest_migrations/api +++ b/api/latest_migrations/api @@ -2,4 +2,4 @@ # If you have a merge conflict in this file, it means you need to run: # manage.py makemigrations --merge # in order to resolve the conflict between migrations. -0061_convert_varchar_to_text +0062_decision_through_tables diff --git a/documentation/meta/media_properties/api.md b/documentation/meta/media_properties/api.md index 3a82cac43af..1f18c207eab 100644 --- a/documentation/meta/media_properties/api.md +++ b/documentation/meta/media_properties/api.md @@ -22,14 +22,15 @@ value). Note that relation fields are always nullable. ### Relations -| Name | Type | DB type | Nature | To | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------- | ------------ | ---------------- | -| [`audio_report`](#Audio-audio_report-notes) | [`ForeignKey`](https://docs.djangoproject.com/en/stable/ref/models/fields/#foreignkey) | `uuid` | One To Many | `AudioReport` | -| [`audiodecision`](#Audio-audiodecision-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `AudioDecision` | -| [`audioset`](#Audio-audioset-notes) | `ForeignObject` | | Many To One | `AudioSet` | -| [`deleted_audio`](#Audio-deleted_audio-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `DeletedAudio` | -| [`lists`](#Audio-lists-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `AudioList` | -| [`sensitive_audio`](#Audio-sensitive_audio-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `SensitiveAudio` | +| Name | Type | DB type | Nature | To | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------- | ------------ | ---------------------- | +| [`audio_report`](#Audio-audio_report-notes) | [`ForeignKey`](https://docs.djangoproject.com/en/stable/ref/models/fields/#foreignkey) | `uuid` | One To Many | `AudioReport` | +| [`audiodecision`](#Audio-audiodecision-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `AudioDecision` | +| [`audiodecisionthrough`](#Audio-audiodecisionthrough-notes) | [`ForeignKey`](https://docs.djangoproject.com/en/stable/ref/models/fields/#foreignkey) | `uuid` | One To Many | `AudioDecisionThrough` | +| [`audioset`](#Audio-audioset-notes) | `ForeignObject` | | Many To One | `AudioSet` | +| [`deleted_audio`](#Audio-deleted_audio-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `DeletedAudio` | +| [`lists`](#Audio-lists-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `AudioList` | +| [`sensitive_audio`](#Audio-sensitive_audio-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `SensitiveAudio` | ### Values @@ -109,6 +110,16 @@ report. **`AudioDecision` docstring:** Moderation decisions taken for audio tracks. +(Audio-audiodecisionthrough-notes)= + +#### `audiodecisionthrough` + +**`AudioDecisionThrough` docstring:** Many-to-many reference table for audio +decisions. + +This is made explicit (rather than using Django's default) so that the audio can +be referenced by `identifier` rather than an arbitrary `id`. + (Audio-audioset-notes)= #### `audioset` @@ -323,13 +334,14 @@ Note that only `name` and `accuracy` are presently surfaced in API results. ### Relations -| Name | Type | DB type | Nature | To | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------- | ------------ | ---------------- | -| [`deleted_image`](#Image-deleted_image-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `DeletedImage` | -| [`image_report`](#Image-image_report-notes) | [`ForeignKey`](https://docs.djangoproject.com/en/stable/ref/models/fields/#foreignkey) | `uuid` | One To Many | `ImageReport` | -| [`imagedecision`](#Image-imagedecision-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `ImageDecision` | -| [`lists`](#Image-lists-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `ImageList` | -| [`sensitive_image`](#Image-sensitive_image-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `SensitiveImage` | +| Name | Type | DB type | Nature | To | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------- | ------------ | ---------------------- | +| [`deleted_image`](#Image-deleted_image-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `DeletedImage` | +| [`image_report`](#Image-image_report-notes) | [`ForeignKey`](https://docs.djangoproject.com/en/stable/ref/models/fields/#foreignkey) | `uuid` | One To Many | `ImageReport` | +| [`imagedecision`](#Image-imagedecision-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `ImageDecision` | +| [`imagedecisionthrough`](#Image-imagedecisionthrough-notes) | [`ForeignKey`](https://docs.djangoproject.com/en/stable/ref/models/fields/#foreignkey) | `uuid` | One To Many | `ImageDecisionThrough` | +| [`lists`](#Image-lists-notes) | [`ManyToManyField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#manytomanyfield) | | Many To Many | `ImageList` | +| [`sensitive_image`](#Image-sensitive_image-notes) | [`OneToOneField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#onetoonefield) | `uuid` | One To One | `SensitiveImage` | ### Values @@ -449,6 +461,16 @@ this report. **`ImageDecision` docstring:** Moderation decisions taken for images. +(Image-imagedecisionthrough-notes)= + +#### `imagedecisionthrough` + +**`ImageDecisionThrough` docstring:** Many-to-many reference table for image +decisions. + +This is made explicit (rather than using Django's default) so that the image can +be referenced by `identifier` rather than an arbitrary `id`. + (Image-last_synced_with_source-notes)= #### `last_synced_with_source`