Coverage for website/announcements/models.py: 71.28%

84 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1from django.core.files.storage import storages 

2from django.core.validators import ( 

3 FileExtensionValidator, 

4 get_available_image_extensions, 

5) 

6from django.db import models 

7from django.db.models import CharField, Manager, Q 

8from django.db.models.functions import Now 

9from django.utils import timezone 

10from django.utils.translation import gettext_lazy as _ 

11 

12from tinymce.models import HTMLField 

13 

14from utils.media.services import get_upload_to_function 

15 

16 

17class VisibleObjectManager(Manager): 

18 """Get all active members, i.e. who have a committee membership.""" 

19 

20 def get_queryset(self): 

21 """Select all visible items.""" 

22 return ( 

23 super() 

24 .get_queryset() 

25 .filter( 

26 (Q(until__isnull=True) | Q(until__gt=Now())) 

27 & (Q(since__isnull=True) | Q(since__lte=Now())) 

28 & ~(Q(since__isnull=True) & Q(until__isnull=True)) 

29 ) 

30 ) 

31 

32 

33class Announcement(models.Model): 

34 """Describes an announcement.""" 

35 

36 objects = models.Manager() 

37 visible_objects = VisibleObjectManager() 

38 

39 content = HTMLField( 

40 verbose_name=_("Content"), 

41 help_text=_("The content of the announcement; what text to display."), 

42 blank=False, 

43 max_length=500, 

44 ) 

45 

46 since = models.DateTimeField( 

47 verbose_name=_("Display since"), 

48 help_text=_("Hide this announcement before this time."), 

49 default=timezone.now, 

50 ) 

51 

52 until = models.DateTimeField( 

53 verbose_name=_("Display until"), 

54 help_text=_("Hide this announcement after this time."), 

55 blank=True, 

56 null=True, 

57 ) 

58 

59 icon = models.CharField( 

60 verbose_name=_("Font Awesome icon"), 

61 help_text=_("Font Awesome abbreviation for icon to use."), 

62 max_length=150, 

63 default="bullhorn", 

64 ) 

65 

66 closeable = models.BooleanField(default=True) 

67 

68 class Meta: 

69 ordering = ("-since",) 

70 

71 def __str__(self): 

72 return str(self.content) 

73 

74 @property 

75 def is_visible(self): 

76 """Is this announcement currently visible.""" 

77 return ( 

78 (self.until is None or self.until > timezone.now()) 

79 and (self.since is None or self.since <= timezone.now()) 

80 and not (self.since is None and self.until is None) 

81 ) 

82 

83 

84class FrontpageArticle(models.Model): 

85 """Front page articles.""" 

86 

87 objects = models.Manager() 

88 visible_objects = VisibleObjectManager() 

89 

90 title = models.CharField( 

91 verbose_name=_("Title"), 

92 help_text=_("The title of the article; what goes in the header"), 

93 blank=False, 

94 max_length=80, 

95 ) 

96 

97 content = HTMLField( 

98 verbose_name=_("Content"), 

99 help_text=_("The content of the article; what text to display."), 

100 blank=False, 

101 max_length=5000, 

102 ) 

103 

104 since = models.DateTimeField( 

105 verbose_name=_("Display since"), 

106 help_text=_("Hide this article before this time."), 

107 default=timezone.now, 

108 ) 

109 

110 until = models.DateTimeField( 

111 verbose_name=_("Display until"), 

112 help_text=_("Hide this article after this time."), 

113 blank=True, 

114 null=True, 

115 ) 

116 

117 class Meta: 

118 ordering = ("-since",) 

119 

120 def __str__(self): 

121 return str(self.title) 

122 

123 @property 

124 def is_visible(self): 

125 """Is this announcement currently visible.""" 

126 return ( 

127 (self.until is None or self.until > timezone.now()) 

128 and (self.since is None or self.since <= timezone.now()) 

129 and not (self.since is None and self.until is None) 

130 ) 

