chore: Bump copyright year
[mailman-postorious.git] / src / postorius / views / user.py
blobcb13af36cd198b4a4fbdbe857a31d98df246fd3e
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 1998-2023 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/>.
20 import logging
21 from urllib.error import HTTPError
23 from django.contrib import messages
24 from django.contrib.auth.decorators import login_required
25 from django.contrib.auth.forms import AdminPasswordChangeForm
26 from django.forms import formset_factory
27 from django.http import Http404
28 from django.shortcuts import redirect, render
29 from django.urls import reverse, reverse_lazy
30 from django.utils.decorators import method_decorator
31 from django.utils.translation import gettext as _
32 from django.views.decorators.debug import sensitive_post_parameters
33 from django.views.decorators.http import require_GET
34 from django.views.generic import FormView
36 from allauth.account.models import EmailAddress
37 from django_mailman3.lib.mailman import get_mailman_client, get_mailman_user
38 from django_mailman3.lib.paginator import MailmanPaginator, paginate
40 from postorius.auth.decorators import superuser_required
41 from postorius.forms import (
42 ChangeSubscriptionForm,
43 ManageAddressForm,
44 ManageAddressFormSet,
45 ManageMemberForm,
46 ManageMemberFormSet,
47 ManageUserForm,
48 UserPreferences,
49 UserPreferencesFormset,
51 from postorius.models import List, SubscriptionMode
52 from postorius.utils import (
53 filter_memberships_by_roles,
54 get_django_user,
55 set_preferred,
57 from postorius.views.generic import MailmanClientMixin
60 logger = logging.getLogger(__name__)
63 class UserPreferencesView(FormView, MailmanClientMixin):
64 """Generic view for the logged-in user's various preferences."""
66 form_class = UserPreferences
68 #: Disabled delivery_status choices that a subscriber cannot set. This is
69 #: different from what an admi is allowed to set.
70 delivery_status_disabled_fields = ['by_moderator', 'by_bounces']
72 def get_context_data(self, **kwargs):
73 data = super(UserPreferencesView, self).get_context_data(**kwargs)
74 data['mm_user'] = self.mm_user
75 return data
77 def get_form_kwargs(self):
78 kwargs = super(UserPreferencesView, self).get_form_kwargs()
79 kwargs['preferences'] = self._get_preferences()
80 # Disable the choice of by_admin and by_bounces for a user.
81 kwargs[
82 'disabled_delivery_choices'
83 ] = self.delivery_status_disabled_fields
84 return kwargs
86 def _set_view_attributes(self, request, *args, **kwargs):
87 self.mm_user = get_mailman_user(request.user)
89 @method_decorator(login_required)
90 def dispatch(self, request, *args, **kwargs):
91 self._set_view_attributes(request, *args, **kwargs)
92 return super(UserPreferencesView, self).dispatch(
93 request, *args, **kwargs
96 def form_valid(self, form):
97 try:
98 form.save()
99 except HTTPError as e:
100 messages.error(self.request, e.msg)
101 if form.has_changed():
102 messages.success(
103 self.request, _('Your preferences have been updated.')
105 else:
106 messages.info(self.request, _('Your preferences did not change.'))
107 return super(UserPreferencesView, self).form_valid(form)
110 class UserMailmanSettingsView(UserPreferencesView):
111 """The logged-in user's global Mailman preferences."""
113 form_class = UserPreferences
114 template_name = 'postorius/user/mailman_settings.html'
115 success_url = reverse_lazy('user_mailmansettings')
117 def _get_preferences(self):
118 # Get the defaults and pre-populate so view shows them
119 combinedpreferences = self._get_combined_preferences()
120 for key in combinedpreferences:
121 if key != 'self_link':
122 self.mm_user.preferences[key] = combinedpreferences[key]
124 # This is a bit of a hack so preferences behave as users expect
125 # We probably don't want to save, only display here
126 # but this means that whatever preferences the users see first are
127 # the ones they have unless they explicitly change them
128 self.mm_user.preferences.save()
130 return self.mm_user.preferences
132 def _get_combined_preferences(self):
133 # Get layers of default preferences to match how they are applied
134 # We ignore self_link as we don't want to over-write it
135 defaultpreferences = get_mailman_client().preferences
136 combinedpreferences = {}
137 for key in defaultpreferences:
138 if key != 'self_link':
139 combinedpreferences[key] = defaultpreferences[key]
141 # Clobber defaults with any preferences already set
142 for key in self.mm_user.preferences:
143 if key != 'self_link':
144 combinedpreferences[key] = self.mm_user.preferences[key]
146 return combinedpreferences
149 class UserAddressPreferencesView(UserPreferencesView):
150 """The logged-in user's address-based Mailman Preferences."""
152 template_name = 'postorius/user/address_preferences.html'
153 success_url = reverse_lazy('user_address_preferences')
155 def get_form_class(self):
156 return formset_factory(
157 UserPreferences, formset=UserPreferencesFormset, extra=0
160 def _get_preferences(self):
161 return [address.preferences for address in self.mm_user.addresses]
163 def _get_combined_preferences(self):
164 # grab the default preferences
165 defaultpreferences = get_mailman_client().preferences
167 # grab your global preferences
168 globalpreferences = self.mm_user.preferences
170 # start a new combined preferences object
171 combinedpreferences = []
173 for address in self.mm_user.addresses:
174 # make a per-address prefs object
175 prefs = {}
177 # initialize with default preferences
178 for key in defaultpreferences:
179 if key != 'self_link':
180 prefs[key] = defaultpreferences[key]
182 # overwrite with user's global preferences
183 for key in globalpreferences:
184 if key != 'self_link':
185 prefs[key] = globalpreferences[key]
187 # overwrite with address-specific preferences
188 for key in address.preferences:
189 if key != 'self_link':
190 prefs[key] = address.preferences[key]
191 combinedpreferences.append(prefs)
193 # put the combined preferences back on the original object
194 for key in prefs:
195 if key != 'self_link':
196 address.preferences[key] = prefs[key]
198 return combinedpreferences
200 def get_context_data(self, **kwargs):
201 data = super(UserAddressPreferencesView, self).get_context_data(
202 **kwargs
204 data['formset'] = data.pop('form')
205 for form, address in list(
206 zip(data['formset'].forms, self.mm_user.addresses)
208 form.address = address
209 return data
212 class UserListOptionsView(UserPreferencesView):
213 """The logged-in user's subscription preferences."""
215 form_class = UserPreferences
216 template_name = 'postorius/user/list_options.html'
218 def _get_subscription(self, member_id):
219 subscription = None
220 # We *could* use the find_members API, but then we'd have to
221 # authenticate that the found subscription belongs to the currently
222 # logged-in user otherwise. That might be a faster choice, but this
223 # page isn't that slow right now.
224 for s in self.mm_user.subscriptions:
225 if s.role == 'member' and s.member_id == member_id:
226 subscription = s
227 break
228 if not subscription:
229 raise Http404(_('Subscription does not exist'))
230 return subscription
232 def _set_view_attributes(self, request, *args, **kwargs):
233 super(UserListOptionsView, self)._set_view_attributes(
234 request, *args, **kwargs
236 self.member_id = kwargs.get('member_id')
237 self.subscription = self._get_subscription(self.member_id)
238 self.mlist = List.objects.get_or_404(
239 fqdn_listname=self.subscription.list_id
241 if (
242 self.subscription.subscription_mode
243 == SubscriptionMode.as_user.name
245 self.subscriber = self.subscription.user.user_id
246 else:
247 self.subscriber = self.subscription.email
249 def _get_preferences(self):
250 return self.subscription.preferences
252 def get_context_data(self, **kwargs):
253 data = super(UserListOptionsView, self).get_context_data(**kwargs)
254 data['mlist'] = self.mlist
255 user_emails = (
256 EmailAddress.objects.filter(user=self.request.user, verified=True)
257 .order_by('email')
258 .values_list('email', flat=True)
260 mm_user = get_mailman_user(self.request.user)
261 primary_email = None
262 if mm_user.preferred_address is None:
263 primary_email = set_preferred(self.request.user, mm_user)
264 else:
265 primary_email = mm_user.preferred_address.email
266 data['change_subscription_form'] = ChangeSubscriptionForm(
267 user_emails,
268 mm_user.user_id,
269 primary_email,
270 initial={
271 'subscriber': self.subscriber,
272 'member_id': self.member_id,
275 return data
277 def get_success_url(self):
278 return reverse(
279 'user_list_options', kwargs=dict(member_id=self.member_id)
283 class UserSubscriptionPreferencesView(UserPreferencesView):
284 """The logged-in user's subscription-based Mailman Preferences."""
286 template_name = 'postorius/user/subscription_preferences.html'
287 success_url = reverse_lazy('user_subscription_preferences')
289 def _get_subscriptions(self):
290 subscriptions = []
291 for s in self.mm_user.subscriptions:
292 if s.role != 'member':
293 continue
294 subscriptions.append(s)
295 return subscriptions
297 def _set_view_attributes(self, request, *args, **kwargs):
298 super(UserSubscriptionPreferencesView, self)._set_view_attributes(
299 request, *args, **kwargs
301 self.subscriptions = self._get_subscriptions()
303 def get_form_class(self):
304 return formset_factory(
305 UserPreferences, formset=UserPreferencesFormset, extra=0
308 def _get_preferences(self):
309 return [sub.preferences for sub in self.subscriptions]
311 def _get_combined_preferences(self):
312 # grab the default preferences
313 defaultpreferences = get_mailman_client().preferences
315 # grab your global preferences
316 globalpreferences = self.mm_user.preferences
318 # start a new combined preferences object
319 combinedpreferences = []
321 for sub in self.subscriptions:
322 # make a per-address prefs object
323 prefs = {}
325 # initialize with default preferences
326 for key in defaultpreferences:
327 if key != 'self_link':
328 prefs[key] = defaultpreferences[key]
330 # overwrite with user's global preferences
331 for key in globalpreferences:
332 if key != 'self_link':
333 prefs[key] = globalpreferences[key]
335 # overwrite with address-based preferences
336 # There is currently no better way to do this,
337 # we may consider revisiting.
338 addresspreferences = {}
339 for address in self.mm_user.addresses:
340 if sub.email == address.email:
341 addresspreferences = address.preferences
343 for key in addresspreferences:
344 if key != 'self_link':
345 prefs[key] = addresspreferences[key]
347 # overwrite with subscription-specific preferences
348 for key in sub.preferences:
349 if key != 'self_link':
350 prefs[key] = sub.preferences[key]
352 combinedpreferences.append(prefs)
354 return combinedpreferences
355 # return [sub.preferences for sub in self.subscriptions]
357 def get_context_data(self, **kwargs):
358 data = super(UserSubscriptionPreferencesView, self).get_context_data(
359 **kwargs
361 data['formset'] = data.pop('form')
362 for form, subscription in list(
363 zip(data['formset'].forms, self.subscriptions)
365 form.list_id = subscription.list_id
366 form.member_id = subscription.member_id
367 form.subscription_mode = subscription.subscription_mode
368 form.address = subscription.address
369 return data
372 @login_required
373 def user_subscriptions(request):
374 """Shows the subscriptions of a user."""
375 mm_user = get_mailman_user(request.user)
376 memberships = [m for m in mm_user.subscriptions]
377 return render(
378 request,
379 'postorius/user/subscriptions.html',
380 {'memberships': memberships},
384 @require_GET
385 @superuser_required
386 def list_users(request):
387 """List of all users."""
388 client = get_mailman_client()
389 query = request.GET.get('q')
391 if query:
393 def _find_users(count, page):
394 return client.find_users_page(query, count, page)
396 find_method = _find_users
397 else:
398 find_method = client.get_user_page
400 users = paginate(
401 find_method,
402 request.GET.get('page'),
403 request.GET.get('count'),
404 paginator_class=MailmanPaginator,
406 return render(
407 request,
408 'postorius/user/all.html',
409 {'all_users': users, 'query': query},
413 @superuser_required
414 @sensitive_post_parameters('password1', 'password2')
415 def manage_user(request, user_id):
416 """Manage a single Mailman user view."""
417 client = get_mailman_client()
418 user = client.get_user(user_id)
419 user_form = ManageUserForm(user=user)
420 addr_formset = formset_factory(
421 ManageAddressForm, formset=ManageAddressFormSet, extra=0
423 sub_formset = formset_factory(
424 ManageMemberForm, formset=ManageMemberFormSet, extra=0
426 django_user = get_django_user(user)
427 addresses = addr_formset(addresses=user.addresses)
428 subscriptions = sub_formset(
429 members=filter_memberships_by_roles(
430 user.subscriptions, roles=['member', 'nonmember']
434 change_password = None
435 if django_user is not None:
436 change_password = AdminPasswordChangeForm(django_user)
437 # The form always grabs focus so stop it from doing that unless there
438 # is an error in the form submitted.
439 change_password.fields['password1'].widget.attrs['autofocus'] = False
441 if request.method == 'POST':
442 # There are 4 forms in this view page, which one was submitted is
443 # distinguished based on the name of the submit button.
444 if 'address_form' in request.POST:
445 # This is the 'addresses' form, built using addr_formset.
446 addresses = addr_formset(request.POST, addresses=user.addresses)
447 if addresses.is_valid():
448 updated = addresses.save()
449 if updated:
450 messages.success(
451 request,
452 _('Successfully updated addresses {}').format(
453 ', '.join(updated)
455 ) # noqa: E501
456 elif 'subs_form' in request.POST:
457 # This is the 'subscriptions' form, built using sub_formset.
458 subscriptions = sub_formset(
459 request.POST, members=user.subscriptions
461 if subscriptions.is_valid():
462 updated = subscriptions.save()
463 if updated:
464 messages.success(
465 request,
466 _('Successfully updated memberships for {}').format(
467 ', '.join(updated)
469 ) # noqa: E501
470 elif 'user_form' in request.POST:
471 # This is the 'user' form, built using ManageUserForm.
472 user_form = ManageUserForm(request.POST, user=user)
473 if user_form.is_valid():
474 user_form.save()
475 messages.success(request, _('Successfully updated user.'))
476 elif 'change_password' in request.POST:
477 change_password = AdminPasswordChangeForm(
478 django_user, request.POST
480 if change_password.is_valid():
481 change_password.save()
482 # This will log the user out, which we want because the admin
483 # is changing their password. In case of user changing their
484 # own passowrd, they can remain authenticated.
485 messages.success(request, _('Password updated successfully'))
486 # Stop the form from grabbing the passowrd if successfully
487 # updated.
488 change_password.fields['password1'].widget.attrs[
489 'autofocus'
490 ] = False
492 return redirect(reverse('manage_user', args=(user_id,)))
494 # In case of GET request, return the formsets with initial data.
495 return render(
496 request,
497 'postorius/user/manage.html',
499 'auser': user,
500 'user_form': user_form,
501 'change_password': change_password,
502 'django_user': django_user,
503 'addresses': addresses,
504 'subscriptions': subscriptions,