mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
* Upgrade to Django 2.0 * more models * i18n foo * Update setup.py * Fix Sentry exception PRETIXEU-JC * Enforce slug uniqueness * Import sorting * Upgrade to Django 2.1 * Travis config * Try to fix PostgreSQL failure * Smaller test matrix * staticfiles→static * Include request in all authenticate() calls
579 lines
23 KiB
Python
579 lines
23 KiB
Python
from django import forms
|
|
from django.contrib import messages
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.core.files import File
|
|
from django.db import transaction
|
|
from django.db.models import Count
|
|
from django.forms import inlineformset_factory
|
|
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.generic import (
|
|
CreateView, DeleteView, DetailView, FormView, ListView, UpdateView,
|
|
)
|
|
|
|
from pretix.base.models import Organizer, Team, TeamInvite, User
|
|
from pretix.base.models.event import EventMetaProperty
|
|
from pretix.base.models.organizer import TeamAPIToken
|
|
from pretix.base.services.mail import SendMailException, mail
|
|
from pretix.control.forms.filter import OrganizerFilterForm
|
|
from pretix.control.forms.organizer import (
|
|
EventMetaPropertyForm, OrganizerDisplaySettingsForm, OrganizerForm,
|
|
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
|
)
|
|
from pretix.control.permissions import OrganizerPermissionRequiredMixin
|
|
from pretix.control.signals import nav_organizer
|
|
from pretix.control.views import PaginationMixin
|
|
from pretix.helpers.urls import build_absolute_uri
|
|
from pretix.presale.style import regenerate_organizer_css
|
|
|
|
|
|
class OrganizerList(PaginationMixin, ListView):
|
|
model = Organizer
|
|
context_object_name = 'organizers'
|
|
template_name = 'pretixcontrol/organizers/index.html'
|
|
|
|
def get_queryset(self):
|
|
qs = Organizer.objects.all()
|
|
if self.filter_form.is_valid():
|
|
qs = self.filter_form.filter_qs(qs)
|
|
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
|
return qs
|
|
else:
|
|
return qs.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
|
|
|
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 OrganizerFilterForm(data=self.request.GET, request=self.request)
|
|
|
|
|
|
class InviteForm(forms.Form):
|
|
user = forms.EmailField(required=False, label=_('User'))
|
|
|
|
|
|
class TokenForm(forms.Form):
|
|
name = forms.CharField(required=False, label=_('Token name'))
|
|
|
|
|
|
class OrganizerDetailViewMixin:
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['nav_organizer'] = []
|
|
ctx['organizer'] = self.request.organizer
|
|
|
|
for recv, retv in nav_organizer.send(sender=self.request.organizer, request=self.request,
|
|
organizer=self.request.organizer):
|
|
ctx['nav_organizer'] += retv
|
|
ctx['nav_organizer'].sort(key=lambda n: n['label'])
|
|
return ctx
|
|
|
|
def get_object(self, queryset=None) -> Organizer:
|
|
return self.request.organizer
|
|
|
|
|
|
class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
model = Organizer
|
|
template_name = 'pretixcontrol/organizers/detail.html'
|
|
permission = None
|
|
context_object_name = 'organizer'
|
|
|
|
def get_object(self, queryset=None) -> Organizer:
|
|
return self.request.organizer
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['events'] = self.request.organizer.events.all()
|
|
return ctx
|
|
|
|
|
|
class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
model = Organizer
|
|
template_name = 'pretixcontrol/organizers/teams.html'
|
|
permission = 'can_change_permissions'
|
|
context_object_name = 'organizer'
|
|
|
|
|
|
class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
|
model = Organizer
|
|
permission = 'can_change_organizer_settings'
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['obj'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def post(self, request, *args, **kwargs):
|
|
form = self.get_form()
|
|
if form.is_valid():
|
|
form.save()
|
|
if form.has_changed():
|
|
self.request.organizer.log_action(
|
|
'pretix.organizer.settings', user=self.request.user, data={
|
|
k: (form.cleaned_data.get(k).name
|
|
if isinstance(form.cleaned_data.get(k), File)
|
|
else form.cleaned_data.get(k))
|
|
for k in form.changed_data
|
|
}
|
|
)
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
|
return self.get(request)
|
|
|
|
|
|
class OrganizerDisplaySettings(OrganizerSettingsFormView):
|
|
model = Organizer
|
|
form_class = OrganizerDisplaySettingsForm
|
|
template_name = 'pretixcontrol/organizers/display.html'
|
|
permission = 'can_change_organizer_settings'
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:organizer.display', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def post(self, request, *args, **kwargs):
|
|
form = self.get_form()
|
|
if form.is_valid():
|
|
form.save()
|
|
if form.has_changed():
|
|
self.request.organizer.log_action(
|
|
'pretix.organizer.settings', user=self.request.user, data={
|
|
k: (form.cleaned_data.get(k).name
|
|
if isinstance(form.cleaned_data.get(k), File)
|
|
else form.cleaned_data.get(k))
|
|
for k in form.changed_data
|
|
}
|
|
)
|
|
regenerate_organizer_css.apply_async(args=(self.request.organizer.pk,))
|
|
messages.success(self.request, _('Your changes have been saved. Please note that it can '
|
|
'take a short period of time until your changes become '
|
|
'active.'))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
|
return self.get(request)
|
|
|
|
|
|
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = Organizer
|
|
form_class = OrganizerUpdateForm
|
|
template_name = 'pretixcontrol/organizers/edit.html'
|
|
permission = 'can_change_organizer_settings'
|
|
context_object_name = 'organizer'
|
|
|
|
@cached_property
|
|
def object(self) -> Organizer:
|
|
return self.request.organizer
|
|
|
|
def get_object(self, queryset=None) -> Organizer:
|
|
return self.object
|
|
|
|
@cached_property
|
|
def sform(self):
|
|
return OrganizerSettingsForm(
|
|
obj=self.object,
|
|
prefix='settings',
|
|
data=self.request.POST if self.request.method == 'POST' else None,
|
|
files=self.request.FILES if self.request.method == 'POST' else None
|
|
)
|
|
|
|
def get_context_data(self, *args, **kwargs) -> dict:
|
|
context = super().get_context_data(*args, **kwargs)
|
|
context['sform'] = self.sform
|
|
context['formset'] = self.formset
|
|
return context
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
self.save_formset(self.object)
|
|
self.sform.save()
|
|
if self.sform.has_changed():
|
|
self.request.organizer.log_action(
|
|
'pretix.organizer.settings',
|
|
user=self.request.user,
|
|
data={
|
|
k: (self.sform.cleaned_data.get(k).name
|
|
if isinstance(self.sform.cleaned_data.get(k), File)
|
|
else self.sform.cleaned_data.get(k))
|
|
for k in self.sform.changed_data
|
|
}
|
|
)
|
|
if form.has_changed():
|
|
self.request.organizer.log_action(
|
|
'pretix.organizer.changed',
|
|
user=self.request.user,
|
|
data={k: form.cleaned_data.get(k) for k in form.changed_data}
|
|
)
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return super().form_valid(form)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
|
kwargs['domain'] = True
|
|
return kwargs
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:organizer.edit', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
form = self.get_form()
|
|
if form.is_valid() and self.sform.is_valid() and self.formset.is_valid():
|
|
return self.form_valid(form)
|
|
else:
|
|
return self.form_invalid(form)
|
|
|
|
@cached_property
|
|
def formset(self):
|
|
formsetclass = inlineformset_factory(
|
|
Organizer, EventMetaProperty,
|
|
form=EventMetaPropertyForm, can_order=False, can_delete=True, extra=0
|
|
)
|
|
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
|
instance=self.object, queryset=self.object.meta_properties.all())
|
|
|
|
def save_formset(self, obj):
|
|
for form in self.formset.initial_forms:
|
|
if form in self.formset.deleted_forms:
|
|
if not form.instance.pk:
|
|
continue
|
|
form.instance.delete()
|
|
form.instance.pk = None
|
|
elif form.has_changed():
|
|
form.save()
|
|
|
|
for form in self.formset.extra_forms:
|
|
if not form.has_changed():
|
|
continue
|
|
if self.formset._should_delete_form(form):
|
|
continue
|
|
form.instance.organizer = obj
|
|
form.save()
|
|
|
|
|
|
class OrganizerCreate(CreateView):
|
|
model = Organizer
|
|
form_class = OrganizerForm
|
|
template_name = 'pretixcontrol/organizers/create.html'
|
|
context_object_name = 'organizer'
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.has_active_staff_session(self.request.session.session_key):
|
|
raise PermissionDenied() # TODO
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The new organizer has been created.'))
|
|
ret = super().form_valid(form)
|
|
t = Team.objects.create(
|
|
organizer=form.instance, name=_('Administrators'),
|
|
all_events=True, can_create_events=True, can_change_teams=True,
|
|
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
|
|
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
|
|
)
|
|
t.members.add(self.request.user)
|
|
return ret
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:organizers')
|
|
|
|
|
|
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = Team
|
|
template_name = 'pretixcontrol/organizers/teams.html'
|
|
permission = 'can_change_teams'
|
|
context_object_name = 'teams'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.teams.annotate(
|
|
memcount=Count('members', distinct=True),
|
|
eventcount=Count('limit_events', distinct=True),
|
|
invcount=Count('invites', distinct=True)
|
|
).all()
|
|
|
|
|
|
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = Team
|
|
template_name = 'pretixcontrol/organizers/team_edit.html'
|
|
permission = 'can_change_teams'
|
|
form_class = TeamForm
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['organizer'] = self.request.organizer
|
|
return kwargs
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.team', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'team': self.object.pk
|
|
})
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The team has been created. You can now add members to the team.'))
|
|
form.instance.organizer = self.request.organizer
|
|
ret = super().form_valid(form)
|
|
form.instance.members.add(self.request.user)
|
|
form.instance.log_action('pretix.team.created', user=self.request.user, data={
|
|
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
|
for k in form.changed_data
|
|
})
|
|
return ret
|
|
|
|
def form_invalid(self, form):
|
|
messages.error(self.request, _('Your changes could not be saved.'))
|
|
return super().form_invalid(form)
|
|
|
|
|
|
class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = Team
|
|
template_name = 'pretixcontrol/organizers/team_edit.html'
|
|
permission = 'can_change_teams'
|
|
context_object_name = 'team'
|
|
form_class = TeamForm
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['organizer'] = self.request.organizer
|
|
return kwargs
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.team', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'team': self.object.pk
|
|
})
|
|
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.team.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
|
for k in form.changed_data
|
|
})
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return super().form_valid(form)
|
|
|
|
def form_invalid(self, form):
|
|
messages.error(self.request, _('Your changes could not be saved.'))
|
|
return super().form_invalid(form)
|
|
|
|
|
|
class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
|
model = Team
|
|
template_name = 'pretixcontrol/organizers/team_delete.html'
|
|
permission = 'can_change_teams'
|
|
context_object_name = 'team'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.teams', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_context_data(self, *args, **kwargs) -> dict:
|
|
context = super().get_context_data(*args, **kwargs)
|
|
context['possible'] = self.is_allowed()
|
|
return context
|
|
|
|
def is_allowed(self) -> bool:
|
|
return self.request.organizer.teams.exclude(pk=self.kwargs.get('team')).filter(
|
|
can_change_teams=True, members__isnull=False
|
|
).exists()
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
success_url = self.get_success_url()
|
|
self.object = self.get_object()
|
|
if self.is_allowed():
|
|
self.object.log_action('pretix.team.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected team has been deleted.'))
|
|
return redirect(success_url)
|
|
else:
|
|
messages.error(request, _('The selected team cannot be deleted.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
template_name = 'pretixcontrol/organizers/team_members.html'
|
|
context_object_name = 'team'
|
|
permission = 'can_change_teams'
|
|
model = Team
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
|
|
|
@cached_property
|
|
def add_form(self):
|
|
return InviteForm(data=(self.request.POST
|
|
if self.request.method == "POST" and "user" in self.request.POST else None))
|
|
|
|
@cached_property
|
|
def add_token_form(self):
|
|
return TokenForm(data=(self.request.POST
|
|
if self.request.method == "POST" and "name" in self.request.POST else None))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['add_form'] = self.add_form
|
|
ctx['add_token_form'] = self.add_token_form
|
|
return ctx
|
|
|
|
def _send_invite(self, instance):
|
|
try:
|
|
mail(
|
|
instance.email,
|
|
_('pretix account invitation'),
|
|
'pretixcontrol/email/invitation.txt',
|
|
{
|
|
'user': self,
|
|
'organizer': self.request.organizer.name,
|
|
'team': instance.team.name,
|
|
'url': build_absolute_uri('control:auth.invite', kwargs={
|
|
'token': instance.token
|
|
})
|
|
},
|
|
event=None,
|
|
locale=self.request.LANGUAGE_CODE
|
|
)
|
|
except SendMailException:
|
|
pass # Already logged
|
|
|
|
@transaction.atomic
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
|
|
if 'remove-member' in request.POST:
|
|
try:
|
|
user = User.objects.get(pk=request.POST.get('remove-member'))
|
|
except User.DoesNotExist:
|
|
pass
|
|
else:
|
|
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
|
|
can_change_teams=True, members__isnull=False
|
|
).exists()
|
|
if not other_admin_teams and self.object.can_change_teams and self.object.members.count() == 1:
|
|
messages.error(self.request, _('You cannot remove the last member from this team as no one would '
|
|
'be left with the permission to change teams.'))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
self.object.members.remove(user)
|
|
self.object.log_action(
|
|
'pretix.team.member.removed', user=self.request.user, data={
|
|
'email': user.email,
|
|
'user': user.pk
|
|
}
|
|
)
|
|
messages.success(self.request, _('The member has been removed from the team.'))
|
|
return redirect(self.get_success_url())
|
|
|
|
elif 'remove-invite' in request.POST:
|
|
try:
|
|
invite = self.object.invites.get(pk=request.POST.get('remove-invite'))
|
|
except TeamInvite.DoesNotExist:
|
|
messages.error(self.request, _('Invalid invite selected.'))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
invite.delete()
|
|
self.object.log_action(
|
|
'pretix.team.invite.deleted', user=self.request.user, data={
|
|
'email': invite.email
|
|
}
|
|
)
|
|
messages.success(self.request, _('The invite has been revoked.'))
|
|
return redirect(self.get_success_url())
|
|
|
|
elif 'remove-token' in request.POST:
|
|
try:
|
|
token = self.object.tokens.get(pk=request.POST.get('remove-token'))
|
|
except TeamAPIToken.DoesNotExist:
|
|
messages.error(self.request, _('Invalid token selected.'))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
token.active = False
|
|
token.save()
|
|
self.object.log_action(
|
|
'pretix.team.token.deleted', user=self.request.user, data={
|
|
'name': token.name
|
|
}
|
|
)
|
|
messages.success(self.request, _('The token has been revoked.'))
|
|
return redirect(self.get_success_url())
|
|
|
|
elif "user" in self.request.POST and self.add_form.is_valid() and self.add_form.has_changed():
|
|
|
|
try:
|
|
user = User.objects.get(email=self.add_form.cleaned_data['user'])
|
|
except User.DoesNotExist:
|
|
if self.object.invites.filter(email=self.add_form.cleaned_data['user']).exists():
|
|
messages.error(self.request, _('This user already has been invited for this team.'))
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
invite = self.object.invites.create(email=self.add_form.cleaned_data['user'])
|
|
self._send_invite(invite)
|
|
self.object.log_action(
|
|
'pretix.team.invite.created', user=self.request.user, data={
|
|
'email': self.add_form.cleaned_data['user']
|
|
}
|
|
)
|
|
messages.success(self.request, _('The new member has been invited to the team.'))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
if self.object.members.filter(pk=user.pk).exists():
|
|
messages.error(self.request, _('This user already has permissions for this team.'))
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
self.object.members.add(user)
|
|
self.object.log_action(
|
|
'pretix.team.member.added', user=self.request.user,
|
|
data={
|
|
'email': user.email,
|
|
'user': user.pk,
|
|
}
|
|
)
|
|
messages.success(self.request, _('The new member has been added to the team.'))
|
|
return redirect(self.get_success_url())
|
|
|
|
elif "name" in self.request.POST and self.add_token_form.is_valid() and self.add_token_form.has_changed():
|
|
token = self.object.tokens.create(name=self.add_token_form.cleaned_data['name'])
|
|
self.object.log_action(
|
|
'pretix.team.token.created', user=self.request.user, data={
|
|
'name': self.add_token_form.cleaned_data['name'],
|
|
'id': token.pk
|
|
}
|
|
)
|
|
messages.success(self.request, _('A new API token has been created with the following secret: {}\n'
|
|
'Please copy this secret to a safe place. You will not be able to '
|
|
'view it again here.').format(token.token))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
messages.error(self.request, _('Your changes could not be saved.'))
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:organizer.team', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'team': self.object.pk
|
|
})
|