Bump copyright year
[mailman-postorious.git] / src / postorius / views / user.py
blob917525880c07c51fe6ff1ff6a7ea98eba5fa619c
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/>.
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 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, ManageAddressForm, ManageAddressFormSet,
43 ManageMemberForm, ManageMemberFormSet, ManageUserForm, UserPreferences,
44 UserPreferencesFormset)
45 from postorius.models import List, SubscriptionMode
46 from postorius.utils import (
47 filter_memberships_by_roles, get_django_user, set_preferred)
48 from postorius.views.generic import MailmanClientMixin
51 logger = logging.getLogger(__name__)
54 class UserPreferencesView(FormView, MailmanClientMixin):
55 """Generic view for the logged-in user's various preferences."""
57 form_class = UserPreferences
59 #: Disabled delivery_status choices that a subscriber cannot set. This is
60 #: different from what an admi is allowed to set.
61 delivery_status_disabled_fields = ['by_moderator', 'by_bounces']
63 def get_context_data(self, **kwargs):
64 data = super(UserPreferencesView, self).get_context_data(**kwargs)
65 data['mm_user'] = self.mm_user
66 return data
68 def get_form_kwargs(self):
69 kwargs = super(UserPreferencesView, self).get_form_kwargs()
70 kwargs['preferences'] = self._get_preferences()
71 # Disable the choice of by_admin and by_bounces for a user.
72 kwargs[
73 'disabled_delivery_choices'] = self.delivery_status_disabled_fields
74 return kwargs
76 def _set_view_attributes(self, request, *args, **kwargs):
77 self.mm_user = get_mailman_user(request.user)
79 @method_decorator(login_required)
80 def dispatch(self, request, *args, **kwargs):
81 self._set_view_attributes(request, *args, **kwargs)
82 return super(UserPreferencesView, self).dispatch(
83 request, *args, **kwargs)
85 def form_valid(self, form):
86 try:
87 form.save()
88 except HTTPError as e:
89 messages.error(self.request, e.msg)
90 if form.has_changed():
91 messages.success(
92 self.request, _('Your preferences have been updated.'))
93 else:
94 messages.info(self.request, _('Your preferences did not change.'))
95 return super(UserPreferencesView, self).form_valid(form)
98 class UserMailmanSettingsView(UserPreferencesView):
99 """The logged-in user's global Mailman preferences."""
101 form_class = UserPreferences
102 template_name = 'postorius/user/mailman_settings.html'
103 success_url = reverse_lazy('user_mailmansettings')
105 def _get_preferences(self):
106 # Get the defaults and pre-populate so view shows them
107 combinedpreferences = self._get_combined_preferences()
108 for key in combinedpreferences:
109 if key != "self_link":
110 self.mm_user.preferences[key] = combinedpreferences[key]
112 # This is a bit of a hack so preferences behave as users expect
113 # We probably don't want to save, only display here
114 # but this means that whatever preferences the users see first are
115 # the ones they have unless they explicitly change them
116 self.mm_user.preferences.save()
118 return self.mm_user.preferences
120 def _get_combined_preferences(self):
121 # Get layers of default preferences to match how they are applied
122 # We ignore self_link as we don't want to over-write it
123 defaultpreferences = get_mailman_client().preferences
124 combinedpreferences = {}
125 for key in defaultpreferences:
126 if key != "self_link":
127 combinedpreferences[key] = defaultpreferences[key]
129 # Clobber defaults with any preferences already set
130 for key in self.mm_user.preferences:
131 if key != "self_link":
132 combinedpreferences[key] = self.mm_user.preferences[key]
134 return(combinedpreferences)
137 class UserAddressPreferencesView(UserPreferencesView):
138 """The logged-in user's address-based Mailman Preferences."""
140 template_name = 'postorius/user/address_preferences.html'
141 success_url = reverse_lazy('user_address_preferences')
143 def get_form_class(self):
144 return formset_factory(
145 UserPreferences, formset=UserPreferencesFormset, extra=0)
147 def _get_preferences(self):
148 return [address.preferences for address in self.mm_user.addresses]
150 def _get_combined_preferences(self):
151 # grab the default preferences
152 defaultpreferences = get_mailman_client().preferences
154 # grab your global preferences
155 globalpreferences = self.mm_user.preferences
157 # start a new combined preferences object
158 combinedpreferences = []
160 for address in self.mm_user.addresses:
161 # make a per-address prefs object
162 prefs = {}
164 # initialize with default preferences
165 for key in defaultpreferences:
166 if key != "self_link":
167 prefs[key] = defaultpreferences[key]
169 # overwrite with user's global preferences
170 for key in globalpreferences:
171 if key != "self_link":
172 prefs[key] = globalpreferences[key]
174 # overwrite with address-specific preferences
175 for key in address.preferences:
176 if key != "self_link":
177 prefs[key] = address.preferences[key]
178 combinedpreferences.append(prefs)
180 # put the combined preferences back on the original object
181 for key in prefs:
182 if key != "self_link":
183 address.preferences[key] = prefs[key]
185 return combinedpreferences
187 def get_context_data(self, **kwargs):
188 data = super(UserAddressPreferencesView, self).get_context_data(
189 **kwargs)
190 data['formset'] = data.pop('form')
191 for form, address in list(zip(
192 data['formset'].forms, self.mm_user.addresses)):
193 form.address = address
194 return data
197 class UserListOptionsView(UserPreferencesView):
198 """The logged-in user's subscription preferences."""
200 form_class = UserPreferences
201 template_name = 'postorius/user/list_options.html'
203 def _get_subscription(self, member_id):
204 subscription = None
205 # We *could* use the find_members API, but then we'd have to
206 # authenticate that the found subscription belongs to the currently
207 # logged-in user otherwise. That might be a faster choice, but this
208 # page isn't that slow right now.
209 for s in self.mm_user.subscriptions:
210 if s.role == 'member' and s.member_id == member_id:
211 subscription = s
212 break
213 if not subscription:
214 raise Http404(_('Subscription does not exist'))
215 return subscription
217 def _set_view_attributes(self, request, *args, **kwargs):
218 super(UserListOptionsView, self)._set_view_attributes(
219 request, *args, **kwargs)
220 self.member_id = kwargs.get('member_id')
221 self.subscription = self._get_subscription(self.member_id)
222 self.mlist = List.objects.get_or_404(
223 fqdn_listname=self.subscription.list_id)
224 if (self.subscription.subscription_mode ==
225 SubscriptionMode.as_user.name):
226 self.subscriber = self.subscription.user.user_id
227 else:
228 self.subscriber = self.subscription.email
230 def _get_preferences(self):
231 return self.subscription.preferences
233 def get_context_data(self, **kwargs):
234 data = super(UserListOptionsView, self).get_context_data(**kwargs)
235 data['mlist'] = self.mlist
236 user_emails = EmailAddress.objects.filter(
237 user=self.request.user, verified=True).order_by(
238 "email").values_list("email", flat=True)
239 mm_user = get_mailman_user(self.request.user)
240 primary_email = None
241 if mm_user.preferred_address is None:
242 primary_email = set_preferred(self.request.user, mm_user)
243 else:
244 primary_email = mm_user.preferred_address.email
245 data['change_subscription_form'] = ChangeSubscriptionForm(
246 user_emails, mm_user.user_id, primary_email,
247 initial={'subscriber': self.subscriber,
248 'member_id': self.member_id})
249 return data
251 def get_success_url(self):
252 return reverse(
253 'user_list_options', kwargs=dict(member_id=self.member_id))
256 class UserSubscriptionPreferencesView(UserPreferencesView):
257 """The logged-in user's subscription-based Mailman Preferences."""
259 template_name = 'postorius/user/subscription_preferences.html'
260 success_url = reverse_lazy('user_subscription_preferences')
262 def _get_subscriptions(self):
263 subscriptions = []
264 for s in self.mm_user.subscriptions:
265 if s.role != 'member':
266 continue
267 subscriptions.append(s)
268 return subscriptions
270 def _set_view_attributes(self, request, *args, **kwargs):
271 super(UserSubscriptionPreferencesView, self)._set_view_attributes(
272 request, *args, **kwargs)
273 self.subscriptions = self._get_subscriptions()
275 def get_form_class(self):
276 return formset_factory(
277 UserPreferences, formset=UserPreferencesFormset, extra=0)
279 def _get_preferences(self):
280 return [sub.preferences for sub in self.subscriptions]
282 def _get_combined_preferences(self):
283 # grab the default preferences
284 defaultpreferences = get_mailman_client().preferences
286 # grab your global preferences
287 globalpreferences = self.mm_user.preferences
289 # start a new combined preferences object
290 combinedpreferences = []
292 for sub in self.subscriptions:
293 # make a per-address prefs object
294 prefs = {}
296 # initialize with default preferences
297 for key in defaultpreferences:
298 if key != "self_link":
299 prefs[key] = defaultpreferences[key]
301 # overwrite with user's global preferences
302 for key in globalpreferences:
303 if key != "self_link":
304 prefs[key] = globalpreferences[key]
306 # overwrite with address-based preferences
307 # There is currently no better way to do this,
308 # we may consider revisiting.
309 addresspreferences = {}
310 for address in self.mm_user.addresses:
311 if sub.email == address.email:
312 addresspreferences = address.preferences
314 for key in addresspreferences:
315 if key != "self_link":
316 prefs[key] = addresspreferences[key]
318 # overwrite with subscription-specific preferences
319 for key in sub.preferences:
320 if key != "self_link":
321 prefs[key] = sub.preferences[key]
323 combinedpreferences.append(prefs)
325 return combinedpreferences
326 # return [sub.preferences for sub in self.subscriptions]
328 def get_context_data(self, **kwargs):
329 data = super(UserSubscriptionPreferencesView, self).get_context_data(
330 **kwargs)
331 data['formset'] = data.pop('form')
332 for form, subscription in list(zip(
333 data['formset'].forms, self.subscriptions)):
334 form.list_id = subscription.list_id
335 form.member_id = subscription.member_id
336 form.subscription_mode = subscription.subscription_mode
337 form.address = subscription.address
338 return data
341 @login_required
342 def user_subscriptions(request):
343 """Shows the subscriptions of a user."""
344 mm_user = get_mailman_user(request.user)
345 memberships = [m for m in mm_user.subscriptions]
346 return render(request, 'postorius/user/subscriptions.html',
347 {'memberships': memberships})
350 @require_GET
351 @superuser_required
352 def list_users(request):
353 """List of all users."""
354 client = get_mailman_client()
355 query = request.GET.get('q')
357 if query:
358 def _find_users(count, page):
359 return client.find_users_page(query, count, page)
360 find_method = _find_users
361 else:
362 find_method = client.get_user_page
364 users = paginate(find_method,
365 request.GET.get('page'),
366 request.GET.get('count'),
367 paginator_class=MailmanPaginator)
368 return render(request,
369 'postorius/user/all.html',
370 {'all_users': users, 'query': query})
373 @superuser_required
374 @sensitive_post_parameters('password1', 'password2')
375 def manage_user(request, user_id):
376 """Manage a single Mailman user view."""
377 client = get_mailman_client()
378 user = client.get_user(user_id)
379 user_form = ManageUserForm(user=user)
380 addr_formset = formset_factory(
381 ManageAddressForm, formset=ManageAddressFormSet, extra=0)
382 sub_formset = formset_factory(
383 ManageMemberForm, formset=ManageMemberFormSet, extra=0)
384 django_user = get_django_user(user)
385 addresses = addr_formset(addresses=user.addresses)
386 subscriptions = sub_formset(members=filter_memberships_by_roles(
387 user.subscriptions, roles=['member', 'nonmember']))
389 change_password = None
390 if django_user is not None:
391 change_password = AdminPasswordChangeForm(django_user)
392 # The form always grabs focus so stop it from doing that unless there
393 # is an error in the form submitted.
394 change_password.fields['password1'].widget.attrs['autofocus'] = False
396 if request.method == 'POST':
397 # There are 4 forms in this view page, which one was submitted is
398 # distinguished based on the name of the submit button.
399 if 'address_form' in request.POST:
400 # This is the 'addresses' form, built using addr_formset.
401 addresses = addr_formset(request.POST, addresses=user.addresses)
402 if addresses.is_valid():
403 updated = addresses.save()
404 if updated:
405 messages.success(
406 request,
407 _('Successfully updated addresses {}').format(', '.join(updated))) # noqa: E501
408 elif 'subs_form' in request.POST:
409 # This is the 'subscriptions' form, built using sub_formset.
410 subscriptions = sub_formset(
411 request.POST, members=user.subscriptions)
412 if subscriptions.is_valid():
413 updated = subscriptions.save()
414 if updated:
415 messages.success(
416 request,
417 _('Successfully updated memberships for {}').format(', '.join(updated))) # noqa: E501
418 elif 'user_form' in request.POST:
419 # This is the 'user' form, built using ManageUserForm.
420 user_form = ManageUserForm(request.POST, user=user)
421 if user_form.is_valid():
422 user_form.save()
423 messages.success(request, _('Successfully updated user.'))
424 elif 'change_password' in request.POST:
425 change_password = AdminPasswordChangeForm(
426 django_user, request.POST)
427 if change_password.is_valid():
428 change_password.save()
429 # This will log the user out, which we want because the admin
430 # is changing their password. In case of user changing their
431 # own passowrd, they can remain authenticated.
432 messages.success(request, _('Password updated successfully'))
433 # Stop the form from grabbing the passowrd if successfully
434 # updated.
435 change_password.fields[
436 'password1'].widget.attrs['autofocus'] = False
438 return render(request,
439 'postorius/user/manage.html',
440 {'auser': user,
441 'user_form': user_form,
442 'change_password': change_password,
443 'django_user': django_user,
444 'addresses': addresses,
445 'subscriptions': subscriptions})