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
« 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 _
12from tinymce.models import HTMLField
14from utils.media.services import get_upload_to_function
17class VisibleObjectManager(Manager):
18 """Get all active members, i.e. who have a committee membership."""
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 )
33class Announcement(models.Model):
34 """Describes an announcement."""
36 objects = models.Manager()
37 visible_objects = VisibleObjectManager()
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 )
46 since = models.DateTimeField(
47 verbose_name=_("Display since"),
48 help_text=_("Hide this announcement before this time."),
49 default=timezone.now,
50 )
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 )
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 )
66 closeable = models.BooleanField(default=True)
68 class Meta:
69 ordering = ("-since",)
71 def __str__(self):
72 return str(self.content)
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 )
84class FrontpageArticle(models.Model):
85 """Front page articles."""
87 objects = models.Manager()
88 visible_objects = VisibleObjectManager()
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 )
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 )
104 since = models.DateTimeField(
105 verbose_name=_("Display since"),
106 help_text=_("Hide this article before this time."),
107 default=timezone.now,
108 )
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 )
117 class Meta:
118 ordering = ("-since",)
120 def __str__(self):
121 return str(self.title)
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 )
133def validate_image(value):
134 return FileExtensionValidator(
135 allowed_extensions=[*get_available_image_extensions(), "svg"]
136 )(value)
139class Slide(models.Model):
140 """Describes an announcement."""
142 objects = models.Manager()
143 visible_objects = VisibleObjectManager()
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 )
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 )
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 )
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 )
181 order = models.PositiveIntegerField(
182 verbose_name=_("Order"),
183 help_text=_("Approximately where this slide should appear in the order"),
184 default=0,
185 )
187 members_only = models.BooleanField(
188 verbose_name=_("Display only for authenticated members"), default=False
189 )
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 )
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 )
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 )
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
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)
228 def save(self, **kwargs):
229 super().save(**kwargs)
230 storage = self.content.storage
232 if self._orig_image and self._orig_image != self.content.name:
233 storage.delete(self._orig_image)
234 self._orig_image = None
236 class Meta:
237 ordering = ("-since",)
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 )
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
256 def __str__(self):
257 return str(self.title)