Coverage for website/newsletters/models.py: 73.97%
61 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
1"""The models defined by the newsletters package."""
3from django.core.exceptions import ValidationError
4from django.db import models
5from django.urls import reverse
6from django.utils import timezone
7from django.utils.translation import gettext_lazy as _
9from tinymce.models import HTMLField
12def newsletter_filename(instance, filename):
13 """Return path to store rendered newsletters."""
14 return f"newsletters/{instance.pk}.html"
17class Newsletter(models.Model):
18 """Describes a newsletter."""
20 title = models.CharField(
21 max_length=150,
22 verbose_name=_("Title"),
23 help_text=_("The title is used for the email subject."),
24 blank=False,
25 )
27 date = models.DateField(
28 verbose_name=_("Date"),
29 help_text=_(
30 "This date is used to extract the week of this "
31 "newsletter, best scenario:"
32 "always use the monday of the week the newsletter is "
33 "for. If you leave it empty no week is shown."
34 ),
35 blank=True,
36 null=True,
37 )
39 send_date = models.DateTimeField(
40 verbose_name=_("Send date"),
41 blank=True,
42 null=True,
43 )
45 description = HTMLField(
46 verbose_name=_("Introduction"),
47 help_text=_(
48 "This is the text that starts the newsletter. It always "
49 'begins with "Dear members" and you can append '
50 "whatever you want."
51 ),
52 blank=False,
53 )
55 sent = models.BooleanField(default=False)
57 rendered_file = models.FileField(
58 upload_to=newsletter_filename,
59 null=True,
60 )
62 def get_absolute_url(self):
63 return reverse("newsletters:preview", args=(self.pk,))
65 def clean(self):
66 super().clean()
68 errors = {}
69 url = "admin/newsletters/"
70 if url in self.description:
71 errors.update(
72 {
73 "description": _(
74 "Please make sure all urls are absolute "
75 "and contain http(s)://."
76 )
77 }
78 )
79 if self.send_date and self.send_date <= timezone.now():
80 errors.update(
81 {"send_date": _("Please make sure the send date is not in the past.")}
82 )
84 if errors:
85 raise ValidationError(errors)
87 class Meta:
88 permissions = (("send_newsletter", "Can send newsletter"),)
90 def __str__(self):
91 return str(self.title)
94class NewsletterContent(models.Model):
95 """Describes one piece of basic content of a newsletter."""
97 title = models.CharField(
98 max_length=150,
99 verbose_name=_("Title"),
100 blank=False,
101 null=False,
102 )
104 url = models.URLField(
105 verbose_name=_("URL"),
106 blank=True,
107 null=True,
108 help_text=_("If filled, it will make the title a link to this URL"),
109 )
111 description = HTMLField(
112 verbose_name=_("Description"),
113 blank=False,
114 null=False,
115 )
117 newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE)
119 order = models.PositiveIntegerField(
120 verbose_name=_("order"), blank=False, null=True, default=0
121 )
123 def clean(self):
124 super().clean()
126 errors = {}
127 url = "admin/newsletters/"
128 if url in self.description: 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 errors.update(
130 {
131 "description": _(
132 "Please make sure all urls are absolute "
133 "and start with http(s)://."
134 )
135 }
136 )
138 if errors: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 raise ValidationError(errors)
141 def __str__(self):
142 return str(self.title)
144 class Meta:
145 ordering = ("order",)
148class NewsletterItem(NewsletterContent):
149 """Describes one piece of text content of a newsletter."""
152class NewsletterEvent(NewsletterContent):
153 """Describes one piece of event content of a newsletter."""
155 where = models.CharField(
156 max_length=150,
157 verbose_name=_("Where"),
158 blank=False,
159 null=False,
160 )
162 start_datetime = models.DateTimeField(
163 verbose_name=_("Start date and time"),
164 blank=False,
165 null=False,
166 )
168 end_datetime = models.DateTimeField(
169 verbose_name=_("End date and time"),
170 blank=False,
171 null=False,
172 )
174 show_costs_warning = models.BooleanField(
175 verbose_name=_("Show warnings about costs"), default=True
176 )
178 price = models.DecimalField(
179 verbose_name=_("Price (in Euro)"),
180 max_digits=8,
181 decimal_places=2,
182 blank=True,
183 null=True,
184 default=None,
185 )
187 penalty_costs = models.DecimalField(
188 verbose_name=_("Fine (in Euro)"),
189 max_digits=8,
190 decimal_places=2,
191 blank=True,
192 null=True,
193 default=None,
194 help_text=_(
195 "This is the price that a member has to pay when he/she did not show up."
196 ),
197 )
199 def clean(self):
200 """Make sure that the event end date is after the start date."""
201 super().clean()
202 if (
203 self.end_datetime is not None
204 and self.start_datetime is not None
205 and self.end_datetime < self.start_datetime
206 ):
207 raise ValidationError(
208 {"end_datetime": _("Can't have an event travel back in time")}
209 )