Coverage for api/models/image.py: 91%

55 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 06:14 +0000

1from django.conf import settings 

2from django.db import models 

3 

4from uuslug import uuslug 

5 

6from api.constants.media_types import IMAGE_TYPE 

7from api.models.media import ( 

8 AbstractDeletedMedia, 

9 AbstractMedia, 

10 AbstractMediaDecision, 

11 AbstractMediaDecisionThrough, 

12 AbstractMediaList, 

13 AbstractMediaReport, 

14 AbstractSensitiveMedia, 

15) 

16from api.models.mixins import FileMixin 

17 

18 

19class ImageFileMixin(FileMixin): 

20 """ 

21 This mixin adds fields related to image resolution to the standard file mixin. 

22 

23 Do not use this as the sole base class. 

24 """ 

25 

26 width = models.IntegerField( 

27 blank=True, 

28 null=True, 

29 help_text="The width of the image in pixels. Not always available.", 

30 ) 

31 height = models.IntegerField( 

32 blank=True, 

33 null=True, 

34 help_text="The height of the image in pixels. Not always available.", 

35 ) 

36 

37 @property 

38 def resolution_in_mp(self): # ~ MP or megapixels 

39 return (self.width * self.height) / 1e6 

40 

41 class Meta: 

42 abstract = True 

43 

44 

45class Image(ImageFileMixin, AbstractMedia): 

46 """ 

47 One image media instance. 

48 

49 Inherited fields 

50 ================ 

51 category: eg. photograph, digitized_artwork & illustration 

52 """ 

53 

54 class Meta(AbstractMedia.Meta): 

55 db_table = "image" 

56 

57 def get_absolute_url(self): 

58 """Enable the "View on site" link in the Django Admin.""" 

59 

60 from django.urls import reverse 

61 

62 return reverse("image-detail", args=[str(self.identifier)]) 

63 

64 @property 

65 def sensitive(self) -> bool: 

66 return hasattr(self, "sensitive_image") 

67 

68 

69class DeletedImage(AbstractDeletedMedia): 

70 """ 

71 Images deleted from the upstream source. 

72 

73 Do not create instances of this model manually. Create an ``ImageReport`` instance 

74 instead. 

75 """ 

76 

77 media_class = Image 

78 es_index = settings.MEDIA_INDEX_MAPPING[IMAGE_TYPE] 

79 

80 media_obj = models.OneToOneField( 

81 to="Image", 

82 to_field="identifier", 

83 on_delete=models.DO_NOTHING, 

84 primary_key=True, 

85 db_constraint=False, 

86 db_column="identifier", 

87 related_name="deleted_image", 

88 help_text="The reference to the deleted image.", 

89 ) 

90 

91 

92class SensitiveImage(AbstractSensitiveMedia): 

93 """ 

94 Images with verified sensitivity reports. 

95 

96 Do not create instances of this model manually. Create an ``ImageReport`` instance 

97 instead. 

98 """ 

99 

100 media_class = Image 

101 es_index = settings.MEDIA_INDEX_MAPPING[IMAGE_TYPE] 

102 

103 media_obj = models.OneToOneField( 

104 to="Image", 

105 to_field="identifier", 

106 on_delete=models.DO_NOTHING, 

107 primary_key=True, 

108 db_constraint=False, 

109 db_column="identifier", 

110 related_name="sensitive_image", 

111 help_text="The reference to the sensitive image.", 

112 ) 

113 

114 class Meta: 

115 db_table = "api_matureimage" 

116 

117 

118class ImageReport(AbstractMediaReport): 

119 """ 

120 User-submitted report of an image. 

121 

122 This contains an ``ImageDecision`` as well, if moderators have made a decision 

123 for this report. 

124 """ 

125 

126 media_class = Image 

127 

128 media_obj = models.ForeignKey( 

129 to="Image", 

130 to_field="identifier", 

131 on_delete=models.DO_NOTHING, 

132 db_constraint=False, 

133 db_column="identifier", 

134 related_name="image_report", 

135 help_text="The reference to the image being reported.", 

136 ) 

137 decision = models.ForeignKey( 

138 to="ImageDecision", 

139 on_delete=models.SET_NULL, 

140 blank=True, 

141 null=True, 

142 help_text="The moderation decision for this report.", 

143 ) 

144 

145 class Meta: 

146 db_table = "nsfw_reports" 

147 

148 

149class ImageDecision(AbstractMediaDecision): 

150 """Moderation decisions taken for images.""" 

151 

152 media_class = Image 

153 

154 media_objs = models.ManyToManyField( 

155 to="Image", 

156 through="ImageDecisionThrough", 

157 help_text="The image items being moderated.", 

158 ) 

159 

160 

161class ImageDecisionThrough(AbstractMediaDecisionThrough): 

162 """ 

163 Many-to-many reference table for image decisions. 

164 

165 This is made explicit (rather than using Django's default) so that the image can 

166 be referenced by `identifier` rather than an arbitrary `id`. 

167 """ 

168 

169 media_class = Image 

170 sensitive_media_class = SensitiveImage 

171 deleted_media_class = DeletedImage 

172 

173 media_obj = models.ForeignKey( 

174 Image, 

175 to_field="identifier", 

176 on_delete=models.DO_NOTHING, 

177 db_column="identifier", 

178 db_constraint=False, 

179 ) 

180 decision = models.ForeignKey(ImageDecision, on_delete=models.CASCADE) 

181 

182 

183class ImageList(AbstractMediaList): 

184 """A list of images. Currently unused.""" 

185 

186 images = models.ManyToManyField( 

187 Image, 

188 related_name="lists", 

189 help_text="A list of identifier keys corresponding to images.", 

190 ) 

191 

192 class Meta: 

193 db_table = "imagelist" 

194 

195 def save(self, *args, **kwargs): 

196 self.slug = uuslug(self.title, instance=self) 

197 super().save(*args, **kwargs)