We can mark the role as translatable and use variable translation.
[mailman-postorious.git] / src / postorius / models.py
blob8b7f85ff32c1d5e3b41253a199807ef6f03b1970
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 1998-2022 by the Free Software Foundation, Inc.
4 # This file is part of Postorius.
6 # Postorius is free software: you can redistribute it and/or modify it under
7 # the terms of the GNU General Public License as published by the Free
8 # Software Foundation, either version 3 of the License, or (at your option)
9 # any later version.
11 # Postorius is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 # more details.
16 # You should have received a copy of the GNU General Public License along with
17 # Postorius. If not, see <http://www.gnu.org/licenses/>.
19 import logging
20 from enum import Enum
21 from urllib.error import HTTPError
22 from urllib.parse import urljoin
24 from django.conf import settings
25 from django.core.exceptions import ImproperlyConfigured
26 from django.db import models
27 from django.db.models.signals import post_delete, post_save
28 from django.dispatch import receiver
29 from django.http import Http404
30 from django.urls import reverse
31 from django.utils.translation import gettext_lazy as _
33 from mailmanclient import MailmanConnectionError
35 from postorius.template_list import TEMPLATES_LIST
36 from postorius.utils import LANGUAGES, get_mailman_client
39 logger = logging.getLogger(__name__)
41 _email_template_help_text = _(
42 'Note: Do not add any secret content in templates as they are '
43 'publicly accessible.\n'
44 'You can use these variables in the templates. \n'
45 '$hyperkitty_url: Permalink to archived message in Hyperkitty\n'
46 '$listname: Name of the Mailing List e.g. ant@example.com \n'
47 '$list_id: The List-ID header e.g. ant.example.com \n'
48 '$display_name: Display name of the mailing list e.g. Ant \n'
49 '$short_listname: Local part of the listname e.g. ant \n'
50 '$domain: The domain part of the listname e.g. example.com \n'
51 '$info: The mailing list\'s longer descriptive text \n'
52 '$request_email: The email address for -request address \n'
53 '$owner_email: The email address for -owner address \n'
54 '$site_email: The email address to reach the owners of the site \n'
55 '$language: The two letter language code for list\'s preferred language e.g. fr, en, de \n' # noqa: E501
59 class SubscriptionMode(Enum):
60 """Valid values for Member.subscription_mode"""
62 as_address = 'as_address'
63 as_user = 'as_user'
66 class MemberRole(Enum):
67 """List of roles a Member resource can have.
69 The primary purpose of this enum right now is only to add the
70 role values to translations. At some point, we want to use MemberRole.owner
71 in all the places we are using the literal role as string.
72 """
73 owner = _('owner')
74 member = _('member')
75 nonmember = _('nonmember')
76 moderator = _('moderator')
79 class MailmanApiError(Exception):
80 """Raised if the API is not available.
81 """
82 pass
85 class Mailman404Error(Exception):
86 """Proxy exception. Raised if the API returns 404."""
87 pass
90 class MailmanRestManager(object):
91 """Manager class to give a model class CRUD access to the API.
92 Returns objects (or lists of objects) retrieved from the API.
93 """
95 def __init__(self, resource_name, resource_name_plural, cls_name=None):
96 self.resource_name = resource_name
97 self.resource_name_plural = resource_name_plural
99 def all(self):
100 try:
101 return getattr(get_mailman_client(), self.resource_name_plural)
102 except AttributeError:
103 raise MailmanApiError
104 except MailmanConnectionError as e:
105 raise MailmanApiError(e)
107 def get(self, *args, **kwargs):
108 try:
109 method = getattr(get_mailman_client(), 'get_' + self.resource_name)
110 return method(*args, **kwargs)
111 except AttributeError as e:
112 raise MailmanApiError(e)
113 except HTTPError as e:
114 if e.code == 404:
115 raise Mailman404Error('Mailman resource could not be found.')
116 else:
117 raise
118 except MailmanConnectionError as e:
119 raise MailmanApiError(e)
121 def get_or_404(self, *args, **kwargs):
122 """Similar to `self.get` but raises standard Django 404 error.
124 try:
125 return self.get(*args, **kwargs)
126 except Mailman404Error:
127 raise Http404
128 except MailmanConnectionError as e:
129 raise MailmanApiError(e)
131 def create(self, *args, **kwargs):
132 try:
133 method = getattr(
134 get_mailman_client(), 'create_' + self.resource_name)
135 return method(*args, **kwargs)
136 except AttributeError as e:
137 raise MailmanApiError(e)
138 except HTTPError as e:
139 if e.code == 409:
140 raise MailmanApiError
141 else:
142 raise
143 except MailmanConnectionError:
144 raise MailmanApiError
146 def delete(self):
147 """Not implemented since the objects returned from the API
148 have a `delete` method of their own.
150 pass
153 class MailmanListManager(MailmanRestManager):
155 def __init__(self):
156 super(MailmanListManager, self).__init__('list', 'lists')
158 def all(self, advertised=False):
159 try:
160 method = getattr(
161 get_mailman_client(), 'get_' + self.resource_name_plural)
162 return method(advertised=advertised)
163 except AttributeError:
164 raise MailmanApiError
165 except MailmanConnectionError as e:
166 raise MailmanApiError(e)
168 def by_mail_host(self, mail_host, advertised=False):
169 objects = self.all(advertised)
170 host_objects = []
171 for obj in objects:
172 if obj.mail_host == mail_host:
173 host_objects.append(obj)
174 return host_objects
177 class MailmanUserManager(MailmanRestManager):
179 def __init__(self):
180 super(MailmanUserManager, self).__init__('user', 'users')
183 class MailmanRestModel(object):
184 """Simple REST Model class to make REST API calls Django style.
186 MailmanApiError = MailmanApiError
187 DoesNotExist = Mailman404Error
189 def __init__(self, *args, **kwargs):
190 self.args = args
191 self.kwargs = kwargs
193 def save(self):
194 """Proxy function for `objects.create`.
195 (REST API uses `create`, while Django uses `save`.)
197 self.objects.create(*self.args, **self.kwargs)
200 class Domain(MailmanRestModel):
201 """Domain model class.
203 objects = MailmanRestManager('domain', 'domains')
206 class List(MailmanRestModel):
207 """List model class.
209 objects = MailmanListManager()
212 class MailmanUser(MailmanRestModel):
213 """MailmanUser model class.
215 objects = MailmanUserManager()
218 class Member(MailmanRestModel):
219 """Member model class.
221 objects = MailmanRestManager('member', 'members')
224 class Style(MailmanRestModel):
227 objects = MailmanRestManager(None, 'styles')
230 TEMPLATE_CONTEXT_CHOICES = (
231 ('site', 'Site Wide'),
232 ('domain', 'Domain Wide'),
233 ('list', 'MailingList Wide')
237 class EmailTemplate(models.Model):
238 """A Template represents contents of partial or complete emails sent out by
239 Mailman Core on various events or when an action is required. Headers and
240 Footers on emails for decorations are also repsented as templates.
243 # Ease differentiating the various Mailman templates by providing the
244 # template file's name (key) prepended in square brackets to the
245 # template's purpose (value).
246 _templates_list_choices = [
247 (t[0], "[{key}] - {value}".format(key=t[0], value=t[1]))
248 for t in TEMPLATES_LIST
251 name = models.CharField(
252 max_length=100, choices=_templates_list_choices,
253 help_text=_('Choose the template you want to customize.'))
254 data = models.TextField(
255 help_text=_email_template_help_text,
256 blank=True,
258 language = models.CharField(
259 max_length=5, choices=LANGUAGES,
260 help_text=_('Language for the template, this should be the list\'s preferred language.'), # noqa: E501
261 blank=True)
262 created_at = models.DateTimeField(auto_now_add=True)
263 modified_at = models.DateTimeField(auto_now=True)
264 context = models.CharField(max_length=50, choices=TEMPLATE_CONTEXT_CHOICES)
265 identifier = models.CharField(blank=True, max_length=100)
267 class Meta:
268 unique_together = ('name', 'identifier', 'language')
270 def __str__(self):
271 return '<EmailTemplate {0} for {1}>'.format(self.name, self.context)
273 @property
274 def description(self):
275 """Return the long description of template that is human readable."""
276 return dict(TEMPLATES_LIST)[self.name]
278 @property
279 def api_url(self):
280 """API url is the remote url that Core can use to fetch templates"""
281 base_url = getattr(settings, 'POSTORIUS_TEMPLATE_BASE_URL', None)
282 if not base_url:
283 raise ImproperlyConfigured(
284 'Setting "POSTORIUS_TEMPLATE_BASE_URL" is not configured.')
285 resource_url = reverse(
286 'rest_template',
287 kwargs=dict(context=self.context,
288 identifier=self.identifier,
289 name=self.name)
291 return urljoin(base_url, resource_url)
293 def _get_context_obj(self):
294 if self.context == 'list':
295 obj = List.objects.get_or_404(fqdn_listname=self.identifier)
296 elif self.context == 'domain':
297 obj = Domain.objects.get_or_404(mail_host=self.identifier)
298 elif self.context == 'site':
299 obj = get_mailman_client()
300 else:
301 obj = None
302 return obj
304 def _update_core(self, deleted=False):
305 obj = self._get_context_obj()
306 if obj is None:
307 return
309 if deleted:
310 # POST'ing an empty string will delete this record in Core.
311 api_url = ''
312 else:
313 # Use the API endpoint of self that Core can use to fetch this.
314 api_url = self.api_url
315 obj.set_template(self.name, api_url)
318 @receiver(post_save, sender=EmailTemplate)
319 def update_core_post_update(sender, **kwargs):
320 kwargs['instance']._update_core()
323 @receiver(post_delete, sender=EmailTemplate)
324 def update_core_post_delete(sender, **kwargs):
325 kwargs['instance']._update_core(deleted=True)