mirror of
https://github.com/pretix/pretix.git
synced 2026-08-08 10:27:49 +00:00
* Improve subject lines for admin-facing emails
A few of the current subjects are ambiguous about the expected
action, and some omit context that would help in an inbox preview
(which event, which address). The rewrites bring them closer to
common conventions in modern transactional email (verb-led,
recipient-addressed, with recipient-meaningful variables). Two
themes:
- Action-required emails lead with the action verb. "Reset your
password", "Confirm event cancellation and bulk refund", and
"Confirm <address> as a sender address" tell the recipient up
front what's expected, where "Password recovery", "Bulk-refund
confirmation" and "Sender address verification" did not.
- Surface the relevant variable when the email is about something
specific. "Data shredding completed for <event>" is more useful
than the generic version when an admin manages several events.
"You've been invited to join <organizer>" names the inviting
organizer. "Confirm <address> as a sender address" names the
address.
The remaining rewrites are lighter rewordings. "New sign-in to
your account" replaces "Login from new source detected" because
"source" is jargon a non-technical recipient wouldn't recognise.
"Changes to your account" replaces "Account information changed"
because the possessive frames the email as being about the
recipient's own account.
Also fixes a hardcoded "pretix" in the confirmation-code subject.
* Standardise admin email sign-offs as "Thanks, The <instance> Team"
The current sign-offs ("Best regards, Your <instance> team") have
a formal tone. A review of the last ~20 transactional emails in
my inbox showed most senders use something friendlier:
- Thanks: Deliveroo, Starling Bank, GitHub, Cloudflare
- Thank you: AWS
- Sincerely: Google Workspace
A small minority (e.g., Sentry) had no sign-off at all. "Thanks"
was the most common, and among that group "The <instance> Team"
was the consistent phrasing rather than "Your <instance> team".
Two templates (cancel_confirm, export_failed) didn't have a
sign-off; they now get one for consistency. Notification emails
are deliberately excluded: they're system alerts rather than
direct correspondence.
* Add anti-phishing notice to admin emails containing confirmation codes
Three admin emails send the recipient a confirmation code to
enter back into a form: confirmation_code, email_setup, and
cancel_confirm. Only confirmation_code had an anti-phishing
warning, and its wording was awkward ("Please do never give this
code to another person. Our support team will never ask for this
code.").
This commit standardises the warning across all three:
> Don't share this code with anyone. The <instance> team will
> never ask you for it.
* Add structured details to login-notice email
The single-sentence body ("The login was performed using <agent>
on <os> from <country>.") is replaced with a labelled bullet list:
Time, Browser, Operating system, Device, Country.
Time and Device are new fields. Device is omitted when ua-parser
can't identify the device, Country when GeoIP isn't available,
so the user only sees fields with real values.
* Restructure notification.txt for clearer layout
- Attributes: bullet list instead of paragraph-per-attribute.
- Actions: label gets a colon, URL on its own paragraph (was
4-space-indented code block).
- Footer: separated by --- and bulleted (manage / disable
links). "Click here X" phrasing dropped (incidentally moots
a missing-"to" typo).
- Minor whitespace fix: detail-block endif now matches the
placement of the rest of the template.
notification.html's footer text is also updated, only to match
the new .txt wording (link labels and intro line). No
structural changes to the HTML template.
* Improve confirmation-code email reason strings
- Drop the redundant "to confirm" opener.
- Replace hardcoded "your pretix account" in email_verify
with "{instance}".
* Polish admin email body copy
A small wording and formatting pass on the admin email bodies,
in three loosely-grouped themes:
1. Sentence case for body text (previously lowercase after
"Hello,"), matching standard English convention.
2. Light restructuring where helpful: bullet lists for sets
of labelled facts; 4-space-indented code blocks for codes
the recipient is meant to type back.
3. Phrasing polish. Some sentences tightened or shortened.
Largely matters of taste, but generally read smoother.
---------
Co-authored-by: Raphael Michel <michel@pretix.eu>
3756 lines
152 KiB
Python
3756 lines
152 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
#
|
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
# this file, see <https://pretix.eu/about/en/license>.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
# <https://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
|
#
|
|
# This file contains Apache-licensed contributions copyrighted by: Bolutife Lawrence, Jakob Schnell, Sohalt
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations under the License.
|
|
|
|
import copy
|
|
import json
|
|
import logging
|
|
import re
|
|
from collections import Counter
|
|
from datetime import time, timedelta
|
|
from decimal import Decimal
|
|
from hashlib import sha1
|
|
from itertools import groupby
|
|
from json import JSONDecodeError
|
|
|
|
import bleach
|
|
import dateutil
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.core.exceptions import (
|
|
BadRequest, PermissionDenied, ValidationError,
|
|
)
|
|
from django.core.files import File
|
|
from django.db import transaction
|
|
from django.db.models import (
|
|
Count, Exists, F, IntegerField, Max, Min, OuterRef, Prefetch,
|
|
ProtectedError, Q, Subquery, Sum,
|
|
)
|
|
from django.db.models.functions import Coalesce, Greatest
|
|
from django.forms import DecimalField
|
|
from django.http import (
|
|
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
|
)
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.urls import NoReverseMatch, reverse
|
|
from django.utils.formats import date_format
|
|
from django.utils.functional import cached_property
|
|
from django.utils.html import format_html
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.timezone import get_current_timezone, now
|
|
from django.utils.translation import gettext, gettext_lazy as _
|
|
from django.views import View
|
|
from django.views.decorators.http import require_http_methods
|
|
from django.views.generic import (
|
|
CreateView, DetailView, FormView, ListView, TemplateView, UpdateView,
|
|
)
|
|
from django.views.generic.detail import SingleObjectMixin
|
|
|
|
from pretix.api.models import ApiCall, WebHook
|
|
from pretix.api.webhooks import manually_retry_all_calls
|
|
from pretix.base.auth import get_auth_backends
|
|
from pretix.base.channels import get_all_sales_channel_types
|
|
from pretix.base.exporter import (
|
|
MultiSheetListExporter, OrganizerLevelExportMixin,
|
|
)
|
|
from pretix.base.i18n import language
|
|
from pretix.base.models import (
|
|
CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry,
|
|
Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer,
|
|
ReusableMedium, ScheduledOrganizerExport, Team, TeamInvite, User,
|
|
)
|
|
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
|
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
|
from pretix.base.models.giftcards import (
|
|
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
|
|
)
|
|
from pretix.base.models.orders import CancellationRequest
|
|
from pretix.base.models.organizer import (
|
|
SalesChannel, TeamAPIToken, TeamQuerySet,
|
|
)
|
|
from pretix.base.payment import PaymentException
|
|
from pretix.base.plugins import (
|
|
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
|
PLUGIN_LEVEL_ORGANIZER, plugin_is_available,
|
|
)
|
|
from pretix.base.services.export import (
|
|
init_organizer_exporters, multiexport, scheduled_organizer_export,
|
|
)
|
|
from pretix.base.services.mail import mail, prefix_subject
|
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
|
from pretix.base.views.tasks import AsyncAction
|
|
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
|
from pretix.control.forms.filter import (
|
|
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
|
|
OrganizerFilterForm, ReusableMediaFilterForm, TeamFilterForm,
|
|
)
|
|
from pretix.control.forms.orders import ExporterForm
|
|
from pretix.control.forms.organizer import (
|
|
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
|
|
EventMetaPropertyAllowedValueFormSet, EventMetaPropertyForm, GateForm,
|
|
GiftCardAcceptanceInviteForm, GiftCardCreateForm, GiftCardUpdateForm,
|
|
KnownDomainFormset, MailSettingsForm, MembershipTypeForm,
|
|
MembershipUpdateForm, OrganizerDeleteForm, OrganizerFooterLinkFormset,
|
|
OrganizerForm, OrganizerPluginEventsForm, OrganizerSettingsForm,
|
|
OrganizerUpdateForm, ReusableMediumCreateForm, ReusableMediumUpdateForm,
|
|
SalesChannelForm, SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
|
|
)
|
|
from pretix.control.forms.rrule import RRuleForm
|
|
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
|
from pretix.control.permissions import (
|
|
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
|
|
organizer_permission_required,
|
|
)
|
|
from pretix.control.signals import nav_organizer
|
|
from pretix.control.views import PaginationMixin
|
|
from pretix.control.views.mailsetup import MailSettingsSetupView
|
|
from pretix.helpers import OF_SELF, GroupConcat
|
|
from pretix.helpers.compat import CompatDeleteView
|
|
from pretix.helpers.dicts import merge_dicts
|
|
from pretix.helpers.format import SafeFormatter, format_map
|
|
from pretix.helpers.urls import mainreverse_absolute
|
|
from pretix.multidomain.urlreverse import eventreverse_absolute
|
|
from pretix.presale.forms.customer import TokenGenerator
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
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, ListView):
|
|
model = Event
|
|
template_name = 'pretixcontrol/organizers/detail.html'
|
|
permission = None
|
|
context_object_name = 'events'
|
|
paginate_by = 50
|
|
|
|
@property
|
|
def organizer(self):
|
|
return self.request.organizer
|
|
|
|
def get_queryset(self):
|
|
qs = self.request.user.get_events_with_any_permission(self.request).select_related(
|
|
'organizer').prefetch_related(
|
|
'organizer', '_settings_objects', 'organizer___settings_objects',
|
|
'organizer__meta_properties',
|
|
'limit_sales_channels',
|
|
Prefetch(
|
|
'meta_values',
|
|
EventMetaValue.objects.select_related('property'),
|
|
to_attr='meta_values_cached'
|
|
)
|
|
).filter(organizer=self.request.organizer).order_by('-date_from')
|
|
qs = qs.annotate(
|
|
min_from=Min('subevents__date_from'),
|
|
max_from=Max('subevents__date_from'),
|
|
max_to=Max('subevents__date_to'),
|
|
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
|
).annotate(
|
|
order_from=Coalesce('max_from', 'date_from'),
|
|
order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to', 'date_from'),
|
|
).order_by("-order_from")
|
|
if self.filter_form.is_valid():
|
|
qs = self.filter_form.filter_qs(qs)
|
|
return qs
|
|
|
|
@cached_property
|
|
def filter_form(self):
|
|
return EventFilterForm(data=self.request.GET, request=self.request, organizer=self.organizer)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['filter_form'] = self.filter_form
|
|
ctx['meta_fields'] = [
|
|
self.filter_form['meta_{}'.format(p.name)] for p in
|
|
self.organizer.meta_properties.filter(filter_allowed=True)
|
|
]
|
|
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
|
|
return ctx
|
|
|
|
|
|
class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
model = Organizer
|
|
template_name = 'pretixcontrol/organizers/teams.html'
|
|
permission = 'organizer.teams:write'
|
|
context_object_name = 'organizer'
|
|
|
|
|
|
class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
|
model = Organizer
|
|
permission = 'organizer.settings.general:write'
|
|
|
|
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 OrganizerMailSettings(OrganizerSettingsFormView):
|
|
form_class = MailSettingsForm
|
|
template_name = 'pretixcontrol/organizers/mail.html'
|
|
permission = 'organizer.settings.general:write'
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.settings.mail', 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) 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 MailSettingsSetup(OrganizerPermissionRequiredMixin, MailSettingsSetupView):
|
|
permission = 'organizer.settings.general:write'
|
|
basetpl = 'pretixcontrol/base.html'
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.settings.mail', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def log_action(self, data):
|
|
self.request.organizer.log_action(
|
|
'pretix.organizer.settings', user=self.request.user, data=data
|
|
)
|
|
|
|
|
|
class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
|
permission = 'organizer.settings.general:write'
|
|
|
|
# return the origin text if key is missing in dict
|
|
class SafeDict(dict):
|
|
def __missing__(self, key):
|
|
return '{' + key + '}'
|
|
|
|
# create index-language mapping
|
|
@cached_property
|
|
def supported_locale(self):
|
|
locales = {}
|
|
for idx, val in enumerate(settings.LANGUAGES):
|
|
if val[0] in self.request.organizer.settings.locales:
|
|
locales[str(idx)] = val[0]
|
|
return locales
|
|
|
|
# get all supported placeholders with dummy values
|
|
def placeholders(self, item):
|
|
ctx = {}
|
|
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(
|
|
MailSettingsForm.base_context[item]).items():
|
|
if s.strip().startswith('*'):
|
|
ctx[p] = s
|
|
else:
|
|
ctx[p] = '<span class="placeholder" title="{}">{}</span>'.format(
|
|
_('This value will be replaced based on dynamic parameters.'),
|
|
s
|
|
)
|
|
return self.SafeDict(ctx)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
preview_item = request.POST.get('item', '')
|
|
if preview_item not in MailSettingsForm.base_context:
|
|
return HttpResponseBadRequest(_('invalid item'))
|
|
|
|
regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d]+)$"
|
|
msgs = {}
|
|
for k, v in request.POST.items():
|
|
# only accept allowed fields
|
|
matched = re.search(regex, k)
|
|
if matched is not None:
|
|
idx = matched.group('idx')
|
|
if idx in self.supported_locale:
|
|
with language(self.supported_locale[idx], self.request.organizer.settings.region):
|
|
if k.startswith('mail_subject_'):
|
|
msgs[self.supported_locale[idx]] = prefix_subject(
|
|
self.request.organizer,
|
|
format_map(bleach.clean(v), self.placeholders(preview_item)),
|
|
highlight=True,
|
|
)
|
|
else:
|
|
placeholders = self.placeholders(preview_item)
|
|
msgs[self.supported_locale[idx]] = format_map(markdown_compile_email(
|
|
format_map(v, placeholders)
|
|
), placeholders, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
|
|
|
return JsonResponse({
|
|
'item': preview_item,
|
|
'msgs': msgs
|
|
})
|
|
|
|
|
|
class OrganizerDisplaySettings(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, View):
|
|
permission = None
|
|
|
|
def get(self, request, *wargs, **kwargs):
|
|
return redirect(reverse('control:organizer.edit', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
}) + '#tab-0-3-open')
|
|
|
|
|
|
class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
|
|
model = Organizer
|
|
template_name = 'pretixcontrol/organizers/delete.html'
|
|
context_object_name = 'organizer'
|
|
form_class = OrganizerDeleteForm
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
if not self.request.organizer.allow_delete():
|
|
messages.error(self.request, _('This organizer can not be deleted.'))
|
|
return self.get(self.request, *self.args, **self.kwargs)
|
|
return super().post(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['organizer'] = self.request.organizer
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
try:
|
|
with transaction.atomic():
|
|
self.request.user.log_action(
|
|
'pretix.organizer.deleted', user=self.request.user,
|
|
data={
|
|
'organizer_id': self.request.organizer.pk,
|
|
'name': str(self.request.organizer.name),
|
|
'logentries': list(self.request.organizer.all_logentries().values_list('pk', flat=True))
|
|
}
|
|
)
|
|
self.request.organizer.delete_sub_objects()
|
|
self.request.organizer.delete()
|
|
self.request.organizer = None
|
|
messages.success(self.request, _('The organizer has been deleted.'))
|
|
return redirect(self.get_success_url())
|
|
except ProtectedError as e:
|
|
err = gettext(
|
|
'The organizer could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
|
|
|
|
# Unlike deleting events (which is done by regular backend users), this feature can only be used by sysadmins,
|
|
# so we expose more technical / less polished information.
|
|
affected_models = set()
|
|
for m in e.protected_objects:
|
|
affected_models.add(type(m)._meta.label)
|
|
|
|
if affected_models:
|
|
err += ' ' + gettext(
|
|
'The following database models still contain data that cannot be deleted automatically: {affected_models}'
|
|
).format(
|
|
affected_models=', '.join(list(affected_models))
|
|
)
|
|
|
|
messages.error(self.request, err)
|
|
return self.get(self.request, *self.args, **self.kwargs)
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:index')
|
|
|
|
|
|
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = Organizer
|
|
form_class = OrganizerUpdateForm
|
|
template_name = 'pretixcontrol/organizers/edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
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 domain_config(self):
|
|
return self.request.user.has_active_staff_session(self.request.session.session_key)
|
|
|
|
@cached_property
|
|
def sform(self):
|
|
return OrganizerSettingsForm(
|
|
obj=self.object,
|
|
prefix='settings',
|
|
is_admin=self.request.user.has_active_staff_session(self.request.session.session_key),
|
|
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['footer_links_formset'] = self.footer_links_formset
|
|
if self.domain_config:
|
|
context['domain_formset'] = self.domain_formset
|
|
return context
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
self.sform.save()
|
|
self.save_footer_links_formset(self.object)
|
|
self.object.cache.clear()
|
|
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 self.footer_links_formset.has_changed():
|
|
self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={
|
|
'data': self.footer_links_formset.cleaned_data
|
|
})
|
|
if self.domain_config and self.domain_formset.has_changed():
|
|
self._save_domain_config()
|
|
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 _save_domain_config(self):
|
|
for form in self.domain_formset.initial_forms:
|
|
if form.instance.pk and form.has_changed():
|
|
self.object.domains.get(pk=form.instance.pk).log_delete(self.request.user)
|
|
self.domain_formset.save()
|
|
for new_obj in self.domain_formset.new_objects:
|
|
new_obj.log_create(self.request.user)
|
|
for ch_obj, form in self.domain_formset.changed_objects:
|
|
ch_obj.log_create(self.request.user)
|
|
self.request.organizer.cache.clear()
|
|
for ev in self.request.organizer.events.all():
|
|
ev.cache.clear()
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
|
kwargs['change_slug'] = 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.footer_links_formset.is_valid() and (not self.domain_config or self.domain_formset.is_valid()):
|
|
return self.form_valid(form)
|
|
else:
|
|
return self.form_invalid(form)
|
|
|
|
@cached_property
|
|
def footer_links_formset(self):
|
|
return OrganizerFooterLinkFormset(self.request.POST if self.request.method == "POST" else None,
|
|
organizer=self.object,
|
|
prefix="footer-links", instance=self.object)
|
|
|
|
@cached_property
|
|
def domain_formset(self):
|
|
return KnownDomainFormset(self.request.POST if self.request.method == "POST" else None, prefix="domains",
|
|
instance=self.object, organizer=self.object)
|
|
|
|
def save_footer_links_formset(self, obj):
|
|
self.footer_links_formset.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, all_event_permissions=True, all_organizer_permissions=True,
|
|
)
|
|
t.members.add(self.request.user)
|
|
return ret
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:organizer', kwargs={
|
|
'organizer': self.object.slug,
|
|
})
|
|
|
|
|
|
def available_plugins(organizer):
|
|
from pretix.base.plugins import get_all_plugins
|
|
|
|
return (p for p in get_all_plugins(organizer=organizer) if not p.name.startswith('.')
|
|
and getattr(p, 'visible', True))
|
|
|
|
|
|
class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
|
model = Organizer
|
|
context_object_name = 'organizer'
|
|
permission = 'organizer.settings.general:write'
|
|
template_name = 'pretixcontrol/organizers/plugins.html'
|
|
|
|
def get_object(self, queryset=None) -> Organizer:
|
|
return self.request.organizer
|
|
|
|
def prepare_links(self, pluginmeta, key):
|
|
links = getattr(pluginmeta, key, [])
|
|
try:
|
|
result = []
|
|
for linktext, urlname, kwargs in links:
|
|
try:
|
|
result.append((
|
|
reverse(urlname, kwargs={"organizer": self.request.organizer.slug}),
|
|
" > ".join(map(str, linktext)) if isinstance(linktext, tuple) else linktext,
|
|
))
|
|
except NoReverseMatch:
|
|
if pluginmeta.level != PLUGIN_LEVEL_ORGANIZER:
|
|
# Ignore, link might be for another level
|
|
pass
|
|
else:
|
|
raise
|
|
return result
|
|
except:
|
|
logger.exception('Failed to resolve settings links.')
|
|
return []
|
|
|
|
def get_context_data(self, *args, **kwargs) -> dict:
|
|
from pretix.base.plugins import CATEGORY_LABELS, CATEGORY_ORDER
|
|
|
|
context = super().get_context_data(*args, **kwargs)
|
|
plugins = list(available_plugins(self.object))
|
|
|
|
active_counter = Counter()
|
|
events_total = 0
|
|
for e in self.object.events.only("plugins").iterator():
|
|
events_total += 1
|
|
for p in e.get_plugins():
|
|
active_counter[p] += 1
|
|
plugins_grouped = groupby(
|
|
sorted(
|
|
plugins,
|
|
key=lambda p: (
|
|
str(getattr(p, 'category', _('Other'))),
|
|
(0 if getattr(p, 'featured', False) else 1),
|
|
str(p.name).lower().replace('pretix ', '')
|
|
),
|
|
),
|
|
lambda p: str(getattr(p, 'category', _('Other')))
|
|
)
|
|
plugins_grouped = [(c, list(plist)) for c, plist in plugins_grouped]
|
|
|
|
active_plugins = self.object.get_plugins()
|
|
|
|
def plugin_details(plugin):
|
|
is_active = plugin.module in active_plugins
|
|
events_counter = active_counter[plugin.module]
|
|
settings_links = self.prepare_links(plugin, 'settings_links') if is_active else None
|
|
navigation_links = self.prepare_links(plugin, 'navigation_links') if is_active else None
|
|
return plugin, is_active, settings_links, navigation_links, events_counter
|
|
|
|
context['plugins'] = sorted([
|
|
(c, CATEGORY_LABELS.get(c, c), map(plugin_details, plist), any(getattr(p, 'picture', None) for p in plist))
|
|
for c, plist
|
|
in plugins_grouped
|
|
], key=lambda c: (CATEGORY_ORDER.index(c[0]), c[1]) if c[0] in CATEGORY_ORDER else (999, str(c[1])))
|
|
context['show_meta'] = settings.PRETIX_PLUGINS_SHOW_META
|
|
context['events_total'] = events_total
|
|
return context
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
context = self.get_context_data(object=self.object)
|
|
return self.render_to_response(context)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
|
|
plugins_available = {
|
|
p.module: p for p in available_plugins(self.object)
|
|
}
|
|
choose_events_next = False
|
|
with transaction.atomic():
|
|
for key, value in request.POST.items():
|
|
if key.startswith("plugin:"):
|
|
module = key.split(":")[1]
|
|
if value == "enable" and module in plugins_available:
|
|
pluginmeta = plugins_available[module]
|
|
if getattr(pluginmeta, 'restricted', False):
|
|
if module not in request.organizer.settings.allowed_restricted_plugins:
|
|
continue
|
|
|
|
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
|
|
if level not in (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID):
|
|
continue
|
|
|
|
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
|
choose_events_next = module
|
|
|
|
self.object.log_action('pretix.organizer.plugins.enabled', user=self.request.user,
|
|
data={'plugin': module})
|
|
self.object.enable_plugin(module, allow_restricted=request.organizer.settings.allowed_restricted_plugins)
|
|
|
|
links = self.prepare_links(pluginmeta, 'settings_links')
|
|
if links:
|
|
info = [
|
|
'<p>',
|
|
format_html(_('The plugin {} is now active, you can configure it here:'),
|
|
format_html("<strong>{}</strong>", pluginmeta.name)),
|
|
'</p><p>',
|
|
] + [
|
|
format_html('<a href="{}" class="btn btn-default">{}</a> ', url, text)
|
|
for url, text in links
|
|
] + ['</p>']
|
|
else:
|
|
info = [
|
|
format_html(_('The plugin {} is now active.'),
|
|
format_html("<strong>{}</strong>", pluginmeta.name)),
|
|
]
|
|
messages.success(self.request, mark_safe("".join(info)))
|
|
elif value == "disable" and module in plugins_available:
|
|
pluginmeta = plugins_available[module]
|
|
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
|
|
if level not in (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID):
|
|
continue
|
|
|
|
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
|
events_to_disable = set(self.request.organizer.events.filter(
|
|
plugins__regex='(^|,)' + module + '(,|$)'
|
|
).values_list("pk", flat=True))
|
|
logentries_to_save = []
|
|
events_to_save = []
|
|
|
|
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
|
|
logentries_to_save.append(
|
|
e.log_action('pretix.event.plugins.disabled', user=self.request.user,
|
|
data={'plugin': module}, save=False)
|
|
)
|
|
e.disable_plugin(module)
|
|
events_to_save.append(e)
|
|
|
|
Event.objects.bulk_update(events_to_save, fields=["plugins"])
|
|
LogEntry.objects.bulk_create(logentries_to_save)
|
|
|
|
self.object.log_action('pretix.organizer.plugins.disabled', user=self.request.user,
|
|
data={'plugin': module})
|
|
self.object.disable_plugin(module)
|
|
messages.success(self.request, _('The plugin has been disabled.'))
|
|
self.object.save()
|
|
if choose_events_next:
|
|
return redirect(reverse('control:organizer.settings.plugin-events', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'plugin': choose_events_next,
|
|
}))
|
|
else:
|
|
return redirect(self.get_success_url())
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:organizer.settings.plugins', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
|
|
class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
|
model = Organizer
|
|
context_object_name = 'organizer'
|
|
permission = 'organizer.settings.general:write'
|
|
template_name = 'pretixcontrol/organizers/plugin_events.html'
|
|
form_class = OrganizerPluginEventsForm
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["events"] = self.request.user.get_events_with_permission(
|
|
"event.settings.general:write", request=self.request
|
|
).filter(organizer=self.request.organizer)
|
|
kwargs["initial"] = {
|
|
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
|
|
}
|
|
return kwargs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
return super().get_context_data(
|
|
plugin=self.plugin,
|
|
**kwargs
|
|
)
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
try:
|
|
self.plugin = next(p for p in available_plugins(self.request.organizer) if p.module == kwargs["plugin"])
|
|
except StopIteration:
|
|
raise Http404(_("Unknown plugin."))
|
|
level = getattr(self.plugin, "level", PLUGIN_LEVEL_EVENT)
|
|
if level == PLUGIN_LEVEL_ORGANIZER:
|
|
raise Http404(_("This plugin can only be enabled for the entire organizer account."))
|
|
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and self.plugin.module not in self.request.organizer.get_plugins():
|
|
raise Http404(_("This plugin is currently not active on the organizer account."))
|
|
|
|
if getattr(self.plugin, 'restricted', False):
|
|
if self.plugin.module not in request.organizer.settings.allowed_restricted_plugins:
|
|
raise Http404(_("This plugin is currently not allowed for this organizer account."))
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self) -> str:
|
|
return reverse('control:organizer.settings.plugins', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic()
|
|
def form_valid(self, form):
|
|
enabled_events_before = set(
|
|
self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)').values_list("pk", flat=True)
|
|
)
|
|
enabled_events_now = {e.pk for e in form.cleaned_data["events"]}
|
|
|
|
events_to_enable = enabled_events_now - enabled_events_before
|
|
events_to_disable = enabled_events_before - enabled_events_now
|
|
events_to_save = []
|
|
logentries_to_save = []
|
|
|
|
for e in self.request.organizer.events.filter(pk__in=events_to_enable):
|
|
if not plugin_is_available(self.plugin, organizer=self.request.organizer, event=e):
|
|
messages.warning(self.request, _("This plugin cannot be activated for event {}.").format(e.name))
|
|
continue
|
|
logentries_to_save.append(
|
|
e.log_action('pretix.event.plugins.enabled', user=self.request.user, data={'plugin': self.plugin.module}, save=False)
|
|
)
|
|
e.enable_plugin(self.plugin.module, allow_restricted=self.request.organizer.settings.allowed_restricted_plugins)
|
|
events_to_save.append(e)
|
|
|
|
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
|
|
logentries_to_save.append(
|
|
e.log_action('pretix.event.plugins.disabled', user=self.request.user, data={'plugin': self.plugin.module}, save=False)
|
|
)
|
|
e.disable_plugin(self.plugin.module)
|
|
events_to_save.append(e)
|
|
|
|
Event.objects.bulk_update(events_to_save, fields=["plugins"])
|
|
LogEntry.objects.bulk_create(logentries_to_save)
|
|
messages.success(self.request, _("Your changes have been saved."))
|
|
return super().form_valid(form)
|
|
|
|
|
|
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|
model = Team
|
|
template_name = 'pretixcontrol/organizers/teams.html'
|
|
permission = 'organizer.teams:write'
|
|
context_object_name = 'teams'
|
|
|
|
def get_queryset(self):
|
|
qs = self.request.organizer.teams.annotate(
|
|
memcount=Count('members', distinct=True),
|
|
eventcount=Count('limit_events', distinct=True),
|
|
invcount=Count('invites', distinct=True)
|
|
).all().order_by('name', 'pk')
|
|
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 TeamFilterForm(data=self.request.GET, request=self.request)
|
|
|
|
|
|
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = Team
|
|
template_name = 'pretixcontrol/organizers/team_edit.html'
|
|
permission = 'organizer.teams:write'
|
|
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
|
|
})
|
|
|
|
@transaction.atomic
|
|
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=form.changed_data_for_log)
|
|
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 = 'organizer.teams:write'
|
|
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
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.team.changed', user=self.request.user, data=form.changed_data_for_log)
|
|
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, CompatDeleteView):
|
|
model = Team
|
|
template_name = 'pretixcontrol/organizers/team_delete.html'
|
|
permission = 'organizer.teams:write'
|
|
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(
|
|
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
|
|
members__isnull=False
|
|
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
success_url = self.get_success_url()
|
|
self.object = self.get_object()
|
|
if not self.is_allowed():
|
|
messages.error(request, _('The selected team cannot be deleted.'))
|
|
return redirect(success_url)
|
|
|
|
try:
|
|
self.object.log_action('pretix.team.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
except ProtectedError as e:
|
|
is_logs = any(isinstance(e, LogEntry) for e in e.protected_objects)
|
|
if is_logs:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"The team could not be deleted because the team or one of its API tokens is part of "
|
|
"historical audit logs."
|
|
)
|
|
)
|
|
else:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
'The team could not be deleted as some constraints (e.g. data created by '
|
|
'plug-ins) do not allow it.'
|
|
)
|
|
)
|
|
return redirect(success_url)
|
|
|
|
messages.success(request, _('The selected team has been deleted.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
template_name = 'pretixcontrol/organizers/team_members.html'
|
|
context_object_name = 'team'
|
|
permission = 'organizer.teams:write'
|
|
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
|
|
ctx['tokens'] = self.object.tokens.order_by("-active", "name", "pk")
|
|
return ctx
|
|
|
|
def _send_invite(self, instance):
|
|
mail(
|
|
instance.email,
|
|
gettext('You\'ve been invited to join %(organizer)s') % {
|
|
'organizer': self.request.organizer.name,
|
|
},
|
|
'pretixcontrol/email/invitation.txt',
|
|
{
|
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
|
'user': self,
|
|
'organizer': self.request.organizer.name,
|
|
'team': instance.team.name,
|
|
'url': mainreverse_absolute('control:auth.invite', kwargs={
|
|
'token': instance.token
|
|
})
|
|
},
|
|
event=None,
|
|
locale=self.request.LANGUAGE_CODE
|
|
)
|
|
|
|
@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, ValueError):
|
|
pass
|
|
else:
|
|
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
|
|
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
|
|
members__isnull=False
|
|
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
|
|
if not other_admin_teams and self.object.has_organizer_permission("organizer.teams:write") 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, ValueError):
|
|
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 'resend-invite' in request.POST:
|
|
try:
|
|
invite = self.object.invites.get(pk=request.POST.get('resend-invite'))
|
|
except (TeamInvite.DoesNotExist, ValueError):
|
|
messages.error(self.request, _('Invalid invite selected.'))
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
self._send_invite(invite)
|
|
self.object.log_action(
|
|
'pretix.team.invite.resent', user=self.request.user, data={
|
|
'email': invite.email
|
|
}
|
|
)
|
|
messages.success(self.request, _('The invite has been resent.'))
|
|
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, ValueError):
|
|
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__iexact=self.add_form.cleaned_data['user'])
|
|
except User.DoesNotExist:
|
|
if self.object.invites.filter(email__iexact=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)
|
|
if 'native' not in get_auth_backends():
|
|
messages.error(self.request, _('Users need to have a pretix account before they can be invited.'))
|
|
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
|
|
})
|
|
|
|
|
|
class DeviceQueryMixin:
|
|
|
|
@cached_property
|
|
def request_data(self):
|
|
if self.request.method == "POST":
|
|
d = self.request.POST
|
|
else:
|
|
d = self.request.GET
|
|
d = d.copy()
|
|
d.setdefault('state', 'active')
|
|
return d
|
|
|
|
@cached_property
|
|
def filter_form(self):
|
|
return DeviceFilterForm(
|
|
data=self.request_data,
|
|
request=self.request,
|
|
)
|
|
|
|
def get_queryset(self):
|
|
qs = self.request.organizer.devices.prefetch_related(
|
|
'limit_events', 'gate',
|
|
).order_by('revoked', '-device_id')
|
|
|
|
if 'device' in self.request_data and '__ALL' not in self.request_data:
|
|
qs = qs.filter(
|
|
id__in=self.request_data.getlist('device')
|
|
)
|
|
elif self.request.method == 'GET' or '__ALL' in self.request_data:
|
|
if self.filter_form.is_valid():
|
|
qs = self.filter_form.filter_qs(qs)
|
|
else:
|
|
raise BadRequest("No devices selected")
|
|
|
|
return qs
|
|
|
|
|
|
class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = Device
|
|
template_name = 'pretixcontrol/organizers/devices.html'
|
|
permission = 'organizer.devices:read'
|
|
context_object_name = 'devices'
|
|
paginate_by = 100
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['filter_form'] = self.filter_form
|
|
return ctx
|
|
|
|
|
|
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = Device
|
|
template_name = 'pretixcontrol/organizers/device_edit.html'
|
|
permission = 'organizer.devices:write'
|
|
form_class = DeviceForm
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['organizer'] = self.request.organizer
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.device.connect', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'device': self.object.pk
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
form.instance.organizer = self.request.organizer
|
|
ret = super().form_valid(form)
|
|
form.instance.log_action('pretix.device.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 DeviceLogView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
template_name = 'pretixcontrol/organizers/device_logs.html'
|
|
permission = 'organizer.devices:read'
|
|
model = LogEntry
|
|
context_object_name = 'logs'
|
|
paginate_by = 20
|
|
|
|
@cached_property
|
|
def device(self):
|
|
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['device'] = self.device
|
|
return ctx
|
|
|
|
def get_queryset(self):
|
|
qs = LogEntry.objects.filter(
|
|
device_id=self.device
|
|
).select_related(
|
|
'user', 'content_type', 'api_token', 'oauth_application',
|
|
).prefetch_related(
|
|
'device', 'event'
|
|
).order_by('-datetime')
|
|
return qs
|
|
|
|
|
|
class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = Device
|
|
template_name = 'pretixcontrol/organizers/device_edit.html'
|
|
permission = 'organizer.devices:write'
|
|
context_object_name = 'device'
|
|
form_class = DeviceForm
|
|
|
|
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(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.devices', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.device.changed', user=self.request.user, data={
|
|
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in form.cleaned_data[k]]
|
|
for k in form.changed_data
|
|
})
|
|
|
|
# If the permission of the device have changed, let's clear "permission denied" errors from the idempotency store
|
|
auth_hash_parts = f'Device {self.object.api_token}:'
|
|
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
|
ApiCall.objects.filter(
|
|
auth_hash=auth_hash,
|
|
response_code=403,
|
|
).delete()
|
|
|
|
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 DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
|
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
|
|
permission = 'organizer.devices:write'
|
|
context_object_name = 'device'
|
|
form_class = DeviceBulkEditForm
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().prefetch_related(None).order_by()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
return HttpResponse(status=405)
|
|
|
|
@cached_property
|
|
def is_submitted(self):
|
|
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
|
|
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
|
|
# that behaviour
|
|
return '_bulk' in self.request.POST
|
|
|
|
def get_form_kwargs(self):
|
|
initial = {}
|
|
mixed_values = set()
|
|
qs = self.get_queryset().annotate(
|
|
limit_events_list=Subquery(
|
|
Device.limit_events.through.objects.filter(
|
|
device_id=OuterRef('pk')
|
|
).order_by().values('device_id').annotate(
|
|
g=GroupConcat('event_id', separator=',', ordered=True)
|
|
).values('g')
|
|
)
|
|
)
|
|
|
|
fields = {
|
|
'all_events': 'all_events',
|
|
'limit_events': 'limit_events_list',
|
|
'security_profile': 'security_profile',
|
|
'gate': 'gate',
|
|
}
|
|
for k, f in fields.items():
|
|
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
|
|
if len(existing_values) == 1:
|
|
if k == 'limit_events':
|
|
if existing_values[0][f]:
|
|
initial[k] = self.request.organizer.events.filter(id__in=existing_values[0][f].split(","))
|
|
else:
|
|
initial[k] = []
|
|
else:
|
|
initial[k] = existing_values[0][f]
|
|
elif len(existing_values) > 1:
|
|
mixed_values.add(k)
|
|
initial[k] = None
|
|
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['organizer'] = self.request.organizer
|
|
kwargs['prefix'] = 'bulkedit'
|
|
kwargs['initial'] = initial
|
|
kwargs['queryset'] = self.get_queryset()
|
|
kwargs['mixed_values'] = mixed_values
|
|
if not self.is_submitted:
|
|
kwargs['data'] = None
|
|
kwargs['files'] = None
|
|
return kwargs
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.devices', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic()
|
|
def form_valid(self, form):
|
|
log_entries = []
|
|
|
|
# Main form
|
|
form.save()
|
|
data = {
|
|
k: (v if k != 'limit_events' else [e.id for e in v])
|
|
for k, v in form.cleaned_data.items()
|
|
if k in form.changed_data
|
|
}
|
|
data['_raw_bulk_data'] = self.request.POST.dict()
|
|
for obj in self.get_queryset():
|
|
log_entries.append(
|
|
obj.log_action('pretix.device.changed', data=data, user=self.request.user, save=False)
|
|
)
|
|
|
|
LogEntry.bulk_create_and_postprocess(log_entries)
|
|
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['devices'] = self.get_queryset()
|
|
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
|
|
return ctx
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
form = self.get_form()
|
|
is_valid = (
|
|
self.is_submitted and
|
|
form.is_valid()
|
|
)
|
|
if is_valid:
|
|
return self.form_valid(form)
|
|
else:
|
|
if self.is_submitted:
|
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
|
return self.form_invalid(form)
|
|
|
|
|
|
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
model = Device
|
|
template_name = 'pretixcontrol/organizers/device_connect.html'
|
|
permission = 'organizer.devices:write'
|
|
context_object_name = 'device'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
if 'ajax' in request.GET:
|
|
return JsonResponse({
|
|
'initialized': bool(self.object.initialized)
|
|
})
|
|
if self.object.initialized:
|
|
messages.success(request, _('This device has been set up successfully.'))
|
|
return redirect(reverse('control:organizer.devices', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
}))
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['qrdata'] = json.dumps({
|
|
'handshake_version': 1,
|
|
'url': settings.SITE_URL,
|
|
'token': self.object.initialization_token,
|
|
})
|
|
return ctx
|
|
|
|
|
|
class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
model = Device
|
|
template_name = 'pretixcontrol/organizers/device_revoke.html'
|
|
permission = 'organizer.devices:write'
|
|
context_object_name = 'device'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
if self.object.revoked:
|
|
messages.success(request, _('This device currently does not have access.'))
|
|
return redirect(reverse('control:organizer.devices', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
}))
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
@transaction.atomic
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
self.object.revoked = True
|
|
self.object.save()
|
|
self.object.log_action('pretix.device.revoked', user=self.request.user)
|
|
messages.success(request, _('Access for this device has been revoked.'))
|
|
return redirect(reverse('control:organizer.devices', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
}))
|
|
|
|
|
|
class WebHookListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = WebHook
|
|
template_name = 'pretixcontrol/organizers/webhooks.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'webhooks'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.webhooks.prefetch_related('limit_events')
|
|
|
|
|
|
class WebHookCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = WebHook
|
|
template_name = 'pretixcontrol/organizers/webhook_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
form_class = WebHookForm
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['organizer'] = self.request.organizer
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.webhooks', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
form.instance.organizer = self.request.organizer
|
|
ret = super().form_valid(form)
|
|
self.request.organizer.log_action('pretix.webhook.created', user=self.request.user, data=merge_dicts({
|
|
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
|
for k in form.changed_data
|
|
}, {'id': form.instance.pk}))
|
|
new_listeners = set(form.cleaned_data['events'])
|
|
for l in new_listeners:
|
|
self.object.listeners.create(action_type=l)
|
|
return ret
|
|
|
|
def form_invalid(self, form):
|
|
messages.error(self.request, _('Your changes could not be saved.'))
|
|
return super().form_invalid(form)
|
|
|
|
|
|
class WebHookUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = WebHook
|
|
template_name = 'pretixcontrol/organizers/webhook_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'webhook'
|
|
form_class = WebHookForm
|
|
|
|
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(WebHook, organizer=self.request.organizer, pk=self.kwargs.get('webhook'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.webhooks', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.request.organizer.log_action('pretix.webhook.changed', user=self.request.user, data=merge_dicts({
|
|
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
|
for k in form.changed_data
|
|
}, {'id': form.instance.pk}))
|
|
|
|
current_listeners = set(self.object.listeners.values_list('action_type', flat=True))
|
|
new_listeners = set(form.cleaned_data['events'])
|
|
for l in current_listeners - new_listeners:
|
|
self.object.listeners.filter(action_type=l).delete()
|
|
for l in new_listeners - current_listeners:
|
|
self.object.listeners.create(action_type=l)
|
|
|
|
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 WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = WebHook
|
|
template_name = 'pretixcontrol/organizers/webhook_logs.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'calls'
|
|
paginate_by = 50
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['webhook'] = self.webhook
|
|
ctx['retry_count'] = self.webhook.retries.count()
|
|
return ctx
|
|
|
|
@cached_property
|
|
def webhook(self):
|
|
return get_object_or_404(
|
|
WebHook, organizer=self.request.organizer, pk=self.kwargs.get('webhook')
|
|
)
|
|
|
|
def get_queryset(self):
|
|
return self.webhook.calls.order_by('-datetime')
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
if request.POST.get("action") == "expedite":
|
|
self.request.organizer.log_action('pretix.webhook.retries.expedited', user=self.request.user, data={
|
|
'webhook': self.webhook.pk,
|
|
})
|
|
manually_retry_all_calls.apply_async(args=(self.webhook.pk,))
|
|
messages.success(request, _('All requests will now be scheduled for an immediate attempt. Please '
|
|
'allow for a few minutes before they are processed.'))
|
|
elif request.POST.get("action") == "drop":
|
|
self.request.organizer.log_action('pretix.webhook.retries.dropped', user=self.request.user, data={
|
|
'webhook': self.webhook.pk,
|
|
})
|
|
self.webhook.retries.all().delete()
|
|
messages.success(request, _('All unprocessed webhooks have been stopped from retrying.'))
|
|
return redirect(reverse('control:organizer.webhook.logs', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'webhook': self.webhook.pk,
|
|
}))
|
|
|
|
|
|
class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
|
model = GiftCardAcceptance
|
|
template_name = 'pretixcontrol/organizers/giftcard_acceptance_invite.html'
|
|
permission = 'organizer.settings.general:write'
|
|
form_class = GiftCardAcceptanceInviteForm
|
|
|
|
def get_form_kwargs(self):
|
|
return {
|
|
**super().get_form_kwargs(),
|
|
'organizer': self.request.organizer,
|
|
}
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
self.request.organizer.gift_card_acceptor_acceptance.get_or_create(
|
|
acceptor=form.cleaned_data['acceptor'],
|
|
reusable_media=form.cleaned_data['reusable_media'],
|
|
active=False,
|
|
)
|
|
self.request.organizer.log_action(
|
|
action='pretix.giftcards.acceptance.acceptor.invited',
|
|
data={
|
|
'acceptor': form.cleaned_data['acceptor'].slug,
|
|
'issuer': self.request.organizer.slug,
|
|
'reusable_media': form.cleaned_data['reusable_media']
|
|
},
|
|
user=self.request.user
|
|
)
|
|
messages.success(self.request, _('The selected organizer has been invited.'))
|
|
return redirect(
|
|
reverse('control:organizer.giftcards.acceptance', kwargs={'organizer': self.request.organizer.slug}))
|
|
|
|
|
|
class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = GiftCardAcceptance
|
|
template_name = 'pretixcontrol/organizers/giftcard_acceptance_list.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'acceptor_acceptance'
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
qs = self.request.organizer.gift_card_acceptor_acceptance.select_related(
|
|
'acceptor'
|
|
).order_by('acceptor__name', 'acceptor_id')
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['issuer_acceptance'] = self.request.organizer.gift_card_issuer_acceptance.select_related(
|
|
'issuer'
|
|
)
|
|
return ctx
|
|
|
|
@transaction.atomic()
|
|
def post(self, request, *args, **kwargs):
|
|
if "delete_acceptor" in request.POST:
|
|
done = self.request.organizer.gift_card_acceptor_acceptance.filter(
|
|
acceptor__slug=request.POST.get("delete_acceptor")
|
|
).delete()
|
|
if done:
|
|
self.request.organizer.log_action(
|
|
action='pretix.giftcards.acceptance.acceptor.removed',
|
|
data={
|
|
'acceptor': request.POST.get("delete_acceptor"),
|
|
'issuer': self.request.organizer.slug
|
|
},
|
|
user=request.user
|
|
)
|
|
messages.success(self.request, _('The selected connection has been removed.'))
|
|
elif "delete_issuer" in request.POST:
|
|
done = self.request.organizer.gift_card_issuer_acceptance.filter(
|
|
issuer__slug=request.POST.get("delete_issuer")
|
|
).delete()
|
|
if done:
|
|
self.request.organizer.log_action(
|
|
action='pretix.giftcards.acceptance.issuer.removed',
|
|
data={
|
|
'issuer': request.POST.get("delete_acceptor"),
|
|
'acceptor': self.request.organizer.slug
|
|
},
|
|
user=request.user
|
|
)
|
|
messages.success(self.request, _('The selected connection has been removed.'))
|
|
if "accept_issuer" in request.POST:
|
|
done = self.request.organizer.gift_card_issuer_acceptance.filter(
|
|
issuer__slug=request.POST.get("accept_issuer")
|
|
).update(active=True)
|
|
if done:
|
|
self.request.organizer.log_action(
|
|
action='pretix.giftcards.acceptance.issuer.accepted',
|
|
data={
|
|
'issuer': request.POST.get("accept_issuer"),
|
|
'acceptor': self.request.organizer.slug
|
|
},
|
|
user=request.user
|
|
)
|
|
messages.success(self.request, _('The selected connection has been accepted.'))
|
|
|
|
return redirect(
|
|
reverse('control:organizer.giftcards.acceptance', kwargs={'organizer': self.request.organizer.slug}))
|
|
|
|
|
|
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = GiftCard
|
|
template_name = 'pretixcontrol/organizers/giftcards.html'
|
|
permission = 'organizer.giftcards:read'
|
|
context_object_name = 'giftcards'
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
s = GiftCardTransaction.objects.filter(
|
|
card=OuterRef('pk')
|
|
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
|
s_last_tx = GiftCardTransaction.objects.filter(
|
|
card=OuterRef('pk')
|
|
).order_by().values('card').annotate(m=Max('datetime')).values('m')
|
|
qs = self.request.organizer.issued_gift_cards.annotate(
|
|
cached_value=Coalesce(Subquery(s), Decimal('0.00')),
|
|
last_tx=Subquery(s_last_tx),
|
|
).order_by('-issuance')
|
|
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
|
|
ctx['other_organizers'] = self.request.user.get_organizers_with_permission(
|
|
'organizer.giftcards:write', self.request
|
|
).exclude(pk=self.request.organizer.pk)
|
|
return ctx
|
|
|
|
@cached_property
|
|
def filter_form(self):
|
|
return GiftCardFilterForm(data=self.request.GET, request=self.request)
|
|
|
|
|
|
class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
template_name = 'pretixcontrol/organizers/giftcard.html'
|
|
permission = 'organizer.giftcards:read'
|
|
context_object_name = 'card'
|
|
|
|
def get_object(self, queryset=None) -> Organizer:
|
|
return get_object_or_404(
|
|
self.request.organizer.issued_gift_cards,
|
|
pk=self.kwargs.get('giftcard')
|
|
)
|
|
|
|
@transaction.atomic()
|
|
def post(self, request, *args, **kwargs):
|
|
if not request.user.has_organizer_permission(request.organizer, "organizer.giftcards:write", request=request):
|
|
raise PermissionDenied()
|
|
self.object = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
|
if 'revert' in request.POST:
|
|
t = get_object_or_404(self.object.transactions.all(), pk=request.POST.get('revert'), order__isnull=False)
|
|
if self.object.value - t.value < Decimal('0.00'):
|
|
messages.error(request, _('Gift cards are not allowed to have negative values.'))
|
|
elif t.value > 0:
|
|
r = t.order.payments.create(
|
|
order=t.order,
|
|
state=OrderPayment.PAYMENT_STATE_CREATED,
|
|
amount=t.value,
|
|
provider='giftcard',
|
|
info=json.dumps({
|
|
'gift_card': self.object.pk,
|
|
'retry': True,
|
|
})
|
|
)
|
|
t.order.log_action('pretix.event.order.payment.started', {
|
|
'local_id': r.local_id,
|
|
'provider': r.provider
|
|
}, user=request.user)
|
|
try:
|
|
r.payment_provider.execute_payment(request, r)
|
|
except PaymentException as e:
|
|
with transaction.atomic():
|
|
r.state = OrderPayment.PAYMENT_STATE_FAILED
|
|
r.save()
|
|
t.order.log_action('pretix.event.order.payment.failed', {
|
|
'local_id': r.local_id,
|
|
'provider': r.provider,
|
|
'error': str(e)
|
|
})
|
|
messages.error(request, _('The transaction could not be reversed.'))
|
|
else:
|
|
messages.success(request, _('The transaction has been reversed.'))
|
|
elif request.POST.get('value'):
|
|
try:
|
|
value = DecimalField(localize=True).to_python(request.POST.get('value'))
|
|
except ValidationError:
|
|
messages.error(request, _('Your input was invalid, please try again.'))
|
|
else:
|
|
if self.object.value + value < Decimal('0.00'):
|
|
messages.error(request, _('Gift cards are not allowed to have negative values.'))
|
|
else:
|
|
self.object.transactions.create(
|
|
value=value,
|
|
text=request.POST.get('text') or None,
|
|
acceptor=request.organizer,
|
|
)
|
|
self.object.log_action(
|
|
action='pretix.giftcards.transaction.manual',
|
|
data={
|
|
'value': value,
|
|
'text': request.POST.get('text'),
|
|
'acceptor_id': self.request.organizer.id,
|
|
'acceptor_slug': self.request.organizer.slug
|
|
},
|
|
user=self.request.user,
|
|
)
|
|
messages.success(request, _('The manual transaction has been saved.'))
|
|
return redirect(reverse(
|
|
'control:organizer.giftcard',
|
|
kwargs={
|
|
'organizer': request.organizer.slug,
|
|
'giftcard': self.object.pk
|
|
}
|
|
))
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
return super().get_context_data(
|
|
**kwargs,
|
|
transactions=self.object.transactions.select_related(
|
|
'order', 'order__event', 'order__event__organizer', 'payment', 'refund'
|
|
).prefetch_related(
|
|
'acceptor'
|
|
)
|
|
)
|
|
|
|
|
|
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
template_name = 'pretixcontrol/organizers/giftcard_create.html'
|
|
permission = 'organizer.giftcards:write'
|
|
form_class = GiftCardCreateForm
|
|
success_url = 'invalid'
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
any_event = self.request.organizer.events.first()
|
|
kwargs['initial'] = {
|
|
'currency': any_event.currency if any_event else settings.DEFAULT_CURRENCY,
|
|
'secret': gen_giftcard_secret(self.request.organizer.settings.giftcard_length)
|
|
}
|
|
kwargs['organizer'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic()
|
|
def post(self, request, *args, **kwargs):
|
|
return super().post(request, *args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The gift card has been created and can now be used.'))
|
|
form.instance.issuer = self.request.organizer
|
|
super().form_valid(form)
|
|
form.instance.log_action(
|
|
action='pretix.giftcards.created',
|
|
user=self.request.user,
|
|
)
|
|
if form.cleaned_data['value']:
|
|
form.instance.transactions.create(
|
|
acceptor=self.request.organizer,
|
|
value=form.cleaned_data['value']
|
|
)
|
|
form.instance.log_action(
|
|
action='pretix.giftcards.transaction.manual',
|
|
user=self.request.user,
|
|
data={
|
|
'value': form.cleaned_data['value'],
|
|
'acceptor_id': self.request.organizer.id,
|
|
'acceptor_slug': self.request.organizer.slug
|
|
}
|
|
)
|
|
return redirect(reverse(
|
|
'control:organizer.giftcard',
|
|
kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'giftcard': self.object.pk
|
|
}
|
|
))
|
|
|
|
|
|
class GiftCardUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
template_name = 'pretixcontrol/organizers/giftcard_edit.html'
|
|
permission = 'organizer.giftcards:write'
|
|
form_class = GiftCardUpdateForm
|
|
success_url = 'invalid'
|
|
context_object_name = 'card'
|
|
model = GiftCard
|
|
|
|
def get_object(self, queryset=None) -> Organizer:
|
|
return get_object_or_404(
|
|
self.request.organizer.issued_gift_cards,
|
|
pk=self.kwargs.get('giftcard')
|
|
)
|
|
|
|
@transaction.atomic()
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The gift card has been changed.'))
|
|
super().form_valid(form)
|
|
form.instance.log_action(
|
|
action='pretix.giftcards.modified',
|
|
user=self.request.user,
|
|
data=dict(form.cleaned_data)
|
|
)
|
|
return redirect(reverse(
|
|
'control:organizer.giftcard',
|
|
kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'giftcard': self.object.pk
|
|
}
|
|
))
|
|
|
|
|
|
class ExportMixin:
|
|
@cached_property
|
|
def exporter(self):
|
|
id = self.request.GET.get("identifier") or self.request.POST.get("exporter") or self.request.GET.get("exporter")
|
|
if not id:
|
|
return None
|
|
for ex in self.exporters:
|
|
if id != ex.identifier:
|
|
continue
|
|
if self.scheduled or self.scheduled_copy_from:
|
|
initial = dict((self.scheduled or self.scheduled_copy_from).export_form_data)
|
|
|
|
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
|
|
test_form.fields = ex.export_form_fields
|
|
for k in initial:
|
|
if initial[k] and k in test_form.fields:
|
|
try:
|
|
if isinstance(test_form.fields[k], forms.SplitDateTimeField):
|
|
initial[k] = dateutil.parser.parse(initial[k])
|
|
else:
|
|
initial[k] = test_form.fields[k].to_python(initial[k])
|
|
except Exception:
|
|
pass
|
|
else:
|
|
# Use form parse cycle to generate useful defaults
|
|
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
|
|
test_form.fields = ex.export_form_fields
|
|
test_form.is_valid()
|
|
initial = {
|
|
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
|
|
}
|
|
if 'events' not in initial:
|
|
initial.setdefault('all_events', True)
|
|
|
|
ex.form = ExporterForm(
|
|
data=(self.request.POST if self.request.method == 'POST' else None),
|
|
prefix=ex.identifier,
|
|
initial=initial
|
|
)
|
|
ex.multisheet_warning = isinstance(ex, MultiSheetListExporter) and len(ex.sheets) > 1
|
|
ex.form.fields = ex.export_form_fields
|
|
if not isinstance(ex, OrganizerLevelExportMixin):
|
|
ex.form.fields.update([
|
|
('all_events',
|
|
forms.BooleanField(
|
|
label=_("All events (that I have access to)"),
|
|
required=False
|
|
)),
|
|
('events',
|
|
forms.ModelMultipleChoiceField(
|
|
queryset=ex.events,
|
|
widget=forms.CheckboxSelectMultiple(
|
|
attrs={
|
|
'class': 'scrolling-multiple-choice',
|
|
'data-inverse-dependency': f'#id_{ex.identifier}-all_events',
|
|
}
|
|
),
|
|
label=_('Events'),
|
|
required=False
|
|
)),
|
|
])
|
|
return ex
|
|
|
|
@cached_property
|
|
def exporters(self):
|
|
raw_exporters = list(init_organizer_exporters(
|
|
self.request.organizer, user=self.request.user, request=self.request,
|
|
staff_session=self.request.user.has_active_staff_session(self.request.session.session_key),
|
|
))
|
|
return sorted(
|
|
raw_exporters,
|
|
key=lambda ex: (
|
|
0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['exporter'] = self.exporter
|
|
ctx['exporters'] = self.exporters
|
|
return ctx
|
|
|
|
def get_scheduled_queryset(self):
|
|
if not self.request.user.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write',
|
|
request=self.request):
|
|
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
|
|
else:
|
|
qs = self.request.organizer.scheduled_exports
|
|
return qs.select_related('owner').order_by('export_identifier', 'schedule_next_run')
|
|
|
|
@cached_property
|
|
def scheduled(self):
|
|
if "scheduled" in self.request.POST:
|
|
return get_object_or_404(self.get_scheduled_queryset(), pk=self.request.POST.get("scheduled"))
|
|
elif "scheduled" in self.request.GET:
|
|
return get_object_or_404(self.get_scheduled_queryset(), pk=self.request.GET.get("scheduled"))
|
|
|
|
@cached_property
|
|
def scheduled_copy_from(self):
|
|
if "scheduled_copy_from" in self.request.GET:
|
|
return get_object_or_404(self.get_scheduled_queryset(), pk=self.request.GET.get("scheduled_copy_from"))
|
|
|
|
|
|
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
|
|
known_errortypes = ['ExportError', 'ExportEmptyError']
|
|
task = multiexport
|
|
template_name = 'pretixcontrol/organizers/export_form.html'
|
|
|
|
def get_success_message(self, value):
|
|
return None
|
|
|
|
def get_success_url(self, value):
|
|
return reverse('cachedfile.download', kwargs={'id': str(value)})
|
|
|
|
def get_error_url(self):
|
|
return reverse('control:organizer.export', kwargs={
|
|
'organizer': self.request.organizer.slug
|
|
})
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if 'async_id' in request.GET and settings.HAS_CELERY:
|
|
return self.get_result(request)
|
|
return TemplateView.get(self, request, *args, **kwargs)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
if not self.exporter:
|
|
messages.error(self.request, _('The selected exporter was not found.'))
|
|
return redirect('control:organizer.export', kwargs={
|
|
'organizer': self.request.organizer.slug
|
|
})
|
|
|
|
if self.scheduled:
|
|
data = self.scheduled.export_form_data
|
|
else:
|
|
if not self.exporter.form.is_valid():
|
|
messages.error(self.request,
|
|
_('There was a problem processing your input. See below for error details.'))
|
|
return self.get(request, *args, **kwargs)
|
|
data = self.exporter.form.cleaned_data
|
|
|
|
cf = CachedFile(web_download=True, session_key=request.session.session_key)
|
|
cf.date = now()
|
|
cf.expires = now() + timedelta(hours=24)
|
|
cf.save()
|
|
return self.do(
|
|
organizer=self.request.organizer.id,
|
|
user=self.request.user.id,
|
|
fileid=str(cf.id),
|
|
provider=self.exporter.identifier,
|
|
device=None,
|
|
token=None,
|
|
form_data=data,
|
|
staff_session=self.request.user.has_active_staff_session(self.request.session.session_key)
|
|
)
|
|
|
|
|
|
class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
|
paginate_by = 25
|
|
context_object_name = 'scheduled'
|
|
|
|
def get_template_names(self):
|
|
if self.exporter:
|
|
return ['pretixcontrol/organizers/export_form.html']
|
|
return ['pretixcontrol/organizers/export.html']
|
|
|
|
@transaction.atomic()
|
|
def post(self, request, *args, **kwargs):
|
|
if request.POST.get("schedule") == "save":
|
|
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled():
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Your user account does not have sufficient permission to run this report, therefore "
|
|
"you cannot change it."
|
|
)
|
|
)
|
|
return super().get(request, *args, **kwargs)
|
|
elif (not self.scheduled or not self.scheduled.pk) and not self.has_permission_to_create_scheduled():
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Your user account does not have sufficient permission to run this report, therefore "
|
|
"you cannot schedule it."
|
|
)
|
|
)
|
|
return super().get(request, *args, **kwargs)
|
|
elif self.exporter.form.is_valid() and self.rrule_form.is_valid() and self.schedule_form.is_valid():
|
|
self.schedule_form.instance.export_identifier = self.exporter.identifier
|
|
self.schedule_form.instance.export_form_data = self.exporter.form.cleaned_data
|
|
self.schedule_form.instance.schedule_rrule = str(self.rrule_form.to_rrule())
|
|
self.schedule_form.instance.error_counter = 0
|
|
self.schedule_form.instance.error_last_message = None
|
|
self.schedule_form.instance.compute_next_run()
|
|
self.schedule_form.instance.save()
|
|
if self.schedule_form.instance.schedule_next_run:
|
|
messages.success(
|
|
request,
|
|
_('Your export schedule has been saved. The next export will start around {datetime}.').format(
|
|
datetime=date_format(self.schedule_form.instance.schedule_next_run, 'SHORT_DATETIME_FORMAT')
|
|
)
|
|
)
|
|
else:
|
|
messages.warning(request, _('Your export schedule has been saved, but no next export is planned.'))
|
|
self.request.organizer.log_action(
|
|
'pretix.organizer.export.schedule.changed' if self.scheduled else 'pretix.organizer.export.schedule.added',
|
|
user=self.request.user, data={
|
|
'id': self.schedule_form.instance.id,
|
|
'export_identifier': self.exporter.identifier,
|
|
'export_form_data': self.exporter.form.cleaned_data,
|
|
'schedule_rrule': self.schedule_form.instance.schedule_rrule,
|
|
**self.schedule_form.cleaned_data,
|
|
}
|
|
)
|
|
return redirect(reverse('control:organizer.export', kwargs={
|
|
'organizer': self.request.organizer.slug
|
|
}))
|
|
else:
|
|
return super().get(request, *args, **kwargs)
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
@cached_property
|
|
def rrule_form(self):
|
|
if self.scheduled:
|
|
initial = RRuleForm.initial_from_rrule(self.scheduled.schedule_rrule)
|
|
elif self.scheduled_copy_from:
|
|
initial = RRuleForm.initial_from_rrule(self.scheduled_copy_from.schedule_rrule)
|
|
else:
|
|
initial = {}
|
|
return RRuleForm(
|
|
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get(
|
|
"schedule") == "save" else None,
|
|
prefix="rrule",
|
|
initial=initial
|
|
)
|
|
|
|
@cached_property
|
|
def schedule_form(self):
|
|
if self.scheduled_copy_from:
|
|
instance = copy.copy(self.scheduled_copy_from)
|
|
instance.pk = None
|
|
else:
|
|
instance = self.scheduled or ScheduledOrganizerExport(
|
|
organizer=self.request.organizer,
|
|
owner=self.request.user,
|
|
timezone=str(get_current_timezone()),
|
|
)
|
|
if not self.scheduled:
|
|
initial = {
|
|
"mail_subject": gettext("Export: {title}").format(title=self.exporter.verbose_name),
|
|
"mail_template": gettext(
|
|
"Hello,\n\nattached to this email, you can find a new scheduled report for {name}.").format(
|
|
name=str(self.request.organizer.name)
|
|
),
|
|
"schedule_rrule_time": time(4, 0, 0),
|
|
}
|
|
else:
|
|
initial = {}
|
|
return ScheduledOrganizerExportForm(
|
|
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get(
|
|
"schedule") == "save" else None,
|
|
prefix="schedule",
|
|
instance=instance,
|
|
initial=initial,
|
|
)
|
|
|
|
def get_queryset(self):
|
|
return self.get_scheduled_queryset()
|
|
|
|
def has_permission_to_edit_scheduled(self):
|
|
# Exports can be edited by
|
|
# - their owner
|
|
# - any staff session user
|
|
# - any user with permission for organizer settings *and* the permissions required to run the report
|
|
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
|
# user B has settings permission (= they can see the export configuration), but not enough permission
|
|
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
|
# as a recipient. Thereby, user B would gain access to data they can't have.
|
|
if not self.exporter:
|
|
# Triggered in scenario 5 in test_organizer_edit_restrictions
|
|
return False
|
|
if self.scheduled.owner == self.request.user:
|
|
return True
|
|
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
|
return True
|
|
if not self.exporter.available_for_user(self.request.user):
|
|
return False
|
|
if self.request.user.has_organizer_permission(self.request.organizer, "organizer.settings.general:write", request=self.request):
|
|
if isinstance(self.exporter, OrganizerLevelExportMixin):
|
|
# Test scenario 5/6 in test_organizer_edit_restrictions
|
|
return self.request.user.has_organizer_permission(
|
|
self.request.organizer, self.exporter.get_required_organizer_permission(), request=self.request
|
|
)
|
|
else:
|
|
if self.scheduled.export_form_data.get("all_events", False):
|
|
# Test scenario 1/2 in test_organizer_edit_restrictions
|
|
return self.request.user.teams.filter(
|
|
TeamQuerySet.event_permission_q(self.exporter.get_required_event_permission()),
|
|
all_events=True,
|
|
).exists()
|
|
else:
|
|
# Test scenario 3/4 in test_organizer_edit_restrictions
|
|
events_selected = self.scheduled.export_form_data.get("events", [])
|
|
events_permission = set(self.request.user.get_events_with_permission(
|
|
self.exporter.get_required_event_permission(), request=self.request,
|
|
).values_list("pk", flat=True))
|
|
return all(e in events_permission for e in events_selected)
|
|
|
|
def has_permission_to_create_scheduled(self):
|
|
# Exports can only be created if the user has the correct permissions. We *ignore* staff sessions, because
|
|
# the export is not *run* during a staff session and then would fail at the scheduled time.
|
|
if self.exporter:
|
|
if isinstance(self.exporter, OrganizerLevelExportMixin):
|
|
if not self.request.user.has_organizer_permission(self.request.organizer, self.exporter.get_required_organizer_permission()):
|
|
return False
|
|
else:
|
|
permission_name = self.exporter.get_required_event_permission()
|
|
if not any(t.has_event_permission(permission_name) for t in self.request.user.teams.filter(organizer=self.request.organizer)):
|
|
return False
|
|
if not self.exporter.available_for_user(self.request.user):
|
|
return False
|
|
return True
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
if "schedule" in self.request.POST or self.scheduled or self.scheduled_copy_from:
|
|
ctx['schedule_form'] = self.schedule_form
|
|
ctx['rrule_form'] = self.rrule_form
|
|
ctx['scheduled_copy_from'] = self.scheduled_copy_from
|
|
|
|
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled() and self.exporter:
|
|
ctx['no_save'] = True
|
|
for f in self.exporter.form.fields.values():
|
|
f.disabled = True
|
|
f.widget.attrs.pop("data-inverse-dependency", None)
|
|
for f in self.rrule_form.fields.values():
|
|
f.disabled = True
|
|
for f in self.schedule_form.fields.values():
|
|
f.disabled = True
|
|
|
|
elif not self.exporter:
|
|
for s in ctx['scheduled']:
|
|
try:
|
|
s.export_verbose_name = [e for e in self.exporters if e.identifier == s.export_identifier][
|
|
0].verbose_name
|
|
except IndexError:
|
|
s.export_verbose_name = "?"
|
|
return ctx
|
|
|
|
|
|
class DeleteScheduledExportView(OrganizerPermissionRequiredMixin, ExportMixin, CompatDeleteView):
|
|
template_name = 'pretixcontrol/organizers/export_delete.html'
|
|
context_object_name = 'export'
|
|
|
|
def get_queryset(self):
|
|
return self.get_scheduled_queryset()
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.export', kwargs={
|
|
'organizer': self.request.organizer.slug
|
|
})
|
|
|
|
@transaction.atomic()
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
self.object.delete()
|
|
self.request.organizer.log_action('pretix.organizer.export.schedule.deleted', user=self.request.user, data={
|
|
'id': self.object.id,
|
|
})
|
|
return redirect(self.get_success_url())
|
|
|
|
|
|
class RunScheduledExportView(OrganizerPermissionRequiredMixin, ExportMixin, View):
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
s = get_object_or_404(self.get_scheduled_queryset(), pk=kwargs.get('pk'))
|
|
scheduled_organizer_export.apply_async(
|
|
kwargs={
|
|
'organizer': s.organizer_id,
|
|
'schedule': s.pk,
|
|
},
|
|
# Scheduled exports usually run on the low-prio queue "background" but if they're manually triggered,
|
|
# we run them with normal priority
|
|
queue='default',
|
|
)
|
|
messages.success(self.request, _('Your export is queued to start soon. The results will be send via email. '
|
|
'Depending on system load and type and size of export, this may take a few '
|
|
'minutes.'))
|
|
return redirect(reverse('control:organizer.export', kwargs={
|
|
'organizer': self.request.organizer.slug
|
|
}))
|
|
|
|
|
|
class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = Gate
|
|
template_name = 'pretixcontrol/organizers/gates.html'
|
|
permission = 'organizer.devices:read'
|
|
context_object_name = 'gates'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.gates.all()
|
|
|
|
|
|
class GateCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = Gate
|
|
template_name = 'pretixcontrol/organizers/gate_edit.html'
|
|
permission = 'organizer.devices:write'
|
|
form_class = GateForm
|
|
|
|
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(Gate, organizer=self.request.organizer, pk=self.kwargs.get('gate'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.gates', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The gate has been created.'))
|
|
form.instance.organizer = self.request.organizer
|
|
ret = super().form_valid(form)
|
|
form.instance.log_action('pretix.gate.created', user=self.request.user, data={
|
|
k: getattr(self.object, k) 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 GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = Gate
|
|
template_name = 'pretixcontrol/organizers/gate_edit.html'
|
|
permission = 'organizer.devices:write'
|
|
context_object_name = 'gate'
|
|
form_class = GateForm
|
|
|
|
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(Gate, organizer=self.request.organizer, pk=self.kwargs.get('gate'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.gates', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.gate.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k)
|
|
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 GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
|
model = Gate
|
|
template_name = 'pretixcontrol/organizers/gate_delete.html'
|
|
permission = 'organizer.devices:write'
|
|
context_object_name = 'gate'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(Gate, organizer=self.request.organizer, pk=self.kwargs.get('gate'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.gates', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
success_url = self.get_success_url()
|
|
self.object = self.get_object()
|
|
self.object.log_action('pretix.gate.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected gate has been deleted.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
class EventMetaPropertyListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = EventMetaProperty
|
|
template_name = 'pretixcontrol/organizers/properties.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'properties'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.meta_properties.all()
|
|
|
|
|
|
class EventMetaPropertyEditorMixin:
|
|
template_name = 'pretixcontrol/organizers/property_edit.html'
|
|
form_class = EventMetaPropertyForm
|
|
|
|
@cached_property
|
|
def formset(self):
|
|
return EventMetaPropertyAllowedValueFormSet(
|
|
data=self.request.POST if self.request.method == "POST" else None,
|
|
organizer=self.request.organizer,
|
|
initial=(self.object.choices or []) if self.object else [],
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['formset'] = self.formset
|
|
return ctx
|
|
|
|
def get_form_kwargs(self):
|
|
return {
|
|
**super().get_form_kwargs(),
|
|
'event': self.request.organizer,
|
|
}
|
|
|
|
def is_default_valid(self):
|
|
choice_keys = [
|
|
f.cleaned_data.get("key") for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
|
]
|
|
default = self.form.cleaned_data["default"]
|
|
if default and choice_keys and default not in choice_keys:
|
|
messages.error(self.request, _("You cannot set a default value that is not a valid value."))
|
|
return False
|
|
return True
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object(self.get_queryset())
|
|
self.form = self.get_form()
|
|
if self.form.is_valid() and self.formset.is_valid() and self.is_default_valid():
|
|
return self.form_valid(self.form)
|
|
else:
|
|
return self.form_invalid(self.form)
|
|
|
|
|
|
class EventMetaPropertyCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, EventMetaPropertyEditorMixin, CreateView):
|
|
model = EventMetaProperty
|
|
permission = 'organizer.settings.general:write'
|
|
|
|
def get_object(self, queryset=None):
|
|
return EventMetaProperty()
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.properties', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The property has been created.'))
|
|
form.instance.organizer = self.request.organizer
|
|
form.instance.choices = [
|
|
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
|
]
|
|
ret = super().form_valid(form)
|
|
form.instance.log_action('pretix.property.created', user=self.request.user, data={
|
|
k: getattr(self.object, k) 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 EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, EventMetaPropertyEditorMixin, UpdateView):
|
|
model = EventMetaProperty
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'property'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.properties', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
form.instance.choices = [
|
|
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
|
]
|
|
if form.has_changed() or self.formset.has_changed():
|
|
self.object.log_action('pretix.property.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k)
|
|
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 EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
|
model = EventMetaProperty
|
|
template_name = 'pretixcontrol/organizers/property_delete.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'property'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.properties', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
success_url = self.get_success_url()
|
|
self.object = self.get_object()
|
|
self.object.log_action('pretix.property.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected property has been deleted.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
@transaction.atomic
|
|
def meta_property_move(request, property, up=True):
|
|
property = get_object_or_404(request.organizer.meta_properties, id=property)
|
|
properties = list(request.organizer.meta_properties.order_by("position"))
|
|
|
|
index = properties.index(property)
|
|
if index != 0 and up:
|
|
properties[index - 1], properties[index] = properties[index], properties[index - 1]
|
|
elif index != len(properties) - 1 and not up:
|
|
properties[index + 1], properties[index] = properties[index], properties[index + 1]
|
|
|
|
for i, prop in enumerate(properties):
|
|
if prop.position != i:
|
|
prop.position = i
|
|
prop.save()
|
|
prop.log_action(
|
|
'pretix.property.reordered', user=request.user, data={
|
|
'position': i,
|
|
}
|
|
)
|
|
messages.success(request, _('The order of properties has been updated.'))
|
|
|
|
|
|
@organizer_permission_required("organizer.settings.general:write")
|
|
@require_http_methods(["POST"])
|
|
def meta_property_move_up(request, organizer, property):
|
|
meta_property_move(request, property, up=True)
|
|
return redirect('control:organizer.properties',
|
|
organizer=request.organizer.slug)
|
|
|
|
|
|
@organizer_permission_required("organizer.settings.general:write")
|
|
@require_http_methods(["POST"])
|
|
def meta_property_move_down(request, organizer, property):
|
|
meta_property_move(request, property, up=False)
|
|
return redirect('control:organizer.properties',
|
|
organizer=request.organizer.slug)
|
|
|
|
|
|
@transaction.atomic
|
|
@organizer_permission_required("organizer.settings.general:write")
|
|
@require_http_methods(["POST"])
|
|
def reorder_meta_properties(request, organizer):
|
|
try:
|
|
ids = json.loads(request.body.decode('utf-8'))['ids']
|
|
except (JSONDecodeError, KeyError, ValueError):
|
|
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
|
|
|
input_meta_properties = list(request.organizer.meta_properties.filter(id__in=[i for i in ids if i.isdigit()]))
|
|
|
|
if len(input_meta_properties) != len(ids):
|
|
raise Http404(_("Some of the provided object ids are invalid."))
|
|
|
|
if len(input_meta_properties) != request.organizer.meta_properties.count():
|
|
raise Http404(_("Not all objects have been selected."))
|
|
|
|
for c in input_meta_properties:
|
|
pos = ids.index(str(c.pk))
|
|
if pos != c.position: # Save unneccessary UPDATE queries
|
|
c.position = pos
|
|
c.save(update_fields=['position'])
|
|
c.log_action(
|
|
'pretix.property.reordered', user=request.user, data={
|
|
'position': pos,
|
|
}
|
|
)
|
|
|
|
return HttpResponse()
|
|
|
|
|
|
class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|
template_name = 'pretixcontrol/organizers/logs.html'
|
|
permission = 'organizer.settings.general:write'
|
|
model = LogEntry
|
|
context_object_name = 'logs'
|
|
|
|
def get_queryset(self):
|
|
# technically, we'd also need to sort by pk since this is a paginated list, but in this case we just can't
|
|
# bear the performance cost
|
|
qs = self.request.organizer.logentry_set.filter(event=None).select_related(
|
|
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
|
).order_by('-datetime')
|
|
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
|
if self.request.GET.get('action_type'):
|
|
qs = qs.filter(action_type=self.request.GET['action_type'])
|
|
if self.request.GET.get('user'):
|
|
qs = qs.filter(user_id=self.request.GET.get('user'))
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data()
|
|
return ctx
|
|
|
|
|
|
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = MembershipType
|
|
template_name = 'pretixcontrol/organizers/membershiptypes.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'types'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.membership_types.all()
|
|
|
|
|
|
class MembershipTypeCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = MembershipType
|
|
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
form_class = MembershipTypeForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(MembershipType, organizer=self.request.organizer, pk=self.kwargs.get('type'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.membershiptypes', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['event'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The membership type has been created.'))
|
|
form.instance.organizer = self.request.organizer
|
|
ret = super().form_valid(form)
|
|
form.instance.log_action('pretix.membershiptype.created', user=self.request.user, data={
|
|
k: getattr(self.object, k) 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 MembershipTypeUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = MembershipType
|
|
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'type'
|
|
form_class = MembershipTypeForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(MembershipType, organizer=self.request.organizer, pk=self.kwargs.get('type'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.membershiptypes', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['event'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.membershiptype.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k)
|
|
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 MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
|
model = MembershipType
|
|
template_name = 'pretixcontrol/organizers/membershiptype_delete.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'type'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(MembershipType, organizer=self.request.organizer, pk=self.kwargs.get('type'))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['is_allowed'] = self.object.allow_delete()
|
|
return ctx
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.membershiptypes', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
success_url = self.get_success_url()
|
|
self.object = self.get_object()
|
|
if self.object.allow_delete():
|
|
self.object.log_action('pretix.membershiptype.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected object has been deleted.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
class SSOProviderListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = CustomerSSOProvider
|
|
template_name = 'pretixcontrol/organizers/ssoproviders.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'providers'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.sso_providers.all()
|
|
|
|
|
|
class SSOProviderCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = CustomerSSOProvider
|
|
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
form_class = SSOProviderForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(CustomerSSOProvider, organizer=self.request.organizer, pk=self.kwargs.get('provider'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.ssoproviders', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['event'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The provider has been created.'))
|
|
form.instance.organizer = self.request.organizer
|
|
ret = super().form_valid(form)
|
|
form.instance.log_action('pretix.ssoprovider.created', user=self.request.user, data={
|
|
k: getattr(self.object, k, self.object.configuration.get(k)) 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 SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = CustomerSSOProvider
|
|
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'provider'
|
|
form_class = SSOProviderForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(CustomerSSOProvider, organizer=self.request.organizer, pk=self.kwargs.get('provider'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.ssoproviders', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['redirect_uri'] = eventreverse_absolute(
|
|
self.request.organizer, 'presale:organizer.customer.login.return',
|
|
kwargs={
|
|
'provider': self.object.pk
|
|
}
|
|
)
|
|
return ctx
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['event'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.ssoprovider.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k, self.object.configuration.get(k)) 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 SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
|
model = CustomerSSOProvider
|
|
template_name = 'pretixcontrol/organizers/ssoprovider_delete.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'provider'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(CustomerSSOProvider, organizer=self.request.organizer, pk=self.kwargs.get('provider'))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['is_allowed'] = self.object.allow_delete()
|
|
return ctx
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.ssoproviders', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
success_url = self.get_success_url()
|
|
self.object = self.get_object()
|
|
if self.object.allow_delete():
|
|
self.object.log_action('pretix.ssoprovider.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected object has been deleted.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
class SSOClientListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = CustomerSSOClient
|
|
template_name = 'pretixcontrol/organizers/ssoclients.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'clients'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.sso_clients.all()
|
|
|
|
|
|
class SSOClientCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
model = CustomerSSOClient
|
|
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
form_class = SSOClientForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.ssoclient.edit', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'client': self.object.pk
|
|
})
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['event'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
secret = form.instance.set_client_secret()
|
|
messages.success(
|
|
self.request,
|
|
_('The SSO client has been created. Please note down the following client secret, it will never be shown '
|
|
'again: {secret}').format(secret=secret)
|
|
)
|
|
form.instance.organizer = self.request.organizer
|
|
ret = super().form_valid(form)
|
|
form.instance.log_action('pretix.ssoclient.created', user=self.request.user, data={
|
|
k: getattr(self.object, k, form.cleaned_data.get(k)) 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 SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
model = CustomerSSOClient
|
|
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'client'
|
|
form_class = SSOClientForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.ssoclient.edit', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'client': self.object.pk
|
|
})
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
return ctx
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['event'] = self.request.organizer
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.ssoclient.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k, form.cleaned_data.get(k)) for k in form.changed_data
|
|
})
|
|
if form.cleaned_data.get('regenerate_client_secret'):
|
|
secret = form.instance.set_client_secret()
|
|
messages.success(
|
|
self.request,
|
|
_('Your changes have been saved. Please note down the following client secret, it will never be shown '
|
|
'again: {secret}').format(secret=secret)
|
|
)
|
|
else:
|
|
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 SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
|
model = CustomerSSOClient
|
|
template_name = 'pretixcontrol/organizers/ssoclient_delete.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'client'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['is_allowed'] = self.object.allow_delete()
|
|
return ctx
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.ssoclients', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
success_url = self.get_success_url()
|
|
self.object = self.get_object()
|
|
if self.object.allow_delete():
|
|
self.object.log_action('pretix.ssoclient.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected object has been deleted.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|
model = Customer
|
|
template_name = 'pretixcontrol/organizers/customers.html'
|
|
permission = 'organizer.customers:read'
|
|
context_object_name = 'customers'
|
|
|
|
def get_queryset(self):
|
|
qs = self.request.organizer.customers.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 CustomerFilterForm(data=self.request.GET, request=self.request)
|
|
|
|
|
|
class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|
template_name = 'pretixcontrol/organizers/customer.html'
|
|
permission = 'organizer.customers:read'
|
|
context_object_name = 'orders'
|
|
|
|
def get_queryset(self):
|
|
q = Q(customer=self.customer)
|
|
if self.request.organizer.settings.customer_accounts_link_by_email and self.customer.email:
|
|
# This is safe because we only let customers with verified emails log in
|
|
q |= Q(email__iexact=self.customer.email)
|
|
qs = Order.objects.filter(
|
|
q
|
|
).select_related('event').prefetch_related('sales_channel').order_by('-datetime', 'pk')
|
|
return qs
|
|
|
|
@cached_property
|
|
def customer(self):
|
|
return get_object_or_404(
|
|
self.request.organizer.customers,
|
|
identifier=self.kwargs.get('customer')
|
|
)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
if request.POST.get('action') == 'pwreset' and self.customer.provider_id is None:
|
|
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
|
|
ctx = self.customer.get_email_context()
|
|
token = TokenGenerator().make_token(self.customer)
|
|
ctx['url'] = eventreverse_absolute(
|
|
self.request.organizer,
|
|
'presale:organizer.customer.recoverpw'
|
|
) + '?id=' + self.customer.identifier + '&token=' + token
|
|
mail(
|
|
self.customer.email,
|
|
self.request.organizer.settings.mail_subject_customer_reset,
|
|
self.request.organizer.settings.mail_text_customer_reset,
|
|
ctx,
|
|
locale=self.customer.locale,
|
|
customer=self.customer,
|
|
organizer=self.request.organizer,
|
|
sensitive=True,
|
|
)
|
|
messages.success(
|
|
self.request,
|
|
_('We\'ve sent the customer an email with further instructions on resetting your password.')
|
|
)
|
|
|
|
return redirect(reverse('control:organizer.customer', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'customer': self.customer.identifier,
|
|
}))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['customer'] = self.customer
|
|
ctx['display_locale'] = dict(settings.LANGUAGES)[self.customer.locale or self.request.organizer.settings.locale]
|
|
|
|
ctx['memberships'] = self.customer.memberships.with_usages().select_related(
|
|
'membership_type', 'granted_in', 'granted_in__order', 'granted_in__order__event'
|
|
)
|
|
|
|
for m in ctx['memberships']:
|
|
if m.membership_type.max_usages:
|
|
m.percent = int(m.usages / m.membership_type.max_usages * 100)
|
|
else:
|
|
m.percent = 0
|
|
|
|
# Only compute this annotations for this page (query optimization)
|
|
s = OrderPosition.objects.filter(
|
|
order=OuterRef('pk')
|
|
).order_by().values('order').annotate(k=Count('id')).values('k')
|
|
i = Invoice.objects.filter(
|
|
order=OuterRef('pk'),
|
|
is_cancellation=False,
|
|
refered__isnull=True,
|
|
).order_by().values('order').annotate(k=Count('id')).values('k')
|
|
annotated = {
|
|
o['pk']: o
|
|
for o in
|
|
Order.annotate_overpayments(Order.objects, sums=True).filter(
|
|
pk__in=[o.pk for o in ctx['orders']]
|
|
).annotate(
|
|
pcnt=Subquery(s, output_field=IntegerField()),
|
|
icnt=Subquery(i, output_field=IntegerField()),
|
|
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
|
|
).values(
|
|
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
|
|
'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum', 'icnt'
|
|
)
|
|
}
|
|
|
|
for o in ctx['orders']:
|
|
if o.pk not in annotated:
|
|
continue
|
|
o.pcnt = annotated.get(o.pk)['pcnt']
|
|
o.is_overpaid = annotated.get(o.pk)['is_overpaid']
|
|
o.is_underpaid = annotated.get(o.pk)['is_underpaid']
|
|
o.is_pending_with_full_payment = annotated.get(o.pk)['is_pending_with_full_payment']
|
|
o.has_external_refund = annotated.get(o.pk)['has_external_refund']
|
|
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
|
|
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
|
|
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
|
|
o.icnt = annotated.get(o.pk)['icnt']
|
|
|
|
ctx["lifetime_spending"] = (
|
|
self.get_queryset()
|
|
.filter(status=Order.STATUS_PAID)
|
|
.values(currency=F("event__currency"))
|
|
.order_by("currency")
|
|
.annotate(spending=Sum("total"))
|
|
)
|
|
|
|
ctx["gift_cards"] = self.customer.customer_gift_cards.all()
|
|
|
|
return ctx
|
|
|
|
|
|
class CustomerCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
template_name = 'pretixcontrol/organizers/customer_edit.html'
|
|
permission = 'organizer.customers:write'
|
|
context_object_name = 'customer'
|
|
form_class = CustomerCreateForm
|
|
|
|
def get_form_kwargs(self):
|
|
ctx = super().get_form_kwargs()
|
|
c = Customer(organizer=self.request.organizer)
|
|
c.assign_identifier()
|
|
ctx['instance'] = c
|
|
return ctx
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
r = super().form_valid(form)
|
|
form.instance.log_action('pretix.customer.created', user=self.request.user, data={
|
|
k: getattr(form.instance, k)
|
|
for k in form.changed_data
|
|
})
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return r
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.customer', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'customer': self.object.identifier,
|
|
})
|
|
|
|
|
|
class CustomerUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
template_name = 'pretixcontrol/organizers/customer_edit.html'
|
|
permission = 'organizer.customers:write'
|
|
context_object_name = 'customer'
|
|
form_class = CustomerUpdateForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(
|
|
self.request.organizer.customers,
|
|
identifier=self.kwargs.get('customer')
|
|
)
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.customer.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k)
|
|
for k in form.changed_data
|
|
})
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.customer', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'customer': self.object.identifier,
|
|
})
|
|
|
|
|
|
class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
template_name = 'pretixcontrol/organizers/customer_membership.html'
|
|
permission = 'organizer.customers:write'
|
|
context_object_name = 'membership'
|
|
form_class = MembershipUpdateForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(
|
|
Membership,
|
|
customer__organizer=self.request.organizer,
|
|
customer__identifier=self.kwargs.get('customer'),
|
|
pk=self.kwargs.get('id')
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['usages'] = self.object.orderposition_set.select_related(
|
|
'order', 'order__event', 'subevent', 'item', 'variation',
|
|
)
|
|
return ctx
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
d = {
|
|
k: getattr(self.object, k)
|
|
for k in form.changed_data
|
|
}
|
|
d['id'] = self.object.pk
|
|
self.object.customer.log_action('pretix.customer.membership.changed', user=self.request.user, data=d)
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.customer', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'customer': self.object.customer.identifier,
|
|
})
|
|
|
|
|
|
class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
|
template_name = 'pretixcontrol/organizers/customer_membership_delete.html'
|
|
permission = 'organizer.customers:write'
|
|
context_object_name = 'membership'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(
|
|
Membership,
|
|
customer__organizer=self.request.organizer,
|
|
customer__identifier=self.kwargs.get('customer'),
|
|
testmode=True,
|
|
pk=self.kwargs.get('id')
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['is_allowed'] = self.object.allow_delete()
|
|
return ctx
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
self.customer = self.object.customer
|
|
success_url = self.get_success_url()
|
|
if self.object.allow_delete():
|
|
self.object.cartposition_set.all().delete()
|
|
self.object.customer.log_action('pretix.customer.membership.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected object has been deleted.'))
|
|
return redirect(success_url)
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.customer', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'customer': self.customer.identifier,
|
|
})
|
|
|
|
|
|
class MembershipCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
template_name = 'pretixcontrol/organizers/customer_membership.html'
|
|
permission = 'organizer.customers:write'
|
|
context_object_name = 'membership'
|
|
form_class = MembershipUpdateForm
|
|
|
|
@cached_property
|
|
def customer(self):
|
|
return get_object_or_404(
|
|
self.request.organizer.customers,
|
|
identifier=self.kwargs.get('customer')
|
|
)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['instance'] = Membership(
|
|
customer=self.customer,
|
|
)
|
|
return kwargs
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
r = super().form_valid(form)
|
|
d = {
|
|
k: getattr(self.object, k)
|
|
for k in form.changed_data
|
|
}
|
|
d['id'] = self.object.pk
|
|
self.customer.log_action('pretix.customer.membership.created', user=self.request.user, data=d)
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return r
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.customer', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'customer': self.object.customer.identifier,
|
|
})
|
|
|
|
|
|
class CustomerAnonymizeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
|
template_name = 'pretixcontrol/organizers/customer_anonymize.html'
|
|
permission = 'organizer.customers:write'
|
|
context_object_name = 'customer'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(
|
|
self.request.organizer.customers,
|
|
identifier=self.kwargs.get('customer')
|
|
)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
with transaction.atomic():
|
|
self.object.anonymize()
|
|
self.object.log_action('pretix.customer.anonymized', user=self.request.user)
|
|
messages.success(self.request, _('The customer account has been anonymized.'))
|
|
return redirect(self.get_success_url())
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.customer', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'customer': self.object.identifier,
|
|
})
|
|
|
|
|
|
class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|
model = ReusableMedium
|
|
template_name = 'pretixcontrol/organizers/reusable_media.html'
|
|
permission = 'organizer.reusablemedia:read'
|
|
context_object_name = 'media'
|
|
|
|
def get_queryset(self):
|
|
qs = self.request.organizer.reusable_media.select_related(
|
|
'customer',
|
|
'linked_giftcard',
|
|
).prefetch_related(
|
|
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order", "order__event"))
|
|
)
|
|
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 ReusableMediaFilterForm(data=self.request.GET, request=self.request)
|
|
|
|
|
|
class ReusableMediumDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/organizers/reusable_medium.html'
|
|
permission = 'organizer.reusablemedia:read'
|
|
|
|
@cached_property
|
|
def medium(self):
|
|
return get_object_or_404(
|
|
self.request.organizer.reusable_media,
|
|
pk=self.kwargs.get('pk')
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['medium'] = self.medium
|
|
return ctx
|
|
|
|
|
|
class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
template_name = 'pretixcontrol/organizers/reusable_medium_edit.html'
|
|
permission = 'organizer.reusablemedia:write'
|
|
context_object_name = 'medium'
|
|
form_class = ReusableMediumCreateForm
|
|
|
|
def get_form_kwargs(self):
|
|
ctx = super().get_form_kwargs()
|
|
c = ReusableMedium(organizer=self.request.organizer)
|
|
ctx['instance'] = c
|
|
return ctx
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
r = super().form_valid(form)
|
|
|
|
data = {
|
|
k: getattr(form.instance, k)
|
|
for k in form.changed_data
|
|
}
|
|
if "linked_orderpositions" in data:
|
|
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
|
|
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data=data)
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return r
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.reusable_medium', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'pk': self.object.pk,
|
|
})
|
|
|
|
|
|
class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
template_name = 'pretixcontrol/organizers/reusable_medium_edit.html'
|
|
permission = 'organizer.reusablemedia:write'
|
|
context_object_name = 'medium'
|
|
form_class = ReusableMediumUpdateForm
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(
|
|
self.request.organizer.reusable_media,
|
|
pk=self.kwargs.get('pk')
|
|
)
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
prev_linked_ops_pks = list(getattr(self.object, "linked_orderpositions").values_list("pk", flat=True))
|
|
result = super().form_valid(form)
|
|
if form.has_changed():
|
|
data = {
|
|
k: getattr(self.object, k)
|
|
for k in form.changed_data
|
|
}
|
|
if "linked_orderpositions" in data:
|
|
# handle changes to linked_orderpositions separately
|
|
linked_ops_pks = data["linked_orderpositions"].values_list("pk", flat=True)
|
|
del data["linked_orderpositions"]
|
|
for op_pk in prev_linked_ops_pks:
|
|
if op_pk not in linked_ops_pks:
|
|
self.object.log_action(
|
|
'pretix.reusable_medium.linked_orderposition.removed',
|
|
user=self.request.user,
|
|
data={
|
|
'linked_orderposition': op_pk,
|
|
}
|
|
)
|
|
for op_pk in linked_ops_pks:
|
|
if op_pk not in prev_linked_ops_pks:
|
|
self.object.log_action(
|
|
'pretix.reusable_medium.linked_orderposition.added',
|
|
user=self.request.user,
|
|
data={
|
|
'linked_orderposition': op_pk,
|
|
}
|
|
)
|
|
if data:
|
|
# log change-action only for changes other than linked_orderpositions
|
|
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data=data)
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
return result
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.reusable_medium', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
'pk': self.object.pk,
|
|
})
|
|
|
|
|
|
class ChannelListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
model = SalesChannel
|
|
template_name = 'pretixcontrol/organizers/channels.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'channels'
|
|
|
|
def get_queryset(self):
|
|
return self.request.organizer.sales_channels.all()
|
|
|
|
|
|
class ChannelEditorMixin:
|
|
form_class = SalesChannelForm
|
|
|
|
def get_form_kwargs(self):
|
|
return {
|
|
**super().get_form_kwargs(),
|
|
'event': self.request.organizer,
|
|
}
|
|
|
|
|
|
class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, CreateView):
|
|
model = SalesChannel
|
|
permission = 'organizer.settings.general:write'
|
|
template_name = 'pretixcontrol/organizers/channel_add.html'
|
|
|
|
def get_object(self, queryset=None):
|
|
return SalesChannel()
|
|
|
|
@property
|
|
def allowed_types(self):
|
|
existing_types = set(self.request.organizer.sales_channels.values_list("type", flat=True))
|
|
return {
|
|
k: t for k, t in get_all_sales_channel_types().items()
|
|
if t.multiple_allowed or t.identifier not in existing_types
|
|
}
|
|
|
|
@cached_property
|
|
def selected_type(self):
|
|
try:
|
|
return self.allowed_types[self.request.GET.get("type")]
|
|
except KeyError:
|
|
return None
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
if not self.selected_type:
|
|
return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
|
|
"types": self.allowed_types.values()
|
|
})
|
|
return super().post(request, *args, **kwargs)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if not self.selected_type:
|
|
return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
|
|
"types": self.allowed_types.values()
|
|
})
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.channels', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx["type"] = self.selected_type
|
|
if self.selected_type.multiple_allowed:
|
|
ctx["identifier_prefix"] = self.selected_type.identifier + "."
|
|
return ctx
|
|
|
|
def get_form_kwargs(self):
|
|
return {
|
|
**super().get_form_kwargs(),
|
|
"type": self.selected_type,
|
|
}
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('The sales channel has been created.'))
|
|
form.instance.organizer = self.request.organizer
|
|
form.instance.type = self.selected_type.identifier
|
|
form.instance.position = (self.request.organizer.sales_channels.aggregate(m=Max("position"))["m"] or 0) + 1
|
|
ret = super().form_valid(form)
|
|
form.instance.log_action('pretix.saleschannel.created', user=self.request.user, data={
|
|
k: getattr(self.object, k) 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 ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, UpdateView):
|
|
model = SalesChannel
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'channel'
|
|
template_name = 'pretixcontrol/organizers/channel_edit.html'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.channels', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
@cached_property
|
|
def type(self):
|
|
return get_all_sales_channel_types()[self.object.type]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx["type"] = self.type
|
|
return ctx
|
|
|
|
def get_form_kwargs(self):
|
|
return {
|
|
**super().get_form_kwargs(),
|
|
"type": self.type,
|
|
}
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
if form.has_changed():
|
|
self.object.log_action('pretix.saleschannel.changed', user=self.request.user, data={
|
|
k: getattr(self.object, k)
|
|
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 ChannelDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
|
|
model = SalesChannel
|
|
template_name = 'pretixcontrol/organizers/channel_delete.html'
|
|
permission = 'organizer.settings.general:write'
|
|
context_object_name = 'channel'
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:organizer.channels', kwargs={
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data()
|
|
ctx["is_allowed"] = self.get_object().allow_delete
|
|
return ctx
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
success_url = self.get_success_url()
|
|
if not self.object.allow_delete():
|
|
messages.error(self.request, _('This channel can not be deleted.'))
|
|
return redirect(success_url)
|
|
try:
|
|
self.object.log_action('pretix.saleschannel.deleted', user=self.request.user)
|
|
self.object.delete()
|
|
messages.success(request, _('The selected sales channel has been deleted.'))
|
|
except ProtectedError:
|
|
messages.error(self.request, _('The channel could not be deleted as some constraints (e.g. data created by '
|
|
'plug-ins) did not allow it.'))
|
|
return redirect(success_url)
|
|
|
|
|
|
@transaction.atomic
|
|
def channel_move(request, channel, up=True):
|
|
channel = get_object_or_404(request.organizer.sales_channels, identifier=channel)
|
|
channels = list(request.organizer.sales_channels.order_by("position"))
|
|
|
|
index = channels.index(channel)
|
|
if index != 0 and up:
|
|
channels[index - 1], channels[index] = channels[index], channels[index - 1]
|
|
elif index != len(channels) - 1 and not up:
|
|
channels[index + 1], channels[index] = channels[index], channels[index + 1]
|
|
|
|
for i, prop in enumerate(channels):
|
|
if prop.position != i:
|
|
prop.position = i
|
|
prop.save()
|
|
prop.log_action(
|
|
'pretix.saleschannel.reordered', user=request.user, data={
|
|
'position': i,
|
|
}
|
|
)
|
|
messages.success(request, _('The order of sales channels has been updated.'))
|
|
|
|
|
|
@organizer_permission_required("organizer.settings.general:write")
|
|
@require_http_methods(["POST"])
|
|
def channel_move_up(request, organizer, channel):
|
|
channel_move(request, channel, up=True)
|
|
return redirect('control:organizer.channels',
|
|
organizer=request.organizer.slug)
|
|
|
|
|
|
@organizer_permission_required("organizer.settings.general:write")
|
|
@require_http_methods(["POST"])
|
|
def channel_move_down(request, organizer, channel):
|
|
channel_move(request, channel, up=False)
|
|
return redirect('control:organizer.channels',
|
|
organizer=request.organizer.slug)
|
|
|
|
|
|
@transaction.atomic
|
|
@organizer_permission_required("organizer.settings.general:write")
|
|
@require_http_methods(["POST"])
|
|
def reorder_channels(request, organizer):
|
|
try:
|
|
ids = json.loads(request.body.decode('utf-8'))['ids']
|
|
except (JSONDecodeError, KeyError, ValueError):
|
|
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
|
|
|
input_channels = list(request.organizer.sales_channels.filter(id__in=[i for i in ids if i.isdigit()]))
|
|
|
|
if len(input_channels) != len(ids):
|
|
raise Http404(_("Some of the provided object ids are invalid."))
|
|
|
|
if len(input_channels) != request.organizer.sales_channels.count():
|
|
raise Http404(_("Not all objects have been selected."))
|
|
|
|
for c in input_channels:
|
|
pos = ids.index(str(c.pk))
|
|
if pos != c.position: # Save unneccessary UPDATE queries
|
|
c.position = pos
|
|
c.save(update_fields=['position'])
|
|
c.log_action(
|
|
'pretix.saleschannel.reordered', user=request.user, data={
|
|
'position': pos,
|
|
}
|
|
)
|
|
|
|
return HttpResponse()
|