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)
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 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
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.
73 'disabled_delivery_choices'] = self
.delivery_status_disabled_fields
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
):
88 except HTTPError
as e
:
89 messages
.error(self
.request
, e
.msg
)
90 if form
.has_changed():
92 self
.request
, _('Your preferences have been updated.'))
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
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
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(
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
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
):
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
:
214 raise Http404(_('Subscription does not exist'))
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
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
)
241 if mm_user
.preferred_address
is None:
242 primary_email
= set_preferred(self
.request
.user
, mm_user
)
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
})
251 def get_success_url(self
):
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
):
264 for s
in self
.mm_user
.subscriptions
:
265 if s
.role
!= 'member':
267 subscriptions
.append(s
)
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
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(
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
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
})
352 def list_users(request
):
353 """List of all users."""
354 client
= get_mailman_client()
355 query
= request
.GET
.get('q')
358 def _find_users(count
, page
):
359 return client
.find_users_page(query
, count
, page
)
360 find_method
= _find_users
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
})
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()
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()
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():
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
435 change_password
.fields
[
436 'password1'].widget
.attrs
['autofocus'] = False
438 return render(request
,
439 'postorius/user/manage.html',
441 'user_form': user_form
,
442 'change_password': change_password
,
443 'django_user': django_user
,
444 'addresses': addresses
,
445 'subscriptions': subscriptions
})