From 77dc679eac9493ef4f3755b91ead06b565772330 Mon Sep 17 00:00:00 2001 From: Olga Bulat Date: Mon, 11 Dec 2023 13:40:50 +0300 Subject: [PATCH] Add new SensitiveMedia and MediaReport models Signed-off-by: Olga Bulat --- api/api/admin/__init__.py | 4 +- ..._rename_imagereport_nsfwreport_and_more.py | 21 ++ ..._sensitiveaudio_sensitiveimage_and_more.py | 271 ++++++++++++++++++ api/api/models/__init__.py | 12 +- api/api/models/audio.py | 52 +++- api/api/models/image.py | 49 +++- api/api/models/media.py | 80 +++++- api/api/serializers/media_serializers.py | 13 +- api/api/views/media_views.py | 3 +- api/test/factory/models/__init__.py | 4 +- api/test/factory/models/audio.py | 8 +- api/test/factory/models/image.py | 8 +- api/test/factory/models/media.py | 18 +- api/test/unit/conftest.py | 20 +- api/test/unit/models/test_media_report.py | 25 +- .../serializers/test_media_serializers.py | 8 + 16 files changed, 539 insertions(+), 57 deletions(-) create mode 100644 api/api/migrations/0055_rename_imagereport_nsfwreport_and_more.py create mode 100644 api/api/migrations/0056_sensitiveaudio_sensitiveimage_and_more.py diff --git a/api/api/admin/__init__.py b/api/api/admin/__init__.py index b689be8cd42..802a813752a 100644 --- a/api/api/admin/__init__.py +++ b/api/api/admin/__init__.py @@ -2,7 +2,7 @@ from api.admin.site import openverse_admin from api.models import PENDING, Audio, AudioReport, ContentProvider, Image, ImageReport -from api.models.media import AbstractDeletedMedia, AbstractMatureMedia +from api.models.media import AbstractDeletedMedia, AbstractSensitiveMedia admin.site = openverse_admin @@ -72,7 +72,7 @@ def has_add_permission(self, *args, **kwargs): for klass in [ - *AbstractMatureMedia.__subclasses__(), + *AbstractSensitiveMedia.__subclasses__(), *AbstractDeletedMedia.__subclasses__(), ]: admin.site.register(klass, MediaSubreportAdmin) diff --git a/api/api/migrations/0055_rename_imagereport_nsfwreport_and_more.py b/api/api/migrations/0055_rename_imagereport_nsfwreport_and_more.py new file mode 100644 index 00000000000..05acf8bcd92 --- /dev/null +++ b/api/api/migrations/0055_rename_imagereport_nsfwreport_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.7 on 2023-12-11 04:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0054_throttledapplication_post_logout_redirect_uris"), + ] + + operations = [ + migrations.RenameModel( + old_name="ImageReport", + new_name="NsfwReport", + ), + migrations.RenameModel( + old_name="AudioReport", + new_name="NsfwReportAudio", + ), + ] diff --git a/api/api/migrations/0056_sensitiveaudio_sensitiveimage_and_more.py b/api/api/migrations/0056_sensitiveaudio_sensitiveimage_and_more.py new file mode 100644 index 00000000000..88e16cf8e54 --- /dev/null +++ b/api/api/migrations/0056_sensitiveaudio_sensitiveimage_and_more.py @@ -0,0 +1,271 @@ +# Generated by Django 4.2.7 on 2023-12-11 06:32 + +import api.models.media +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0055_rename_imagereport_nsfwreport_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="SensitiveAudio", + fields=[ + ("created_on", models.DateTimeField(auto_now_add=True)), + ( + "media_obj", + models.OneToOneField( + db_column="identifier", + db_constraint=False, + help_text="The reference to the sensitive audio.", + on_delete=django.db.models.deletion.DO_NOTHING, + primary_key=True, + related_name="sensitive_audio", + serialize=False, + to="api.audio", + to_field="identifier", + ), + ), + ], + options={ + "verbose_name_plural": "Sensitive audio", + }, + bases=(api.models.media.PerformIndexUpdateMixin, models.Model), + ), + migrations.CreateModel( + name="SensitiveImage", + fields=[ + ("created_on", models.DateTimeField(auto_now_add=True)), + ( + "media_obj", + models.OneToOneField( + db_column="identifier", + db_constraint=False, + help_text="The reference to the sensitive image.", + on_delete=django.db.models.deletion.DO_NOTHING, + primary_key=True, + related_name="sensitive_image", + serialize=False, + to="api.image", + to_field="identifier", + ), + ), + ], + options={ + "abstract": False, + }, + bases=(api.models.media.PerformIndexUpdateMixin, models.Model), + ), + migrations.AlterField( + model_name="nsfwreport", + name="media_obj", + field=models.ForeignKey( + db_column="identifier", + db_constraint=False, + help_text="The reference to the image being reported.", + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="nsfw_image_report", + to="api.image", + to_field="identifier", + ), + ), + migrations.AlterField( + model_name="nsfwreport", + name="reason", + field=models.CharField( + choices=[ + ("sensitive", "sensitive"), + ("dmca", "dmca"), + ("other", "other"), + ], + help_text="The reason to report media to Openverse.", + max_length=20, + ), + ), + migrations.AlterField( + model_name="nsfwreport", + name="status", + field=models.CharField( + choices=[ + ("pending_review", "pending_review"), + ("sensitive_filtered", "sensitive_filtered"), + ("deindexed", "deindexed"), + ("no_action", "no_action"), + ], + default="pending_review", + max_length=20, + ), + ), + migrations.AlterField( + model_name="nsfwreportaudio", + name="media_obj", + field=models.ForeignKey( + db_column="identifier", + db_constraint=False, + help_text="The reference to the audio being reported.", + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="nsfw_audio_report", + to="api.audio", + to_field="identifier", + ), + ), + migrations.AlterField( + model_name="nsfwreportaudio", + name="reason", + field=models.CharField( + choices=[ + ("sensitive", "sensitive"), + ("dmca", "dmca"), + ("other", "other"), + ], + help_text="The reason to report media to Openverse.", + max_length=20, + ), + ), + migrations.AlterField( + model_name="nsfwreportaudio", + name="status", + field=models.CharField( + choices=[ + ("pending_review", "pending_review"), + ("sensitive_filtered", "sensitive_filtered"), + ("deindexed", "deindexed"), + ("no_action", "no_action"), + ], + default="pending_review", + max_length=20, + ), + ), + migrations.CreateModel( + name="ImageReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "reason", + models.CharField( + choices=[ + ("sensitive", "sensitive"), + ("dmca", "dmca"), + ("other", "other"), + ], + help_text="The reason to report media to Openverse.", + max_length=20, + ), + ), + ( + "description", + models.TextField( + blank=True, + help_text="The explanation on why media is being reported.", + max_length=500, + null=True, + ), + ), + ( + "status", + models.CharField( + choices=[ + ("pending_review", "pending_review"), + ("sensitive_filtered", "sensitive_filtered"), + ("deindexed", "deindexed"), + ("no_action", "no_action"), + ], + default="pending_review", + max_length=20, + ), + ), + ( + "media_obj", + models.ForeignKey( + db_column="identifier", + db_constraint=False, + help_text="The reference to the image being reported.", + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="image_report", + to="api.image", + to_field="identifier", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="AudioReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "reason", + models.CharField( + choices=[ + ("sensitive", "sensitive"), + ("dmca", "dmca"), + ("other", "other"), + ], + help_text="The reason to report media to Openverse.", + max_length=20, + ), + ), + ( + "description", + models.TextField( + blank=True, + help_text="The explanation on why media is being reported.", + max_length=500, + null=True, + ), + ), + ( + "status", + models.CharField( + choices=[ + ("pending_review", "pending_review"), + ("sensitive_filtered", "sensitive_filtered"), + ("deindexed", "deindexed"), + ("no_action", "no_action"), + ], + default="pending_review", + max_length=20, + ), + ), + ( + "media_obj", + models.ForeignKey( + db_column="identifier", + db_constraint=False, + help_text="The reference to the audio being reported.", + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="audio_report", + to="api.audio", + to_field="identifier", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/api/api/models/__init__.py b/api/api/models/__init__.py index c34e863d798..7f7aceaa435 100644 --- a/api/api/models/__init__.py +++ b/api/api/models/__init__.py @@ -7,8 +7,18 @@ AudioSet, DeletedAudio, MatureAudio, + NsfwReportAudio, + SensitiveAudio, +) +from api.models.image import ( + DeletedImage, + Image, + ImageList, + ImageReport, + MatureImage, + NsfwReport, + SensitiveImage, ) -from api.models.image import DeletedImage, Image, ImageList, ImageReport, MatureImage from api.models.media import ( DEINDEXED, DMCA, diff --git a/api/api/models/audio.py b/api/api/models/audio.py index 377fc8d716f..ae8166f5357 100644 --- a/api/api/models/audio.py +++ b/api/api/models/audio.py @@ -13,6 +13,7 @@ AbstractMedia, AbstractMediaList, AbstractMediaReport, + AbstractSensitiveMedia, ) from api.models.mixins import FileMixin, ForeignIdentifierMixin, MediaMixin from api.utils.waveform import generate_peaks @@ -190,7 +191,8 @@ class Audio(AudioFileMixin, AbstractMedia): @property def mature(self) -> bool: - return hasattr(self, "mature_audio") + # TODO: Remove ``mature_audio`` check after db migration. + return hasattr(self, "mature_audio") or hasattr(self, "sensitive_audio") @property def alternative_files(self): @@ -260,6 +262,32 @@ class Meta: verbose_name_plural = "Deleted audio" +class SensitiveAudio(AbstractSensitiveMedia): + """ + Stores all audio tracks that have been flagged as 'sensitive'. + + Do not create instances of this model manually. Create an ``AudioReport`` instance + instead. + """ + + media_class = Audio + es_index = settings.MEDIA_INDEX_MAPPING[AUDIO_TYPE] + + media_obj = models.OneToOneField( + to="Audio", + to_field="identifier", + on_delete=models.DO_NOTHING, + primary_key=True, + db_constraint=False, + db_column="identifier", + related_name="sensitive_audio", + help_text="The reference to the sensitive audio.", + ) + + class Meta: + verbose_name_plural = "Sensitive audio" + + class MatureAudio(AbstractMatureMedia): """ Stores all audio tracks that have been flagged as 'mature'. @@ -288,7 +316,7 @@ class Meta: class AudioReport(AbstractMediaReport): media_class = Audio - mature_class = MatureAudio + sensitive_class = SensitiveAudio deleted_class = DeletedAudio media_obj = models.ForeignKey( @@ -301,6 +329,26 @@ class AudioReport(AbstractMediaReport): help_text="The reference to the audio being reported.", ) + @property + def audio_url(self): + return super().url("audio") + + +class NsfwReportAudio(AbstractMediaReport): + media_class = Audio + sensitive_class = MatureAudio + deleted_class = DeletedAudio + + media_obj = models.ForeignKey( + to="Audio", + to_field="identifier", + on_delete=models.DO_NOTHING, + db_constraint=False, + db_column="identifier", + related_name="nsfw_audio_report", + help_text="The reference to the audio being reported.", + ) + class Meta: db_table = "nsfw_reports_audio" diff --git a/api/api/models/image.py b/api/api/models/image.py index bc4fffb0755..ac427013d3f 100644 --- a/api/api/models/image.py +++ b/api/api/models/image.py @@ -10,6 +10,7 @@ AbstractMedia, AbstractMediaList, AbstractMediaReport, + AbstractSensitiveMedia, ) from api.models.mixins import FileMixin @@ -54,7 +55,8 @@ class Meta(AbstractMedia.Meta): @property def mature(self) -> bool: - return hasattr(self, "mature_image") + # TODO: Remove ``mature_image`` check after db migration. + return hasattr(self, "mature_image") or hasattr(self, "sensitive_image") class DeletedImage(AbstractDeletedMedia): @@ -80,6 +82,29 @@ class DeletedImage(AbstractDeletedMedia): ) +class SensitiveImage(AbstractSensitiveMedia): + """ + Stores all images that have been flagged as 'sensitive'. + + Do not create instances of this model manually. Create an ``ImageReport`` instance + instead. + """ + + media_class = Image + es_index = settings.MEDIA_INDEX_MAPPING[IMAGE_TYPE] + + media_obj = models.OneToOneField( + to="Image", + to_field="identifier", + on_delete=models.DO_NOTHING, + primary_key=True, + db_constraint=False, + db_column="identifier", + related_name="sensitive_image", + help_text="The reference to the sensitive image.", + ) + + class MatureImage(AbstractMatureMedia): """ Stores all images that have been flagged as 'mature'. @@ -105,7 +130,7 @@ class MatureImage(AbstractMatureMedia): class ImageReport(AbstractMediaReport): media_class = Image - mature_class = MatureImage + sensitive_class = SensitiveImage deleted_class = DeletedImage media_obj = models.ForeignKey( @@ -118,6 +143,26 @@ class ImageReport(AbstractMediaReport): help_text="The reference to the image being reported.", ) + @property + def image_url(self): + return super().url("images") + + +class NsfwReport(AbstractMediaReport): + media_class = Image + sensitive_class = MatureImage + deleted_class = DeletedImage + + media_obj = models.ForeignKey( + to="Image", + to_field="identifier", + on_delete=models.DO_NOTHING, + db_constraint=False, + db_column="identifier", + related_name="nsfw_image_report", + help_text="The reference to the image being reported.", + ) + class Meta: db_table = "nsfw_reports" diff --git a/api/api/models/media.py b/api/api/models/media.py index 187e0c6817f..e493e920bef 100644 --- a/api/api/models/media.py +++ b/api/api/models/media.py @@ -16,10 +16,12 @@ PENDING = "pending_review" MATURE_FILTERED = "mature_filtered" +SENSITIVE_FILTERED = "sensitive_filtered" DEINDEXED = "deindexed" NO_ACTION = "no_action" MATURE = "mature" +SENSITIVE = "sensitive" DMCA = "dmca" OTHER = "other" @@ -133,25 +135,29 @@ class AbstractMediaReport(models.Model): """ Generic model from which to inherit all reported media classes. - 'Reported' here refers to content reports such as mature, copyright-violating or - deleted content. Subclasses must populate ``media_class``, ``mature_class`` and + 'Reported' here refers to content reports such as sensitive, copyright-violating or + deleted content. Subclasses must populate ``media_class``, ``sensitive_class`` and ``deleted_class`` fields. """ media_class: type[models.Model] = None """the model class associated with this media type e.g. ``Image`` or ``Audio``""" - mature_class: type[models.Model] = None - """the class storing mature media e.g. ``MatureImage`` or ``MatureAudio``""" + sensitive_class: type[models.Model] = None + """the class storing sensitive media e.g. ``SensitiveImage`` or ``MatureAudio``""" deleted_class: type[models.Model] = None """the class storing deleted media e.g. ``DeletedImage`` or ``DeletedAudio``""" BASE_URL = settings.BASE_URL - REPORT_CHOICES = [(MATURE, MATURE), (DMCA, DMCA), (OTHER, OTHER)] + REPORT_CHOICES = [ + (SENSITIVE, SENSITIVE), + (DMCA, DMCA), + (OTHER, OTHER), + ] STATUS_CHOICES = [ (PENDING, PENDING), - (MATURE_FILTERED, MATURE_FILTERED), + (SENSITIVE_FILTERED, SENSITIVE_FILTERED), (DEINDEXED, DEINDEXED), (NO_ACTION, NO_ACTION), ] @@ -213,18 +219,18 @@ def save(self, *args, **kwargs): Extend the built-in ``save()`` functionality of Django with Elasticsearch integration to update records and refresh indices. - Media marked as mature or deleted also leads to instantiation of their - corresponding mature or deleted classes. + Media marked as sensitive or deleted also leads to instantiation of their + corresponding sensitive or deleted classes. """ self.clean() super().save(*args, **kwargs) - if self.status == MATURE_FILTERED: + if self.status == SENSITIVE_FILTERED: # Create an instance of the mature class for this media. This will # automatically set the ``mature`` field in the ES document. - self.mature_class.objects.create(media_obj=self.media_obj) + self.sensitive_class.objects.create(media_obj=self.media_obj) elif self.status == DEINDEXED: # Create an instance of the deleted class for this media, so that we don't # reindex it later. This will automatically delete the ES document and the @@ -383,6 +389,60 @@ def delete(self, *args, **kwargs): super().delete(*args, **kwargs) +class AbstractSensitiveMedia(PerformIndexUpdateMixin, models.Model): + """ + Generic model from which to inherit all sensitive media classes. + + Subclasses must populate ``media_class`` and ``es_index`` fields. + """ + + media_class: type[models.Model] = None + """the model class associated with this media type e.g. ``Image`` or ``Audio``""" + es_index: str = None + """the name of the ES index from ``settings.MEDIA_INDEX_MAPPING``""" + + created_on = models.DateTimeField(auto_now_add=True) + + media_obj = models.OneToOneField( + to="AbstractMedia", + to_field="identifier", + on_delete=models.DO_NOTHING, + primary_key=True, + db_constraint=False, + db_column="identifier", + related_name="sensitive_abstract_media", + help_text="The reference to the sensitive media.", + ) + """ + Sub-classes must override this field to point to a concrete sub-class of + ``AbstractMedia``. + """ + + class Meta: + abstract = True + + def _update_es(self, is_mature: bool, raise_errors: bool): + """ + Update the Elasticsearch document associated with the given model. + + :param is_mature: whether to mark the media item as mature + :param raise_errors: whether to raise an error if the no media item is found + """ + self._perform_index_update( + "update", + raise_errors, + doc={"mature": is_mature}, + ) + + def save(self, *args, **kwargs): + self._update_es(True, True) + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + self._update_es(False, False) + super().delete(*args, **kwargs) + + class AbstractMediaList(OpenLedgerModel): """ Generic model from which to inherit media lists. diff --git a/api/api/serializers/media_serializers.py b/api/api/serializers/media_serializers.py index d8a52d4c1f8..14207919a80 100644 --- a/api/api/serializers/media_serializers.py +++ b/api/api/serializers/media_serializers.py @@ -13,7 +13,7 @@ from api.constants.licenses import LICENSE_GROUPS from api.constants.sorting import DESCENDING, RELEVANCE, SORT_DIRECTIONS, SORT_FIELDS from api.controllers import search_controller -from api.models.media import AbstractMedia +from api.models.media import MATURE_FILTERED, SENSITIVE_FILTERED, AbstractMedia from api.serializers.base import BaseModelSerializer from api.serializers.fields import SchemableHyperlinkedIdentityField from api.utils.help_text import make_comma_separated_help_text @@ -396,6 +396,7 @@ def to_internal_value(self, data): """ data["reason"] = self._map_reason(data.get("reason")) + data["status"] = self._map_status(data.get("status")) return super().to_internal_value(data) def validate(self, attrs): @@ -409,7 +410,15 @@ def validate(self, attrs): return attrs - def _map_reason(self, value): + @staticmethod + def _map_status(value): + """Map `mature_filtered` to `sensitive_filtered` for compatibility.""" + if value == MATURE_FILTERED: + return SENSITIVE_FILTERED + return value + + @staticmethod + def _map_reason(value): """ Map `sensitive` to `mature` for forwards compatibility. diff --git a/api/api/views/media_views.py b/api/api/views/media_views.py index d228382ee37..42ed5006696 100644 --- a/api/api/views/media_views.py +++ b/api/api/views/media_views.py @@ -10,6 +10,7 @@ from adrf.views import APIView as AsyncAPIView from adrf.viewsets import ViewSetMixin as AsyncViewSetMixin from asgiref.sync import sync_to_async +from elasticsearch_dsl.response import Hit from api.constants.media_types import MediaType from api.constants.search import SearchStrategy @@ -101,7 +102,7 @@ def _get_request_serializer(self, request): req_serializer.is_valid(raise_exception=True) return req_serializer - def get_db_results(self, results): + def get_db_results(self, results: list[Hit]) -> list[AbstractMedia]: """ Map ES hits to ORM model instances. diff --git a/api/test/factory/models/__init__.py b/api/test/factory/models/__init__.py index 628c8d484eb..7560ee6cafb 100644 --- a/api/test/factory/models/__init__.py +++ b/api/test/factory/models/__init__.py @@ -2,12 +2,12 @@ AudioAddOnFactory, AudioFactory, AudioReportFactory, - MatureAudioFactory, + SensitiveAudioFactory, ) from test.factory.models.image import ( ImageFactory, ImageReportFactory, - MatureImageFactory, + SensitiveImageFactory, ) from test.factory.models.oauth2 import ( AccessTokenFactory, diff --git a/api/test/factory/models/audio.py b/api/test/factory/models/audio.py index 9735b2c2c1a..e8347b92f84 100644 --- a/api/test/factory/models/audio.py +++ b/api/test/factory/models/audio.py @@ -4,18 +4,18 @@ import factory from factory.django import DjangoModelFactory -from api.models.audio import Audio, AudioAddOn, AudioReport, MatureAudio +from api.models.audio import Audio, AudioAddOn, AudioReport, SensitiveAudio -class MatureAudioFactory(DjangoModelFactory): +class SensitiveAudioFactory(DjangoModelFactory): class Meta: - model = MatureAudio + model = SensitiveAudio media_obj = factory.SubFactory("test.factory.models.audio.AudioFactory") class AudioFactory(MediaFactory): - _mature_factory = MatureAudioFactory + _sensitive_factory = SensitiveAudioFactory class Meta: model = Audio diff --git a/api/test/factory/models/image.py b/api/test/factory/models/image.py index dc034ed3db0..9b68be75e31 100644 --- a/api/test/factory/models/image.py +++ b/api/test/factory/models/image.py @@ -3,18 +3,18 @@ import factory from factory.django import DjangoModelFactory -from api.models.image import Image, ImageReport, MatureImage +from api.models.image import Image, ImageReport, SensitiveImage -class MatureImageFactory(DjangoModelFactory): +class SensitiveImageFactory(DjangoModelFactory): class Meta: - model = MatureImage + model = SensitiveImage media_obj = factory.SubFactory("test.factory.models.image.ImageFactory") class ImageFactory(MediaFactory): - _mature_factory = MatureImageFactory + _sensitive_factory = SensitiveImageFactory class Meta: model = Image diff --git a/api/test/factory/models/media.py b/api/test/factory/models/media.py index 2df54763b8d..1cf84c01cef 100644 --- a/api/test/factory/models/media.py +++ b/api/test/factory/models/media.py @@ -37,8 +37,8 @@ class MediaFactory(DjangoModelFactory): ) # Sub-factories must set this to their corresponding - # ``AbstractMatureMedia`` subclass - _mature_factory = None + # ``AbstractSensitiveMedia`` subclass + _sensitive_factory = None _highest_pre_existing_pk = None @@ -77,8 +77,8 @@ def create(cls, *args, **kwargs) -> AbstractMedia | tuple[AbstractMedia, Hit]: see the factory-behaviour specific kwargs below. :Keyword Arguments: - * *mature_reported* (``bool``) -- - Create a mature report for this media. + * *sensitive_reported* (``bool``) -- + Create a sensitive report for this media. * *provider_marked_mature* (``bool``) -- Set ``mature=true`` on the Elasticsearch document. * *sensitive_text* (``bool``) -- @@ -90,7 +90,9 @@ def create(cls, *args, **kwargs) -> AbstractMedia | tuple[AbstractMedia, Hit]: Whether to return the Elasticsearch ``Hit`` along with the created media object. """ - mature_reported = kwargs.pop("mature_reported", False) + sensitive_reported = kwargs.pop("sensitive_reported", False) or kwargs.pop( + "mature_reported", False + ) provider_marked_mature = kwargs.pop("provider_marked_mature", False) sensitive_text = kwargs.pop("sensitive_text", False) skip_es = kwargs.pop("skip_es", False) @@ -120,13 +122,13 @@ def create(cls, *args, **kwargs) -> AbstractMedia | tuple[AbstractMedia, Hit]: hit = cls._save_model_to_es( model, add_to_filtered_index=not sensitive_text, - mature=provider_marked_mature or mature_reported, + mature=provider_marked_mature or sensitive_reported, ) else: hit = None - if mature_reported: - cls._mature_factory.create(media_obj=model) + if sensitive_reported: + cls._sensitive_factory.create(media_obj=model) if pook_active: # Reactivate pook if it was active diff --git a/api/test/unit/conftest.py b/api/test/unit/conftest.py index 155daedf17f..9b3afc98b95 100644 --- a/api/test/unit/conftest.py +++ b/api/test/unit/conftest.py @@ -19,10 +19,10 @@ DeletedAudio, DeletedImage, Image, - MatureAudio, - MatureImage, + SensitiveAudio, + SensitiveImage, ) -from api.models.media import AbstractDeletedMedia, AbstractMatureMedia, AbstractMedia +from api.models.media import AbstractDeletedMedia, AbstractMedia, AbstractSensitiveMedia from api.serializers.audio_serializers import ( AudioReportRequestSerializer, AudioSearchRequestSerializer, @@ -81,8 +81,8 @@ class MediaTypeConfig: filtered_index: str model_factory: MediaFactory model_class: AbstractMedia - mature_factory: MediaFactory - mature_class: AbstractMatureMedia + sensitive_factory: MediaFactory + sensitive_class: AbstractSensitiveMedia search_request_serializer: MediaSearchRequestSerializer model_serializer: MediaSerializer report_serializer: MediaReportRequestSerializer @@ -91,7 +91,7 @@ class MediaTypeConfig: @property def indexes(self): - return (self.origin_index, self.filtered_index) + return self.origin_index, self.filtered_index MEDIA_TYPE_CONFIGS = { @@ -102,12 +102,12 @@ def indexes(self): filtered_index="image-filtered", model_factory=model_factories.ImageFactory, model_class=Image, - mature_factory=model_factories.MatureImageFactory, + sensitive_factory=model_factories.SensitiveImageFactory, search_request_serializer=ImageSearchRequestSerializer, model_serializer=ImageSerializer, report_serializer=ImageReportRequestSerializer, report_factory=model_factories.ImageReportFactory, - mature_class=MatureImage, + sensitive_class=SensitiveImage, deleted_class=DeletedImage, ), "audio": MediaTypeConfig( @@ -117,12 +117,12 @@ def indexes(self): filtered_index="audio-filtered", model_factory=model_factories.AudioFactory, model_class=Audio, - mature_factory=model_factories.MatureAudioFactory, + sensitive_factory=model_factories.SensitiveAudioFactory, search_request_serializer=AudioSearchRequestSerializer, model_serializer=AudioSerializer, report_serializer=AudioReportRequestSerializer, report_factory=model_factories.AudioReportFactory, - mature_class=MatureAudio, + sensitive_class=SensitiveAudio, deleted_class=DeletedAudio, ), } diff --git a/api/test/unit/models/test_media_report.py b/api/test/unit/models/test_media_report.py index e936c47d356..8a497fac690 100644 --- a/api/test/unit/models/test_media_report.py +++ b/api/test/unit/models/test_media_report.py @@ -1,3 +1,4 @@ +import logging import uuid from django.core.exceptions import ObjectDoesNotExist @@ -11,9 +12,9 @@ DEINDEXED, DMCA, MATURE, - MATURE_FILTERED, OTHER, PENDING, + SENSITIVE_FILTERED, AbstractDeletedMedia, AbstractMatureMedia, ) @@ -42,18 +43,24 @@ def test_pending_reports_have_no_subreport_models( report = media_type_config.report_factory.create(media_obj=media, reason=reason) assert report.status == PENDING - assert not media_type_config.mature_class.objects.filter(media_obj=media).exists() + assert not media_type_config.sensitive_class.objects.filter( + media_obj=media + ).exists() assert not media_type_config.deleted_class.objects.filter(media_obj=media).exists() -def test_mature_filtering_creates_mature_image_instance(media_type_config, settings): +def test_mature_filtering_creates_sensitive_image_instance(media_type_config, settings): media = media_type_config.model_factory.create() media_type_config.report_factory.create( - media_obj=media, reason=MATURE, status=MATURE_FILTERED + media_obj=media, reason=MATURE, status=SENSITIVE_FILTERED ) - assert media_type_config.mature_class.objects.filter(media_obj=media).exists() + logging.info( + f"media_type_config.sensitive_class: {media_type_config.sensitive_class}, media: {media}, " + f"{media_type_config.sensitive_class.objects.filter(media_obj=media).all()}" + ) + assert media_type_config.sensitive_class.objects.filter(media_obj=media).exists() for index in media_type_config.indexes: doc = settings.ES.get( @@ -74,10 +81,10 @@ def test_deleting_mature_image_instance_resets_mature_flag(media_type_config, se media = media_type_config.model_factory.create() # Mark as mature. media_type_config.report_factory.create( - media_obj=media, reason=MATURE, status=MATURE_FILTERED + media_obj=media, reason=MATURE, status=SENSITIVE_FILTERED ) # Delete mature instance. - media_type_config.mature_class.objects.get(media_obj=media).delete() + media_type_config.sensitive_class.objects.get(media_obj=media).delete() # Assert the media are back to mature=False # The previous test asserts they get set to mature=True @@ -238,7 +245,7 @@ def test_mature_media_ignores_elasticsearch_404_errors( ) # This should pass despite the 404 enforced above - media_type_config.mature_factory.create( + media_type_config.sensitive_factory.create( media_obj=media, ) @@ -262,7 +269,7 @@ def test_mature_media_reraises_elasticsearch_400_errors(settings, media_type_con # This should fail due to the 400 enforced above with pytest.raises(BadRequestError): - media_type_config.mature_factory.create( + media_type_config.sensitive_factory.create( media_obj=media, ) diff --git a/api/test/unit/serializers/test_media_serializers.py b/api/test/unit/serializers/test_media_serializers.py index d48798c40cb..fa7064385c3 100644 --- a/api/test/unit/serializers/test_media_serializers.py +++ b/api/test/unit/serializers/test_media_serializers.py @@ -1,3 +1,4 @@ +import logging import random import uuid from test.factory.models.oauth2 import AccessTokenFactory @@ -119,6 +120,7 @@ def test_media_serializer_sensitivity( mature_reported=has_confirmed_report, with_hit=True, ) + logging.info(f"Created model: {model}, {model.mature}\nhit: {hit}, {hit.mature}") other_result_ids = [str(uuid.uuid4()) for _ in range(6)] context = { @@ -138,6 +140,12 @@ def test_media_serializer_sensitivity( if has_confirmed_report: expected_sensitivity.add(sensitivity.USER_REPORTED) + logging.info(f"Serializer data: {serializer.data}") + logging.info( + f"Expected sensitivity: {expected_sensitivity}\nActual sensitivity: " + f"{serializer.data['unstable__sensitivity']}" + ) + assert set(serializer.data["unstable__sensitivity"]) == expected_sensitivity