mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
User management UI for system administrators
This commit is contained in:
@@ -539,3 +539,60 @@ class CheckInFilterForm(FilterForm):
|
||||
qs = qs.filter(item=fdata.get('item'))
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class UserFilterForm(FilterForm):
|
||||
orders = {
|
||||
'fullname': 'fullname',
|
||||
'email': 'email',
|
||||
}
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=(
|
||||
('', _('All')),
|
||||
('active', _('Active')),
|
||||
('inactive', _('Inactive')),
|
||||
),
|
||||
required=False
|
||||
)
|
||||
superuser = forms.ChoiceField(
|
||||
label=_('Administrator'),
|
||||
choices=(
|
||||
('', _('All')),
|
||||
('yes', _('Administrator')),
|
||||
('no', _('No administrator')),
|
||||
),
|
||||
required=False
|
||||
)
|
||||
query = forms.CharField(
|
||||
label=_('Search query'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search query'),
|
||||
'autofocus': 'autofocus'
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('status') == 'active':
|
||||
qs = qs.filter(is_active=True)
|
||||
elif fdata.get('status') == 'inactive':
|
||||
qs = qs.filter(is_active=False)
|
||||
|
||||
if fdata.get('superuser') == 'yes':
|
||||
qs = qs.filter(is_superuser=True)
|
||||
elif fdata.get('superuser') == 'no':
|
||||
qs = qs.filter(is_superuser=False)
|
||||
|
||||
if fdata.get('query'):
|
||||
qs = qs.filter(
|
||||
Q(email__icontains=fdata.get('query'))
|
||||
| Q(fullname__icontains=fdata.get('query'))
|
||||
)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
return qs
|
||||
|
||||
88
src/pretix/control/forms/users.py
Normal file
88
src/pretix/control/forms/users.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pytz import common_timezones
|
||||
|
||||
from pretix.base.models import User
|
||||
|
||||
|
||||
class UserEditForm(forms.ModelForm):
|
||||
error_messages = {
|
||||
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
|
||||
"Please choose a different one."),
|
||||
'pw_mismatch': _("Please enter the same password twice"),
|
||||
}
|
||||
|
||||
new_pw = forms.CharField(max_length=255,
|
||||
required=False,
|
||||
label=_("New password"),
|
||||
widget=forms.PasswordInput())
|
||||
new_pw_repeat = forms.CharField(max_length=255,
|
||||
required=False,
|
||||
label=_("Repeat new password"),
|
||||
widget=forms.PasswordInput())
|
||||
timezone = forms.ChoiceField(
|
||||
choices=((a, a) for a in common_timezones),
|
||||
label=_("Default timezone"),
|
||||
help_text=_('Only used for views that are not bound to an event. For all '
|
||||
'event views, the event timezone is used instead.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'fullname',
|
||||
'locale',
|
||||
'timezone',
|
||||
'email',
|
||||
'require_2fa',
|
||||
'is_active',
|
||||
'is_superuser'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['email'].required = True
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data['email']
|
||||
if User.objects.filter(Q(email=email) & ~Q(pk=self.instance.pk)).exists():
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['duplicate_identifier'],
|
||||
code='duplicate_identifier',
|
||||
)
|
||||
return email
|
||||
|
||||
def clean_new_pw(self):
|
||||
password1 = self.cleaned_data.get('new_pw', '')
|
||||
if password1 and validate_password(password1, user=self.instance) is not None:
|
||||
raise forms.ValidationError(
|
||||
_(password_validators_help_texts()),
|
||||
code='pw_invalid'
|
||||
)
|
||||
return password1
|
||||
|
||||
def clean_new_pw_repeat(self):
|
||||
password1 = self.cleaned_data.get('new_pw')
|
||||
password2 = self.cleaned_data.get('new_pw_repeat')
|
||||
if password1 and password1 != password2:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_mismatch'],
|
||||
code='pw_mismatch'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
password1 = self.cleaned_data.get('new_pw')
|
||||
|
||||
if password1:
|
||||
self.instance.set_password(password1)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
@@ -282,4 +282,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
text = text + ' ' + str(_('Your email address has been changed to {email}.').format(email=data['email']))
|
||||
if 'new_pw' in data:
|
||||
text = text + ' ' + str(_('Your password has been changed.'))
|
||||
if data.get('is_active') is True:
|
||||
text = text + ' ' + str(_('Your account has been enabled.'))
|
||||
elif data.get('is_active') is False:
|
||||
text = text + ' ' + str(_('Your account has been disabled.'))
|
||||
return text
|
||||
|
||||
@@ -217,6 +217,15 @@
|
||||
{% trans "Order search" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if request.user.is_superuser %}
|
||||
<li>
|
||||
<a href="{% url 'control:users' %}"
|
||||
{% if "users" in url_name %}class="active"{% endif %}>
|
||||
<i class="fa fa-user fa-fw"></i>
|
||||
{% trans "Users" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for nav in nav_global %}
|
||||
<li>
|
||||
<a href="{{ nav.url }}" {% if nav.active %}class="active"{% endif %}
|
||||
|
||||
29
src/pretix/control/templates/pretixcontrol/users/create.html
Normal file
29
src/pretix/control/templates/pretixcontrol/users/create.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Create user" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Create user" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Base settings" %}</legend>
|
||||
{% bootstrap_field form.is_active layout='control' %}
|
||||
{% bootstrap_field form.fullname layout='control' %}
|
||||
{% bootstrap_field form.locale layout='control' %}
|
||||
{% bootstrap_field form.timezone layout='control' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Log-in settings" %}</legend>
|
||||
{% bootstrap_field form.email layout='control' %}
|
||||
{% bootstrap_field form.new_pw layout='control' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='control' %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
73
src/pretix/control/templates/pretixcontrol/users/form.html
Normal file
73
src/pretix/control/templates/pretixcontrol/users/form.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "User" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "User" %} {{ user.email }}</h1>
|
||||
<p>
|
||||
<form action="{% url "control:users.reset" id=user.pk %}" method="post" class="form-inline helper-display-inline">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default">{% trans "Send password reset email" %}</button>
|
||||
</form>
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-xs-12">
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Base settings" %}</legend>
|
||||
{% bootstrap_field form.is_active layout='control' %}
|
||||
{% bootstrap_field form.fullname layout='control' %}
|
||||
{% bootstrap_field form.locale layout='control' %}
|
||||
{% bootstrap_field form.timezone layout='control' %}
|
||||
{% bootstrap_field form.is_superuser layout='control' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Log-in settings" %}</legend>
|
||||
{% bootstrap_field form.email layout='control' %}
|
||||
{% bootstrap_field form.new_pw layout='control' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='control' %}
|
||||
{% bootstrap_field form.require_2fa layout='control' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Team memberships" %}</legend>
|
||||
<ul>
|
||||
{% for t in teams %}
|
||||
<li>
|
||||
<a href="{% url "control:organizer.team" organizer=t.organizer.slug team=t.pk %}">
|
||||
{% blocktrans trimmed with team=t.name organizer=t.organizer.name %}
|
||||
Team "{{ team }}" of organizer "{{ organizer }}"
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "User history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=user %}
|
||||
<li class="list-group-item logentry">
|
||||
<p class="meta">
|
||||
<span class="fa fa-clock-o"></span> {{ user.date_joined|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "User created." %}
|
||||
</p>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
68
src/pretix/control/templates/pretixcontrol/users/index.html
Normal file
68
src/pretix/control/templates/pretixcontrol/users/index.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Users" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Users" %}</h1>
|
||||
<form class="row filter-form" action="" method="get">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.query layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.status layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.superuser layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
<button class="btn btn-primary btn-block" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
<span class="hidden-md">
|
||||
{% trans "Filter" %}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p>
|
||||
<a href="{% url "control:users.add" %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new user" %}
|
||||
</a>
|
||||
</p>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "E-mail address" %}
|
||||
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Full name" %}
|
||||
<a href="?{% url_replace request 'ordering' '-fullname' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'fullname' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Active" %}</th>
|
||||
<th>{% trans "Administrator" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td><strong>
|
||||
<a href="{% url "control:users.edit" id=u.pk %}">{{ u.email }}</a>
|
||||
</strong></td>
|
||||
<td>{{ u.fullname }}</td>
|
||||
<td>{% if u.is_active %}<span class="fa fa-check-circle"></span>{% endif %}</td>
|
||||
<td>{% if u.is_superuser %}<span class="fa fa-check-circle"></span>{% endif %}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:users.edit" id=u.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,8 @@ from django.conf.urls import include, url
|
||||
|
||||
from pretix.control.views import (
|
||||
auth, checkin, dashboards, event, global_settings, item, main, orders,
|
||||
organizer, search, subevents, typeahead, user, vouchers, waitinglist,
|
||||
organizer, search, subevents, typeahead, user, users, vouchers,
|
||||
waitinglist,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -17,7 +18,11 @@ urlpatterns = [
|
||||
url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
|
||||
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
|
||||
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
|
||||
url(r'^users/$', users.UserListView.as_view(), name='users'),
|
||||
url(r'^users/select2$', typeahead.users_select2, name='users.select2'),
|
||||
url(r'^users/add$', users.UserCreateView.as_view(), name='users.add'),
|
||||
url(r'^users/(?P<id>\d+)/$', users.UserEditView.as_view(), name='users.edit'),
|
||||
url(r'^users/(?P<id>\d+)/reset$', users.UserResetView.as_view(), name='users.reset'),
|
||||
url(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
|
||||
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
|
||||
url(r'^settings/notifications/$', user.UserNotificationsEditView.as_view(), name='user.settings.notifications'),
|
||||
|
||||
@@ -25,8 +25,7 @@ from pretix.base.forms.auth import (
|
||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
||||
)
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
from pretix.base.services.mail import SendMailException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -200,15 +199,7 @@ class Forgot(TemplateView):
|
||||
rc.setex('pretix_pwreset_%s' % (user.id), 3600 * 24, '1')
|
||||
|
||||
try:
|
||||
mail(
|
||||
user.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
|
||||
{
|
||||
'user': user,
|
||||
'url': (build_absolute_uri('control:auth.forgot.recover')
|
||||
+ '?id=%d&token=%s' % (user.id, default_token_generator.make_token(user)))
|
||||
},
|
||||
None, locale=user.locale
|
||||
)
|
||||
user.send_password_reset()
|
||||
except SendMailException:
|
||||
messages.error(request, _('There was an error sending the mail. Please try again later.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
117
src/pretix/control/views/users.py
Normal file
117
src/pretix/control/views/users.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import ListView
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.control.forms.filter import UserFilterForm
|
||||
from pretix.control.forms.users import UserEditForm
|
||||
from pretix.control.permissions import AdministratorPermissionRequiredMixin
|
||||
from pretix.control.views import CreateView, UpdateView
|
||||
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
||||
|
||||
|
||||
class UserListView(AdministratorPermissionRequiredMixin, ListView):
|
||||
template_name = 'pretixcontrol/users/index.html'
|
||||
context_object_name = 'users'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = User.objects.all()
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return UserFilterForm(data=self.request.GET)
|
||||
|
||||
|
||||
class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, UpdateView):
|
||||
template_name = 'pretixcontrol/users/form.html'
|
||||
context_object_name = 'user'
|
||||
form_class = UserEditForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(User, pk=self.kwargs.get("id"))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['teams'] = self.object.teams.select_related('organizer')
|
||||
return ctx
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:users.edit', kwargs=self.kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
|
||||
data = {}
|
||||
for k in form.changed_data:
|
||||
if k != 'new_pw_repeat':
|
||||
if 'new_pw' == k:
|
||||
data['new_pw'] = True
|
||||
else:
|
||||
data[k] = form.cleaned_data[k]
|
||||
|
||||
sup = super().form_valid(form)
|
||||
|
||||
if 'require_2fa' in form.changed_data and form.cleaned_data['require_2fa']:
|
||||
self.object.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
|
||||
elif 'require_2fa' in form.changed_data and not form.cleaned_data['require_2fa']:
|
||||
self.object.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
|
||||
self.object.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
|
||||
|
||||
return sup
|
||||
|
||||
|
||||
class UserResetView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
|
||||
try:
|
||||
self.object.send_password_reset()
|
||||
except SendMailException:
|
||||
messages.error(request, _('There was an error sending the mail. Please try again later.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
self.object.log_action('pretix.control.auth.user.forgot_password.mail_sent',
|
||||
user=request.user)
|
||||
messages.success(request, _('We sent out an e-mail containing further instructions.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:users.edit', kwargs=self.kwargs)
|
||||
|
||||
|
||||
class UserCreateView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, CreateView):
|
||||
template_name = 'pretixcontrol/users/create.html'
|
||||
context_object_name = 'user'
|
||||
form_class = UserEditForm
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
f = super().get_form(form_class)
|
||||
f.fields['new_pw'].required = True
|
||||
f.fields['new_pw_repeat'].required = True
|
||||
return f
|
||||
|
||||
def get_initial(self):
|
||||
i = super().get_initial()
|
||||
i['timezone'] = settings.TIME_ZONE
|
||||
return i
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:users')
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The new user has been created.'))
|
||||
return super().form_valid(form)
|
||||
Reference in New Issue
Block a user