Coverage for api/utils/moderation.py: 13%
48 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 06:14 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 06:14 +0000
1from typing import Literal
3import structlog
5from api.constants.moderation import DecisionAction
6from api.models.audio import (
7 Audio,
8 AudioDecision,
9 AudioDecisionThrough,
10 DeletedAudio,
11 SensitiveAudio,
12)
13from api.models.image import (
14 DeletedImage,
15 Image,
16 ImageDecision,
17 ImageDecisionThrough,
18 SensitiveImage,
19)
20from api.models.media import AbstractDeletedMedia, AbstractMedia, AbstractSensitiveMedia
23logger = structlog.get_logger(__name__)
26def perform_moderation(
27 request,
28 media_type: Literal["audio", "image"],
29 mod_objects: list[
30 type[AbstractSensitiveMedia] | type[AbstractDeletedMedia] | type[AbstractMedia]
31 ],
32 action: DecisionAction,
33):
34 """
35 Perform bulk moderation on the given models.
37 If the decision action is forward, ``mod_objects`` is a queryset of
38 ``Media`` items. We can get the UUIDs from the ``identifier`` field.
40 If the decision action is reverse, ``mod_objects`` is a queryset of
41 ``SensitiveMedia`` or ``DeletedMedia`` items. We can get the UUIDs
42 from the ``media_obj_id`` field.
44 Note that bulk moderation will not resolve any open reports. It is
45 up to the moderator to manually link open reports with the
46 appropriate decisions and resolve them.
48 :param request: the request used to determine the moderator
49 :param media_type: the type of media being bulk-moderated
50 :param mod_objects: a ``QuerySet`` of media items to bulk-moderate
51 :para action: the action of the bulk moderation decision
52 """
54 match media_type:
55 case "audio":
56 Media = Audio
57 SensitiveMedia = SensitiveAudio
58 DeletedMedia = DeletedAudio
59 MediaDecision = AudioDecision
60 MediaDecisionThrough = AudioDecisionThrough
61 case "image":
62 Media = Image
63 SensitiveMedia = SensitiveImage
64 DeletedMedia = DeletedImage
65 MediaDecision = ImageDecision
66 MediaDecisionThrough = ImageDecisionThrough
68 # The following block uses ``list`` to force evaluation of the
69 # queryset and store the values because calling ``delete`` below
70 # will otherwise make the querysets evaluate differently.
71 if action.is_reverse:
72 identifiers = list(mod_objects.values_list("media_obj_id", flat=True))
73 else:
74 identifiers = list(mod_objects.values_list("identifier", flat=True))
76 logger.info(
77 "Performing bulk moderation action.",
78 action=action,
79 model=mod_objects.model._meta.label,
80 identifiers=identifiers,
81 )
83 match action:
84 case DecisionAction.MARKED_SENSITIVE:
85 created = SensitiveMedia.objects.bulk_create(
86 [SensitiveMedia(media_obj_id=identifier) for identifier in identifiers]
87 )
88 logger.debug(f"Created sensitive-{media_type} items.", count=len(created))
89 SensitiveMedia.bulk_perform_action(True, mod_objects)
91 case DecisionAction.DEINDEXED_COPYRIGHT | DecisionAction.DEINDEXED_SENSITIVE:
92 created = DeletedMedia.objects.bulk_create(
93 [DeletedMedia(media_obj_id=identifier) for identifier in identifiers]
94 )
95 logger.debug(f"Created deleted-{media_type} items.", count=len(created))
96 DeletedMedia.bulk_perform_action(mod_objects)
98 case DecisionAction.REVERSED_MARK_SENSITIVE:
99 media_items = Media.objects.filter(identifier__in=identifiers)
100 SensitiveMedia.bulk_perform_action(False, media_items)
101 logger.debug(
102 f"Unmarked {media_type} items as sensitive.",
103 identifier_count=len(identifiers),
104 media_item_count=len(media_items),
105 )
107 count, _ = mod_objects.delete()
108 logger.debug(f"Deleted sensitive-{media_type} items.", count=count)
110 case DecisionAction.REVERSED_DEINDEX:
111 # There is no bulk action for reversed-deindex. The media
112 # item will eventually be reindexed through data refresh.
113 count, _ = mod_objects.delete()
114 logger.debug(f"Deleted deleted-{media_type} items.", count=count)
116 media_decision = MediaDecision.objects.create(
117 action=action,
118 moderator=request.user,
119 notes=request.POST.get("notes"),
120 )
121 logger.debug(
122 f"{media_type}-decision_created",
123 decision=media_decision.id,
124 action=media_decision.action,
125 notes=media_decision.notes,
126 moderator=media_decision.moderator.get_username(),
127 )
129 created = MediaDecisionThrough.objects.bulk_create(
130 [
131 MediaDecisionThrough(decision_id=media_decision.id, media_obj_id=identifier)
132 for identifier in identifiers
133 ]
134 )
135 logger.debug(f"{media_type}-decision-through_bulk_created", count=len(created))
137 return media_decision