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)
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
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/>.
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
,
49 UserPreferencesFormset
,
51 from postorius
.models
import List
, SubscriptionMode
52 from postorius
.utils
import (
53 filter_memberships_by_roles
,
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
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.
82 'disabled_delivery_choices'
83 ] = self
.delivery_status_disabled_fields
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
):
99 except HTTPError
as e
:
100 messages
.error(self
.request
, e
.msg
)
101 if form
.has_changed():
103 self
.request
, _('Your preferences have been updated.')
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
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
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(
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
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
):
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
:
229 raise Http404(_('Subscription does not exist'))
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
242 self
.subscription
.subscription_mode
243 == SubscriptionMode
.as_user
.name
245 self
.subscriber
= self
.subscription
.user
.user_id
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
256 EmailAddress
.objects
.filter(user
=self
.request
.user
, verified
=True)
258 .values_list('email', flat
=True)
260 mm_user
= get_mailman_user(self
.request
.user
)
262 if mm_user
.preferred_address
is None:
263 primary_email
= set_preferred(self
.request
.user
, mm_user
)
265 primary_email
= mm_user
.preferred_address
.email
266 data
['change_subscription_form'] = ChangeSubscriptionForm(
271 'subscriber': self
.subscriber
,
272 'member_id': self
.member_id
,
277 def get_success_url(self
):
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
):
291 for s
in self
.mm_user
.subscriptions
:
292 if s
.role
!= 'member':
294 subscriptions
.append(s
)
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
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(
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
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
]
379 'postorius/user/subscriptions.html',
380 {'memberships': memberships
},
386 def list_users(request
):
387 """List of all users."""
388 client
= get_mailman_client()
389 query
= request
.GET
.get('q')
393 def _find_users(count
, page
):
394 return client
.find_users_page(query
, count
, page
)
396 find_method
= _find_users
398 find_method
= client
.get_user_page
402 request
.GET
.get('page'),
403 request
.GET
.get('count'),
404 paginator_class
=MailmanPaginator
,
408 'postorius/user/all.html',
409 {'all_users': users
, 'query': query
},
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()
452 _('Successfully updated addresses {}').format(
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()
466 _('Successfully updated memberships for {}').format(
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():
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
488 change_password
.fields
['password1'].widget
.attrs
[
492 return redirect(reverse('manage_user', args
=(user_id
,)))
494 # In case of GET request, return the formsets with initial data.
497 'postorius/user/manage.html',
500 'user_form': user_form
,
501 'change_password': change_password
,
502 'django_user': django_user
,
503 'addresses': addresses
,
504 'subscriptions': subscriptions
,