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

1"""The models defined by the newsletters package.""" 

2 

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 _ 

8 

9from tinymce.models import HTMLField 

10 

11 

12def newsletter_filename(instance, filename): 

13 """Return path to store rendered newsletters.""" 

14 return f"newsletters/{instance.pk}.html" 

15 

16 

17class Newsletter(models.Model): 

18 """Describes a newsletter.""" 

19 

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 ) 

26 

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 ) 

38 

39 send_date = models.DateTimeField( 

40 verbose_name=_("Send date"), 

41 blank=True, 

42 null=True, 

43 ) 

44 

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 ) 

54 

55 sent = models.BooleanField(default=False) 

56 

57 rendered_file = models.FileField( 

58 upload_to=newsletter_filename, 

59 null=True, 

60 ) 

61 

62 def get_absolute_url(self): 

63 return reverse("newsletters:preview", args=(self.pk,)) 

64 

65 def clean(self): 

66 super().clean() 

67 

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 ) 

83 

84 if errors: 

85 raise ValidationError(errors) 

86 

87 class Meta: 

88 permissions = (("send_newsletter", "Can send newsletter"),) 

89 

90 def __str__(self): 

91 return str(self.title) 

92 

93 

94class NewsletterContent(models.Model): 

95 """Describes one piece of basic content of a newsletter.""" 

96 

97 title = models.CharField( 

98 max_length=150, 

99 verbose_name=_("Title"), 

100 blank=False, 

101 null=False, 

102 ) 

103 

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 ) 

110 

111 description = HTMLField( 

112 verbose_name=_("Description"), 

113 blank=False, 

114 null=False, 

115 ) 

116 

117 newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE) 

118 

119 order = models.PositiveIntegerField( 

120 verbose_name=_("order"), blank=False, null=True, default=0 

121 ) 

122 

123 def clean(self): 

124 super().clean() 

125 

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 ) 

137 

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) 

140 

141 def __str__(self): 

142 return str(self.title) 

143 

144 class Meta: 

145 ordering = ("order",) 

146 

147 

148class NewsletterItem(NewsletterContent): 

149 """Describes one piece of text content of a newsletter.""" 

150 

151 

152class NewsletterEvent(NewsletterContent): 

153 """Describes one piece of event content of a newsletter.""" 

154 

155 where = models.CharField( 

156 max_length=150, 

157 verbose_name=_("Where"), 

158 blank=False, 

159 null=False, 

160 ) 

161 

162 start_datetime = models.DateTimeField( 

163 verbose_name=_("Start date and time"), 

164 blank=False, 

165 null=False, 

166 ) 

167 

168 end_datetime = models.DateTimeField( 

169 verbose_name=_("End date and time"), 

170 blank=False, 

171 null=False, 

172 ) 

173 

174 show_costs_warning = models.BooleanField( 

175 verbose_name=_("Show warnings about costs"), default=True 

176 ) 

177 

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 ) 

186 

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 ) 

198 

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 )