131 

132 

133def validate_image(value): 

134 return FileExtensionValidator( 

135 allowed_extensions=[*get_available_image_extensions(), "svg"] 

136 )(value) 

137 

138 

139class Slide(models.Model): 

140 """Describes an announcement.""" 

141 

142 objects = models.Manager() 

143 visible_objects = VisibleObjectManager() 

144 

145 title = CharField( 

146 verbose_name=_("Title"), 

147 help_text=_("The title of the slide; just for the admin."), 

148 blank=False, 

149 max_length=100, 

150 ) 

151 

152 content = models.FileField( 

153 verbose_name=_("Content"), 

154 help_text=_("The content of the slide; what image to display."), 

155 blank=False, 

156 upload_to=get_upload_to_function("announcements/slides"), 

157 storage=storages["public"], 

158 validators=[validate_image], 

159 ) 

160 

161 since = models.DateTimeField( 

162 verbose_name=_("Display since"), 

163 help_text=_( 

164 "Hide this slide before this time. When all date- and " 

165 "time-fields are left blank, the slide won't " 

166 "be visible. It will, however, be visible on an event-page " 

167 "if it's linked to an event." 

168 ), 

169 default=timezone.now, 

170 blank=True, 

171 null=True, 

172 ) 

173 

174 until = models.DateTimeField( 

175 verbose_name=_("Display until"), 

176 help_text=_("Hide this slide after this time."), 

177 blank=True, 

178 null=True, 

179 ) 

180 

181 order = models.PositiveIntegerField( 

182 verbose_name=_("Order"), 

183 help_text=_("Approximately where this slide should appear in the order"), 

184 default=0, 

185 ) 

186 

187 members_only = models.BooleanField( 

188 verbose_name=_("Display only for authenticated members"), default=False 

189 ) 

190 

191 custom_url = models.URLField( 

192 verbose_name=_("Link"), 

193 help_text=_( 

194 "Place the user is taken to when clicking the slide. " 

195 "If left blank, will default to the linked event, if any." 

196 ), 

197 blank=True, 

198 null=True, 

199 ) 

200 

201 url_blank = models.BooleanField( 

202 verbose_name=_("Link outside thalia.nu"), 

203 help_text=_("Clicking the slide will open a new tab"), 

204 default=False, 

205 ) 

206 

207 event = models.OneToOneField( 

208 "events.Event", 

209 related_name="slide", 

210 null=True, 

211 blank=True, 

212 help_text="This event's header image will be changed to this slide.", 

213 on_delete=models.deletion.SET_NULL, 

214 ) 

215 

216 def __init__(self, *args, **kwargs): 

217 super().__init__(*args, **kwargs) 

218 if self.content: 218 ↛ 219line 218 didn't jump to line 219 because the condition on line 218 was never true

219 self._orig_image = self.content.name 

220 else: 

221 self._orig_image = None 

222 

223 def delete(self, using=None, keep_parents=False): 

224 if self.content.name: 

225 self.content.delete() 

226 return super().delete(using, keep_parents) 

227 

228 def save(self, **kwargs): 

229 super().save(**kwargs) 

230 storage = self.content.storage 

231 

232 if self._orig_image and self._orig_image != self.content.name: 

233 storage.delete(self._orig_image) 

234 self._orig_image = None 

235 

236 class Meta: 

237 ordering = ("-since",) 

238 

239 @property 

240 def is_visible(self): 

241 """Is this slide currently visible.""" 

242 return ( 

243 (self.until is None or self.until > timezone.now()) 

244 and (self.since is None or self.since <= timezone.now()) 

245 and not (self.since is None and self.until is None) 

246 ) 

247 

248 @property 

249 def url(self): 

250 if self.custom_url: 

251 return self.custom_url 

252 if self.event: 

253 return self.event.get_absolute_url() 

254 return None 

255 

256 def __str__(self): 

257 return str(self.title)