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

1from typing import Literal 

2 

3import structlog 

4 

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 

21 

22 

23logger = structlog.get_logger(__name__) 

24 

25 

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. 

36 

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. 

39 

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. 

43 

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. 

47 

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 """ 

53 

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 

67 

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)) 

75 

76 logger.info( 

77 "Performing bulk moderation action.", 

78 action=action, 

79 model=mod_objects.model._meta.label, 

80 identifiers=identifiers, 

81 ) 

82 

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) 

90 

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) 

97 

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 ) 

106 

107 count, _ = mod_objects.delete() 

108 logger.debug(f"Deleted sensitive-{media_type} items.", count=count) 

109 

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) 

115 

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 ) 

128 

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)) 

136 

137 return media_decision