mirror of
https://github.com/pretix/pretix.git
synced 2026-08-12 11:07:02 +00:00
Syncing fork to upstream (#1)
Sync master with master of pretix/pretix@300f8f6 * Automatically sort new products to the end * Drop "squash your commits" from the dev guide * Add variation descriptions and allow to order addons * Link to Django's runserver options in dev docs * Allow <br> tags in rich text * Copy from event: deal with deleted items * Make validate_cart useful together with addons * Fix collapsing panels in the addon choice step * Button text change if addons are present * Update translations * Squash migrations and bump version * Ticket PDFs: Do not hide attendee name if code is hidden * Add a user guide on payments * Link PayPal and Stripe documentation in the respective forms * Hide payment fees if they are all equal to 0.00 * Refs #39 -- New concept of "teams" (#478) * New models * CRUD UI * UI for adding/removing team members * Log display for teams * Fix invitations, move frontend * Drop old models (incomplete) * Drop more old stuff * Drop even more old stuff * Fix tests * Fix permission test * flake8 fix * Add tests fore the new code * Rebase migrations * Fix typo in method name * Update translations * Force ordering of events on dashboard * Fix typos in events * Prepare the pretixdroid API for an async mode in the app * Pretixdroid tests: Ignore microseconds (chopped by mysql) * pretixdroid API: Add related lookups * Add idempotenty nonces to pretixdroid API * pretixdroid: force-accepting unpaids and time display * Marked webfonts as binary files (#487) Webfonts now listed as binary in `.gitattributes`. Works on pretix/pretix#486 * Fix #456 -- Allow products to be excluded from ticket-generation (#483) * Added non-admission setting to event `ticket_download_nonadm` now setting in storage. Still need logic for order page/PDF generation. Works on pretix/pretix#456. * Download button considers `ticket_download_nonadm` Modified Django tags to look at item admission attribute and `ticket_download_nonadm` setting. Works on pretix/pretix#456. * Ticket output for non-admission disabled PDFs/etc. will only be permitted/generated for items with the `admission` attribute, or if the `ticket_download_nonadm` event setting is true. Applies to single and whole-order ticket downloads. Works on pretix/pretix#456. * Fixed product exclusion in PDF output Forgot PDF output was a plugin, now includes same check as base `BaseTicketOutput.generate_order`. Works on pretix/pretix#456 * Mail signature (#485) * added signature field -- no function yet * added mail signature feature * fixed style issue * fixed problem with signature default * added unit test for mail signatures * added unit test for mail signatures * [WIP] Fix #447 -- Sendmail plugin: Create new mail based on an old one (#476) * send old email content to the new one * error key event * test commit * query bad ID * query bad ID * query bad ID * query bad ID * Update pretixdroid API version * Refs #447 -- Extend copying old mails to subject and receipients * Fixed bugs and added test for date range rendering (#488) * fixed bug for same dates, added unit check for daterange * fixed local language override in unit test * Fix #297 -- pretixdroid: Show metrics in the control panel (#481) * add checkin status page add dashboard widget add checkin page under orders * modify checkin logic added new fields in checkin page added filter items * add tests for checkins & minor improvement * support addin_product & noadm setting logic * remove name ordering check test case * Fix #379 -- Add logo to event organizers (#431) * [WIP] Add logo to event organizers. * Fix indentation issues. * Refactor code Refactor code Refactor code * Add new migration * Take files into account for organizer sform (settings form) * Fix grammer * Make bootstrap form errors specific to each fieldset * Display logo on organizer's page * Fix PR issues Fix PR issues Fix PR issues * Reorder imports * Remove conflicting migration * Fix rebase conflict * Fix #41 -- Drag-and-drop ticket editor Undo/redo Useful toolbox Font selection Add text content Use hex for colors JS-side dump and load Save Load layout, proper undo/redo First steps to Python rendering More PDF rendering Copy and paste Buttons for keyboard actions Splash Screen Block unbeforeunload in dirty state Remove debugging output Preview Upload new PDFs via the editor Fix bugs during PDF reload, link in settings form New default ticket Add OpenSans BI Custom fonts, fix tests * Added bootstrap-colorpicker * Allow inline PDF display in CSP header * Add fontpack to list of plugins * Update German translation * Add ticketoutputpdf's assets to MANIFEST.i * Fix migration of old ticket styles * Fix iCal download URL * Multi-line location field, new field for admission time * Admission date and time in editor * Remove icon from "add to calendar" * Try to fix PDF display problems in Safari * Proxy cachedfiles that are used as editor previews * Check Event.presale_is_running in more places * Fix CSS generation with an empty color field * Fix missing placeholders and reformat the sendmail view * Fix bug that lead to wrong payment amount when switching payment method to PayPal later * Update translation
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "1.3.0-alpha1"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
+219
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-04-27 09:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.organizer
|
||||
|
||||
|
||||
def create_teams(apps, schema_editor):
|
||||
Event = apps.get_model('pretixbase', 'Event')
|
||||
Organizer = apps.get_model('pretixbase', 'Organizer')
|
||||
Team = apps.get_model('pretixbase', 'Team')
|
||||
TeamInvite = apps.get_model('pretixbase', 'TeamInvite')
|
||||
EventPermission = apps.get_model('pretixbase', 'EventPermission')
|
||||
OrganizerPermission = apps.get_model('pretixbase', 'OrganizerPermission')
|
||||
|
||||
for o in Organizer.objects.prefetch_related('events'):
|
||||
for e in o.events.all():
|
||||
teams = {}
|
||||
|
||||
for p in e.user_perms.all():
|
||||
pkey = (p.can_change_settings, p.can_change_items, p.can_view_orders,
|
||||
p.can_change_permissions, p.can_change_orders, p.can_view_vouchers,
|
||||
p.can_change_vouchers)
|
||||
if pkey not in teams:
|
||||
team = Team()
|
||||
team.can_change_event_settings = p.can_change_settings
|
||||
team.can_change_items = p.can_change_items
|
||||
team.can_view_orders = p.can_view_orders
|
||||
team.can_change_orders = p.can_change_orders
|
||||
team.can_view_vouchers = p.can_view_vouchers
|
||||
team.can_change_vouchers = p.can_change_vouchers
|
||||
team.organizer = o
|
||||
team.name = '{} Team {}'.format(
|
||||
str(e.name), len(teams) + 1
|
||||
)
|
||||
team.save()
|
||||
team.limit_events.add(e)
|
||||
|
||||
teams[pkey] = team
|
||||
|
||||
if p.user:
|
||||
teams[pkey].members.add(p.user)
|
||||
else:
|
||||
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
|
||||
|
||||
teams = {}
|
||||
for p in o.user_perms.all():
|
||||
pkey = (p.can_create_events, p.can_change_permissions)
|
||||
if pkey not in teams:
|
||||
team = Team()
|
||||
team.can_change_organizer_settings = True
|
||||
team.can_create_events = p.can_create_events
|
||||
team.can_change_teams = p.can_change_permissions
|
||||
team.organizer = o
|
||||
team.name = '{} Team {}'.format(
|
||||
str(o.name), len(teams) + 1
|
||||
)
|
||||
team.save()
|
||||
teams[pkey] = team
|
||||
|
||||
if p.user:
|
||||
teams[pkey].members.add(p.user)
|
||||
else:
|
||||
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0051_auto_20170206_2027_squashed_0057_auto_20170501_2116'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=190, verbose_name='Team name')),
|
||||
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
|
||||
('can_create_events', models.BooleanField(default=False, verbose_name='Can create events')),
|
||||
('can_change_teams', models.BooleanField(default=False, verbose_name='Can change permissions')),
|
||||
('can_change_organizer_settings', models.BooleanField(default=False, verbose_name='Can change organizer settings')),
|
||||
('can_change_event_settings', models.BooleanField(default=False, verbose_name='Can change event settings')),
|
||||
('can_change_items', models.BooleanField(default=False, verbose_name='Can change product settings')),
|
||||
('can_view_orders', models.BooleanField(default=False, verbose_name='Can view orders')),
|
||||
('can_change_orders', models.BooleanField(default=False, verbose_name='Can change orders')),
|
||||
('can_view_vouchers', models.BooleanField(default=False, verbose_name='Can view vouchers')),
|
||||
('can_change_vouchers', models.BooleanField(default=False, verbose_name='Can change vouchers')),
|
||||
('limit_events', models.ManyToManyField(to='pretixbase.Event', verbose_name='Limit to events')),
|
||||
('members', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL, verbose_name='Team members')),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='pretixbase.Organizer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Teams',
|
||||
'verbose_name': 'Team',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamInvite',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True)),
|
||||
('token', models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='pretixbase.Team')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(
|
||||
create_teams, migrations.RunPython.noop
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-01 21:16
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0056_auto_20170414_1044'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='itemaddon',
|
||||
options={'ordering': ('position', 'pk')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemaddon',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemvariation',
|
||||
name='description',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, help_text='This is shown below the variation name in lists.', null=True, verbose_name='Description'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-04-29 10:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0052_team_teaminvite'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='eventpermission',
|
||||
name='event',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventpermission',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizerpermission',
|
||||
name='organizer',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizerpermission',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='permitted',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizer',
|
||||
name='permitted',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='can_change_teams',
|
||||
field=models.BooleanField(default=False, verbose_name='Can change teams and permissions'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='limit_events',
|
||||
field=models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EventPermission',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OrganizerPermission',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-04 07:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0058_auto_20170429_1020'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='nonce',
|
||||
field=models.CharField(blank=True, max_length=190, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-10 10:27
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0059_checkin_nonce'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='date_admission',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Admission time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='location',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location'),
|
||||
),
|
||||
]
|
||||
@@ -3,7 +3,7 @@ from .auth import U2FDevice, User
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventPermission, RequiredAction,
|
||||
Event, Event_SettingsStore, EventLock, RequiredAction,
|
||||
generate_invite_token,
|
||||
)
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
@@ -18,6 +18,6 @@ from .orders import (
|
||||
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, Organizer_SettingsStore, OrganizerPermission
|
||||
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
|
||||
from .vouchers import Voucher
|
||||
from .waitinglist import WaitingListEntry
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
||||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
|
||||
@@ -81,6 +84,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._teamcache = {}
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
@@ -147,6 +154,103 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return LogEntry.objects.filter(content_type=ContentType.objects.get_for_model(User),
|
||||
object_id=self.pk)
|
||||
|
||||
def _get_teams_for_organizer(self, organizer):
|
||||
if 'o{}'.format(organizer.pk) not in self._teamcache:
|
||||
self._teamcache['o{}'.format(organizer.pk)] = list(self.teams.filter(organizer=organizer))
|
||||
return self._teamcache['o{}'.format(organizer.pk)]
|
||||
|
||||
def _get_teams_for_event(self, organizer, event):
|
||||
if 'e{}'.format(event.pk) not in self._teamcache:
|
||||
self._teamcache['e{}'.format(event.pk)] = list(self.teams.filter(organizer=organizer).filter(
|
||||
Q(all_events=True) | Q(limit_events=event)
|
||||
))
|
||||
return self._teamcache['e{}'.format(event.pk)]
|
||||
|
||||
class SuperuserPermissionSet:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def get_event_permission_set(self, organizer, event) -> Union[set, SuperuserPermissionSet]:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular event
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
|
||||
def get_organizer_permission_set(self, organizer) -> Union[set, SuperuserPermissionSet]:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular organizer
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the event ``event``.
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return True
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
if teams:
|
||||
self._teamcache['e{}'.format(event.pk)] = teams
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_organizer_permission(self, organizer, perm_name=None):
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return True
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
if teams:
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_events_with_any_permission(self):
|
||||
"""
|
||||
Returns a queryset of events the user has any permissions to.
|
||||
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
from .event import Event
|
||||
|
||||
if self.is_superuser:
|
||||
return Event.objects.all()
|
||||
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=self.teams.filter(all_events=True).values_list('organizer', flat=True))
|
||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
|
||||
class U2FDevice(Device):
|
||||
json_data = models.TextField()
|
||||
|
||||
@@ -8,3 +8,4 @@ class Checkin(models.Model):
|
||||
"""
|
||||
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins')
|
||||
datetime = models.DateTimeField(default=now)
|
||||
nonce = models.CharField(max_length=190, null=True, blank=True)
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.email import CustomSMTPBackend
|
||||
from pretix.base.models.base import LoggedModel
|
||||
@@ -21,7 +21,6 @@ from pretix.base.validators import EventSlugBlacklistValidator
|
||||
from pretix.helpers.daterange import daterange
|
||||
|
||||
from ..settings import settings_hierarkey
|
||||
from .auth import User
|
||||
from .organizer import Organizer
|
||||
|
||||
|
||||
@@ -79,8 +78,6 @@ class Event(LoggedModel):
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
|
||||
permitted = models.ManyToManyField(User, through='EventPermission',
|
||||
related_name="events", )
|
||||
currency = models.CharField(max_length=10,
|
||||
verbose_name=_("Default currency"),
|
||||
choices=CURRENCY_CHOICES,
|
||||
@@ -88,6 +85,8 @@ class Event(LoggedModel):
|
||||
date_from = models.DateTimeField(verbose_name=_("Event start time"))
|
||||
date_to = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Event end time"))
|
||||
date_admission = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Admission time"))
|
||||
is_public = models.BooleanField(default=False,
|
||||
verbose_name=_("Visible in public lists"),
|
||||
help_text=_("If selected, this event may show up on the ticket system's start page "
|
||||
@@ -102,7 +101,7 @@ class Event(LoggedModel):
|
||||
verbose_name=_("Start of presale"),
|
||||
help_text=_("No products will be sold before this date."),
|
||||
)
|
||||
location = I18nCharField(
|
||||
location = I18nTextField(
|
||||
null=True, blank=True,
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
@@ -140,7 +139,7 @@ class Event(LoggedModel):
|
||||
return []
|
||||
return self.plugins.split(",")
|
||||
|
||||
def get_date_from_display(self, tz=None) -> str:
|
||||
def get_date_from_display(self, tz=None, show_times=True) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
@@ -148,7 +147,17 @@ class Event(LoggedModel):
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
|
||||
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
)
|
||||
|
||||
def get_time_from_display(self, tz=None) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start time of the event, ignoring
|
||||
the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
return _date(
|
||||
self.date_from.astimezone(tz), "TIME_FORMAT"
|
||||
)
|
||||
|
||||
def get_date_to_display(self, tz=None) -> str:
|
||||
@@ -269,7 +278,8 @@ class Event(LoggedModel):
|
||||
q.event = self
|
||||
q.save()
|
||||
for i in items:
|
||||
q.items.add(item_map[i.pk])
|
||||
if i.pk in item_map:
|
||||
q.items.add(item_map[i.pk])
|
||||
for v in vars:
|
||||
q.variations.add(variation_map[v.pk])
|
||||
|
||||
@@ -306,69 +316,6 @@ def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
class EventPermission(models.Model):
|
||||
"""
|
||||
The relation between an Event and a User who has permissions to
|
||||
access an event.
|
||||
|
||||
:param event: The event this permission refers to
|
||||
:type event: Event
|
||||
:param user: The user this permission set applies to
|
||||
:type user: User
|
||||
:param can_change_settings: If ``True``, the user can change all basic settings for this event.
|
||||
:type can_change_settings: bool
|
||||
:param can_change_items: If ``True``, the user can change and add items and related objects for this event.
|
||||
:type can_change_items: bool
|
||||
:param can_view_orders: If ``True``, the user can inspect details of all orders.
|
||||
:type can_view_orders: bool
|
||||
:param can_change_orders: If ``True``, the user can change details of orders
|
||||
:type can_change_orders: bool
|
||||
"""
|
||||
|
||||
event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE, null=True, blank=True)
|
||||
invite_email = models.EmailField(null=True, blank=True)
|
||||
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
can_change_settings = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change event settings")
|
||||
)
|
||||
can_change_items = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change product settings")
|
||||
)
|
||||
can_view_orders = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can view orders")
|
||||
)
|
||||
can_change_permissions = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change permissions")
|
||||
)
|
||||
can_change_orders = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change orders")
|
||||
)
|
||||
can_view_vouchers = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can view vouchers")
|
||||
)
|
||||
can_change_vouchers = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change vouchers")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Event permission")
|
||||
verbose_name_plural = _("Event permissions")
|
||||
|
||||
def __str__(self):
|
||||
return _("%(name)s on %(object)s") % {
|
||||
'name': str(self.user),
|
||||
'object': str(self.event),
|
||||
}
|
||||
|
||||
|
||||
class EventLock(models.Model):
|
||||
event = models.CharField(max_length=36, primary_key=True)
|
||||
date = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -310,6 +310,8 @@ class ItemVariation(models.Model):
|
||||
:type item: Item
|
||||
:param value: A string defining this variation
|
||||
:type value: str
|
||||
:param description: A short description
|
||||
:type description: str
|
||||
:param active: Whether this variation is being sold.
|
||||
:type active: bool
|
||||
:param default_price: This variation's default price
|
||||
@@ -327,6 +329,11 @@ class ItemVariation(models.Model):
|
||||
default=True,
|
||||
verbose_name=_("Active"),
|
||||
)
|
||||
description = I18nTextField(
|
||||
verbose_name=_("Description"),
|
||||
help_text=_("This is shown below the variation name in lists."),
|
||||
null=True, blank=True,
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
@@ -413,9 +420,14 @@ class ItemAddOn(models.Model):
|
||||
default=1,
|
||||
verbose_name=_('Maximum number')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('base_item', 'addon_category'),)
|
||||
ordering = ('position', 'pk')
|
||||
|
||||
def clean(self):
|
||||
if self.max_count < self.min_count:
|
||||
|
||||
@@ -42,8 +42,6 @@ class Organizer(LoggedModel):
|
||||
],
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
permitted = models.ManyToManyField(User, through='OrganizerPermission',
|
||||
related_name="organizers")
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer")
|
||||
@@ -74,39 +72,131 @@ def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
class OrganizerPermission(models.Model):
|
||||
class Team(LoggedModel):
|
||||
"""
|
||||
The relation between an Organizer and a User who has permissions to
|
||||
access an organizer profile.
|
||||
A team is a collection of people given certain access rights to one or more events of an organizer.
|
||||
|
||||
:param organizer: The organizer this relation refers to
|
||||
:param name: The name of this team
|
||||
:type name: str
|
||||
:param organizer: The organizer this team belongs to
|
||||
:type organizer: Organizer
|
||||
:param user: The user this set of permissions is valid for
|
||||
:type user: User
|
||||
:param can_create_events: Whether or not this user can create new events with this
|
||||
organizer account.
|
||||
:param members: A set of users who belong to this team
|
||||
:param all_events: Whether this team has access to all events of this organizer
|
||||
:type all_events: bool
|
||||
:param limit_events: A set of events this team has access to. Irrelevant if ``all_events`` is ``True``.
|
||||
:param can_create_events: Whether or not the members can create new events with this organizer account.
|
||||
:type can_create_events: bool
|
||||
:param can_change_teams: If ``True``, the members can change the teams of this organizer account.
|
||||
:type can_change_teams: bool
|
||||
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
|
||||
:type can_change_organizer_settings: bool
|
||||
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
|
||||
:type can_change_event_settings: bool
|
||||
:param can_change_items: If ``True``, the members can change and add items and related objects for the associated events.
|
||||
:type can_change_items: bool
|
||||
:param can_view_orders: If ``True``, the members can inspect details of all orders of the associated events.
|
||||
:type can_view_orders: bool
|
||||
:param can_change_orders: If ``True``, the members can change details of orders of the associated events.
|
||||
:type can_change_orders: bool
|
||||
:param can_view_vouchers: If ``True``, the members can inspect details of all vouchers of the associated events.
|
||||
:type can_view_vouchers: bool
|
||||
:param can_change_vouchers: If ``True``, the members can change and create vouchers for the associated events.
|
||||
:type can_change_vouchers: bool
|
||||
"""
|
||||
organizer = models.ForeignKey(Organizer, related_name="teams", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=190, verbose_name=_("Team name"))
|
||||
members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members"))
|
||||
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
|
||||
|
||||
organizer = models.ForeignKey(Organizer, related_name="user_perms", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, related_name="organizer_perms", on_delete=models.CASCADE, null=True, blank=True)
|
||||
invite_email = models.EmailField(null=True, blank=True)
|
||||
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
can_create_events = models.BooleanField(
|
||||
default=True,
|
||||
default=False,
|
||||
verbose_name=_("Can create events"),
|
||||
)
|
||||
can_change_permissions = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change permissions"),
|
||||
can_change_teams = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change teams and permissions"),
|
||||
)
|
||||
can_change_organizer_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change organizer settings")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer permission")
|
||||
verbose_name_plural = _("Organizer permissions")
|
||||
can_change_event_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change event settings")
|
||||
)
|
||||
can_change_items = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change product settings")
|
||||
)
|
||||
can_view_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view orders")
|
||||
)
|
||||
can_change_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change orders")
|
||||
)
|
||||
can_view_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view vouchers")
|
||||
)
|
||||
can_change_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change vouchers")
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("%(name)s on %(object)s") % {
|
||||
'name': str(self.user),
|
||||
'name': str(self.name),
|
||||
'object': str(self.organizer),
|
||||
}
|
||||
|
||||
def permission_set(self) -> set:
|
||||
attribs = dir(self)
|
||||
return {
|
||||
a for a in attribs if a.startswith('can_') and self.has_permission(a)
|
||||
}
|
||||
|
||||
@property
|
||||
def can_change_settings(self): # Legacy compatiblilty
|
||||
return self.can_change_event_settings
|
||||
|
||||
def has_permission(self, perm_name):
|
||||
try:
|
||||
return getattr(self, perm_name)
|
||||
except AttributeError:
|
||||
raise ValueError('Invalid required permission: %s' % perm_name)
|
||||
|
||||
def permission_for_event(self, event):
|
||||
if self.all_events:
|
||||
return event.organizer_id == self.organizer_id
|
||||
else:
|
||||
return self.limit_events.filter(pk=event.pk).exists()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Team")
|
||||
verbose_name_plural = _("Teams")
|
||||
|
||||
|
||||
class TeamInvite(models.Model):
|
||||
"""
|
||||
A TeamInvite represents someone who has been invited to a team but hasn't accept the invitation
|
||||
yet.
|
||||
|
||||
:param team: The team the person is invited to
|
||||
:type team: Team
|
||||
:param email: The email the invite has been sent to
|
||||
:type email: str
|
||||
:param token: The secret required to redeem the invite
|
||||
:type token: str
|
||||
"""
|
||||
team = models.ForeignKey(Team, related_name="invites", on_delete=models.CASCADE)
|
||||
email = models.EmailField(null=True, blank=True)
|
||||
token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("Invite to team '{team}' for '{email}'").format(
|
||||
team=str(self.team), email=self.email
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
@@ -150,9 +150,9 @@ class BasePaymentProvider:
|
||||
forms.BooleanField(
|
||||
label=_('Calculate the fee from the total value including the fee.'),
|
||||
help_text=_('We recommend you to enable this if you want your users to pay the payment fees of your '
|
||||
'payment provider. <a href="/control/help/payment/fee_reverse" target="_blank">Click here '
|
||||
'payment provider. <a href="{docs_url}" target="_blank">Click here '
|
||||
'for detailled information on what this does.</a> Don\'t forget to set the correct fees '
|
||||
'above!'),
|
||||
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
|
||||
required=False
|
||||
)),
|
||||
('_invoice_text',
|
||||
@@ -266,7 +266,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> "bool|str":
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called after the user selects this provider as his payment method.
|
||||
If you provided a form to the user to enter payment data, this method should
|
||||
@@ -394,14 +394,14 @@ class BasePaymentProvider:
|
||||
"""
|
||||
return False
|
||||
|
||||
def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def retry_prepare(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Deprecated, use order_prepare instead
|
||||
"""
|
||||
raise DeprecationWarning('retry_prepare is deprecated, use order_prepare instead')
|
||||
return self.order_prepare(request, order)
|
||||
|
||||
def order_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def order_prepare(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called if the user retries to pay an unpaid order (after the user filled in
|
||||
e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
|
||||
@@ -409,6 +409,10 @@ class BasePaymentProvider:
|
||||
|
||||
It should return and report errors the same way as :py:meth:`checkout_prepare`, but
|
||||
receives an ``Order`` object instead of a cart object.
|
||||
|
||||
Note: The ``Order`` object given to this method might be different from the version
|
||||
stored in the database as it's total will already contain the payment fee for the
|
||||
new payment method.
|
||||
"""
|
||||
form = self.payment_form(request)
|
||||
if form.is_valid():
|
||||
@@ -458,7 +462,7 @@ class BasePaymentProvider:
|
||||
return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, '
|
||||
'please transfer the money back manually.')
|
||||
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called if the event administrator confirms the refund.
|
||||
|
||||
@@ -523,7 +527,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
def order_control_refund_render(self, order: Order) -> str:
|
||||
return ''
|
||||
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> "bool|str":
|
||||
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called if the event administrator confirms the refund.
|
||||
|
||||
|
||||
@@ -114,6 +114,16 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
subject = "[%s] %s" % (prefix, subject)
|
||||
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
signature = str(event.settings.get('mail_text_signature'))
|
||||
if signature:
|
||||
signature = signature.format(event=event.name)
|
||||
signature_md = signature.replace('\n', '<br>\n')
|
||||
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
|
||||
htmlctx['signature'] = signature_md
|
||||
body_plain += signature
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
body_plain += _(
|
||||
"You are receiving this email because you placed an order for {event}."
|
||||
).format(event=event.name)
|
||||
|
||||
@@ -163,6 +163,10 @@ DEFAULTS = {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
},
|
||||
'ticket_download_nonadm': {
|
||||
'default': 'True',
|
||||
'type': bool
|
||||
},
|
||||
'last_order_modification_date': {
|
||||
'default': None,
|
||||
'type': datetime
|
||||
@@ -187,6 +191,10 @@ DEFAULTS = {
|
||||
'default': settings.MAIL_FROM,
|
||||
'type': str
|
||||
},
|
||||
'mail_text_signature': {
|
||||
'type': LazyI18nString,
|
||||
'default': ""
|
||||
},
|
||||
'mail_text_resend_link': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
@@ -148,6 +148,18 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if signature %}
|
||||
<tr>
|
||||
<td class="gap"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="order containertd">
|
||||
<div class="content">
|
||||
{{ signature | safe }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="footer">
|
||||
<div>
|
||||
|
||||
@@ -11,6 +11,7 @@ ALLOWED_TAGS = [
|
||||
'acronym',
|
||||
'b',
|
||||
'blockquote',
|
||||
'br',
|
||||
'code',
|
||||
'em',
|
||||
'i',
|
||||
|
||||
@@ -54,12 +54,15 @@ class BaseTicketOutput:
|
||||
|
||||
If you override this method, make sure that positions that are addons (i.e. ``addon_to``
|
||||
is set) are only outputted if the event setting ``ticket_download_addons`` is active.
|
||||
Do the same for positions that are non-admission without ``ticket_download_nonadm`` active.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||
for pos in order.positions.all():
|
||||
if pos.addon_to_id and not self.event.settings.ticket_download_addons:
|
||||
continue
|
||||
if not pos.item.admission and not self.event.settings.ticket_download_nonadm:
|
||||
continue
|
||||
fname, __, content = self.generate(pos)
|
||||
zipf.writestr('{}-{}{}'.format(
|
||||
order.code, pos.positionid, os.path.splitext(fname)[1]
|
||||
|
||||
@@ -2,6 +2,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
@@ -26,7 +27,7 @@ class EventWizardFoundationForm(forms.Form):
|
||||
self.fields['organizer'] = forms.ModelChoiceField(
|
||||
label=_("Organizer"),
|
||||
queryset=Organizer.objects.filter(
|
||||
id__in=self.user.organizer_perms.filter(can_create_events=True).values_list('organizer', flat=True)
|
||||
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
|
||||
),
|
||||
widget=forms.RadioSelect,
|
||||
empty_label=None,
|
||||
@@ -111,6 +112,16 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
|
||||
class EventWizardCopyForm(forms.Form):
|
||||
|
||||
@staticmethod
|
||||
def copy_from_queryset(user):
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=user.teams.filter(
|
||||
all_events=True, can_change_event_settings=True, can_change_items=True
|
||||
).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter(
|
||||
can_change_event_settings=True, can_change_items=True
|
||||
).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop('organizer')
|
||||
kwargs.pop('locales')
|
||||
@@ -118,11 +129,7 @@ class EventWizardCopyForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['copy_from_event'] = forms.ModelChoiceField(
|
||||
label=_("Copy configuration from"),
|
||||
queryset=Event.objects.filter(
|
||||
id__in=self.user.event_perms.filter(
|
||||
can_change_items=True, can_change_settings=True
|
||||
).values_list('event', flat=True)
|
||||
),
|
||||
queryset=EventWizardCopyForm.copy_from_queryset(self.user),
|
||||
widget=forms.RadioSelect,
|
||||
empty_label=_('Do not copy'),
|
||||
required=False
|
||||
@@ -136,6 +143,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
@@ -146,6 +154,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
'currency',
|
||||
'date_from',
|
||||
'date_to',
|
||||
'date_admission',
|
||||
'is_public',
|
||||
'presale_start',
|
||||
'presale_end',
|
||||
@@ -154,6 +163,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
widgets = {
|
||||
'date_from': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'date_to': forms.DateTimeInput(attrs={'class': 'datetimepicker', 'data-date-after': '#id_date_from'}),
|
||||
'date_admission': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
|
||||
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
|
||||
'data-date-after': '#id_presale_start'}),
|
||||
@@ -467,6 +477,15 @@ class MailSettingsForm(SettingsForm):
|
||||
label=_("Sender address"),
|
||||
help_text=_("Sender address for outgoing emails")
|
||||
)
|
||||
|
||||
mail_text_signature = I18nFormField(
|
||||
label=_("Signature"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("This will be attached to every email. Available placeholders: {event}"),
|
||||
validators=[PlaceholderValidator(['{event}'])]
|
||||
)
|
||||
|
||||
mail_text_order_placed = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
@@ -586,7 +605,8 @@ class DisplaySettingsForm(SettingsForm):
|
||||
validators=[
|
||||
RegexValidator(regex='^#[0-9a-fA-F]{6}$',
|
||||
message=_('Please enter the hexadecimal code of a color, e.g. #990000.'))
|
||||
]
|
||||
],
|
||||
widget=forms.TextInput(attrs={'class': 'colorpickerfield'})
|
||||
)
|
||||
logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
@@ -624,6 +644,11 @@ class TicketSettingsForm(SettingsForm):
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
|
||||
)
|
||||
ticket_download_nonadm = forms.BooleanField(
|
||||
label=_("Generate tickets for non-admission products"),
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_ticket_download'}),
|
||||
)
|
||||
|
||||
def prepare_fields(self):
|
||||
# See clean()
|
||||
|
||||
@@ -211,6 +211,7 @@ class ItemVariationForm(I18nModelForm):
|
||||
'value',
|
||||
'active',
|
||||
'default_price',
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||
from pretix.base.models import Organizer, Team
|
||||
from pretix.control.forms import ExtFileField
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
|
||||
|
||||
@@ -65,3 +67,46 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
instance.get_cache().clear()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['limit_events'].queryset = organizer.events.all()
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ['name', 'all_events', 'limit_events', 'can_create_events',
|
||||
'can_change_teams', 'can_change_organizer_settings',
|
||||
'can_change_event_settings', 'can_change_items',
|
||||
'can_view_orders', 'can_change_orders',
|
||||
'can_view_vouchers', 'can_change_vouchers']
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events'
|
||||
}),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
if self.instance.pk and not data['can_change_teams']:
|
||||
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists():
|
||||
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
|
||||
'the permission to change teams and permissions.'))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
|
||||
organizer_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. We will show your logo with a maximal height of 120 pixels.')
|
||||
)
|
||||
|
||||
@@ -121,7 +121,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
|
||||
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
|
||||
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.')
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.team.created': _('The team has been created.'),
|
||||
'pretix.team.changed': _('The team settings have been modified.'),
|
||||
'pretix.team.deleted': _('The team has been deleted.'),
|
||||
}
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
@@ -149,6 +152,23 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
if logentry.action_type.startswith('pretix.event.tickets.provider.'):
|
||||
return _('The settings of a ticket output provider have been changed.')
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.added':
|
||||
return _('{user} has been added to the team.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.removed':
|
||||
return _('{user} has been removed from the team.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.member.joined':
|
||||
return _('{user} has joined the team using the invite sent to {email}.').format(
|
||||
user=data.get('email'), email=data.get('invite_email')
|
||||
)
|
||||
|
||||
if logentry.action_type == 'pretix.team.invite.created':
|
||||
return _('{user} has been invited to the team.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.team.invite.deleted':
|
||||
return _('The invite for {user} has been revoked.').format(user=data.get('email'))
|
||||
|
||||
if logentry.action_type == 'pretix.user.settings.changed':
|
||||
text = str(_('Your account settings have been changed.'))
|
||||
if 'email' in data:
|
||||
|
||||
@@ -9,9 +9,7 @@ from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, EventPermission, Organizer, OrganizerPermission,
|
||||
)
|
||||
from pretix.base.models import Event, Organizer
|
||||
|
||||
|
||||
class PermissionMiddleware(MiddlewareMixin):
|
||||
@@ -61,53 +59,23 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
|
||||
events = Event.objects.all() if request.user.is_superuser else request.user.events
|
||||
request.user.events_cache = events.order_by(
|
||||
"organizer", "date_from").prefetch_related("organizer")
|
||||
events = request.user.get_events_with_any_permission()
|
||||
request.user.events_cache = events.order_by("organizer", "date_from").prefetch_related("organizer")
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
try:
|
||||
if request.user.is_superuser:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
request.eventperm = EventPermission(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
else:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
permitted__id__exact=request.user.id,
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer')[0]
|
||||
request.eventperm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
request.organizer = request.event.organizer
|
||||
except IndexError:
|
||||
request.event = Event.objects.filter(
|
||||
slug=url.kwargs['event'],
|
||||
organizer__slug=url.kwargs['organizer'],
|
||||
).select_related('organizer').first()
|
||||
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event):
|
||||
raise Http404(_("The selected event was not found or you "
|
||||
"have no permission to administrate it."))
|
||||
request.organizer = request.event.organizer
|
||||
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
|
||||
elif 'organizer' in url.kwargs:
|
||||
try:
|
||||
if request.user.is_superuser:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
)[0]
|
||||
request.orgaperm = OrganizerPermission(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
else:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
permitted__id__exact=request.user.id,
|
||||
)[0]
|
||||
request.orgaperm = OrganizerPermission.objects.get(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
except IndexError:
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
).first()
|
||||
if not request.organizer or not request.user.has_organizer_permission(request.organizer):
|
||||
raise Http404(_("The selected organizer was not found or you "
|
||||
"have no permission to administrate it."))
|
||||
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import EventPermission, OrganizerPermission
|
||||
|
||||
|
||||
def event_permission_required(permission):
|
||||
"""
|
||||
This view decorator rejects all requests with a 403 response which are not from
|
||||
users having the given permission for the event the request is associated with.
|
||||
"""
|
||||
if permission == 'can_change_settings':
|
||||
# Legacy support
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def decorator(function):
|
||||
def wrapper(request, *args, **kw):
|
||||
if not request.user.is_authenticated: # NOQA
|
||||
# just a double check, should not ever happen
|
||||
raise PermissionDenied()
|
||||
if request.user.is_superuser:
|
||||
|
||||
allowed = (
|
||||
request.user.is_superuser
|
||||
or request.user.has_event_permission(request.organizer, request.event, permission)
|
||||
)
|
||||
if allowed:
|
||||
return function(request, *args, **kw)
|
||||
try:
|
||||
perm = EventPermission.objects.get(
|
||||
event=request.event,
|
||||
user=request.user
|
||||
)
|
||||
except EventPermission.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
allowed = not permission
|
||||
try:
|
||||
if permission:
|
||||
allowed = getattr(perm, permission)
|
||||
except AttributeError:
|
||||
pass
|
||||
if allowed or request.user.is_superuser:
|
||||
return function(request, *args, **kw)
|
||||
|
||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -55,29 +47,23 @@ def organizer_permission_required(permission):
|
||||
This view decorator rejects all requests with a 403 response which are not from
|
||||
users having the given permission for the event the request is associated with.
|
||||
"""
|
||||
if permission == 'can_change_settings':
|
||||
# Legacy support
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
def decorator(function):
|
||||
def wrapper(request, *args, **kw):
|
||||
if not request.user.is_authenticated: # NOQA
|
||||
# just a double check, should not ever happen
|
||||
raise PermissionDenied()
|
||||
if request.user.is_superuser:
|
||||
|
||||
allowed = (
|
||||
request.user.is_superuser
|
||||
or request.user.has_organizer_permission(request.organizer, permission)
|
||||
)
|
||||
if allowed:
|
||||
return function(request, *args, **kw)
|
||||
try:
|
||||
perm = OrganizerPermission.objects.get(
|
||||
organizer=request.organizer,
|
||||
user=request.user
|
||||
)
|
||||
except OrganizerPermission.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
allowed = not permission
|
||||
try:
|
||||
if permission:
|
||||
allowed = getattr(perm, permission)
|
||||
except AttributeError:
|
||||
pass
|
||||
if allowed or request.user.is_superuser:
|
||||
return function(request, *args, **kw)
|
||||
|
||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fileupload/jquery.ui.widget.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{{ html_head|safe }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -65,7 +68,7 @@
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-calendar"></i>
|
||||
{{ request.event }} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="{% url "control:events" %}">{% trans "Event overview" %}</a></li>
|
||||
<li><a href="{% url "control:events" %}">{% trans "Events overview" %}</a></li>
|
||||
{% regroup request.user.events_cache by organizer as event_list %}
|
||||
{% for g in event_list %}
|
||||
<li class="dropdown-header">{{ g.grouper }}</li>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Check-ins" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Check-ins" %}</h1>
|
||||
<p>
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<select name="status" class="form-control">
|
||||
<option value="">{% trans "All status" %}</option>
|
||||
<option value="1" {% if request.GET.status == "1" %}selected="selected"{% endif %}>{% trans "Checked in" %}</option>
|
||||
<option value="0" {% if request.GET.status == "0" %}selected="selected"{% endif %}>{% trans "Not checked in" %}</option>
|
||||
</select>
|
||||
<select name="item" class="form-control">
|
||||
<option value="">{% trans "All products" %}</option>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.id }}"
|
||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||
{{ item.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" name="user" class="form-control" placeholder="{% trans "Search user" %}" value="{{ request.GET.user }}">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</form>
|
||||
</p>
|
||||
{% if entries|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No check-in record was found.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'item'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Email" %} <a href="?{% url_replace request 'ordering' '-email'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'email'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Name" %} <a href="?{% url_replace request 'ordering' '-name'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Status" %} <a href="?{% url_replace request 'ordering' '-status'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'status'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Timestamp" %} <a href="?{% url_replace request 'ordering' '-timestamp'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'timestamp'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in entries %}
|
||||
{% with e.checkins.first as checkin %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=e.order.code %}"
|
||||
>{{ e.order.code }}</a></strong>
|
||||
</td>
|
||||
<td>{{ e.item.name }}</td>
|
||||
<td>{{ e.order.email }}</td>
|
||||
<td>
|
||||
{% if e.addon_to %}
|
||||
{{ e.addon_to.attendee_name }}
|
||||
{% elif e.attendee_name %}
|
||||
{{ e.attendee_name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not checkin %}
|
||||
<span class="label label-danger">{% trans "Not checked in" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-success">{% trans "Checked in" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if checkin %}
|
||||
{{ checkin.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,9 +1,10 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
you have been invited to the team of an event that uses pretix for their
|
||||
you have been invited to a team on pretix, a platform to perform event
|
||||
ticket sales.
|
||||
|
||||
Event: {{ event }}
|
||||
Organizer: {{ organizer }}
|
||||
Team: {{ team }}
|
||||
|
||||
If you want to join that team, just click on the following link:
|
||||
{{ url }}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
you have been invited to the team of an event organizer that uses pretix
|
||||
for their ticket sales.
|
||||
|
||||
Organizer: {{ organizer }}
|
||||
|
||||
If you want to join that team, just click on the following link:
|
||||
{{ url }}
|
||||
|
||||
If you do not want to join, you can safely ignore or delete this email.
|
||||
|
||||
Best regards,
|
||||
|
||||
Your pretix team
|
||||
{% endblocktrans %}
|
||||
@@ -10,7 +10,7 @@
|
||||
{% trans "Dashboard" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if request.eventperm.can_change_settings or request.eventperm.can_change_permissions %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset or 'can_change_permissions' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-wrench fa-fw"></i>
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.eventperm.can_change_items %}
|
||||
{% if 'can_change_items' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.items' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="has-children">
|
||||
@@ -55,7 +55,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.eventperm.can_view_orders %}
|
||||
{% if 'can_view_orders' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.orders' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="has-children">
|
||||
@@ -90,10 +90,16 @@
|
||||
{% trans "Waiting list" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'control:event.orders.checkins' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if url_name == "event.orders.checkins" %}class="active"{% endif %}>
|
||||
{% trans "Check-ins" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.eventperm.can_view_vouchers %}
|
||||
{% if 'can_view_vouchers' in request.eventpermset %}
|
||||
<li>
|
||||
<a href="{% url 'control:event.vouchers' organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
{% if url_name == "event.vouchers" %}class="active"{% endif %}>
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
{% trans "Customer actions" %}
|
||||
</option>
|
||||
{% for up in userlist %}
|
||||
<option value="{{ up.user_id }}" {% if request.GET.user == up.user_id %}selected="selected"{% endif %}>
|
||||
{{ up.user }}
|
||||
</option>
|
||||
{% if up.user__id %}
|
||||
<option value="{{ up.user__id }}" {% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||
{{ up.user__email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<legend>{% trans "E-mail settings" %}</legend>
|
||||
{% bootstrap_field form.mail_prefix layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_from layout="horizontal" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "E-mail content" %}</legend>
|
||||
|
||||
@@ -1,88 +1,19 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal form-permissions">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Permissions" %}</legend>
|
||||
{% bootstrap_formset_errors formset %}
|
||||
{{ formset.management_form }}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Change settings" %}</th>
|
||||
<th>{% trans "Change products" %}</th>
|
||||
<th>{% trans "View orders" %}</th>
|
||||
<th>{% trans "Change orders" %}</th>
|
||||
<th>{% trans "Change permissions" %}</th>
|
||||
<th>{% trans "View vouchers" %}</th>
|
||||
<th>{% trans "Change vouchers" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in formset %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ form.id }}
|
||||
{% if form.instance.user %}
|
||||
{{ form.instance.user }}
|
||||
{% else %}
|
||||
{{ form.instance.invite_email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.can_change_settings }}</td>
|
||||
<td>{{ form.can_change_items }}</td>
|
||||
<td>{{ form.can_view_orders }}</td>
|
||||
<td>{{ form.can_change_orders }}</td>
|
||||
<td>{{ form.can_change_permissions }}</td>
|
||||
<td>{{ form.can_view_vouchers }}</td>
|
||||
<td>{{ form.can_change_vouchers }}</td>
|
||||
<td>{{ form.DELETE }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<strong>{% trans "Adding a new user" %}</strong><br>
|
||||
{% blocktrans trimmed %}
|
||||
To add a new user, you can enter their email address here. If they already have a
|
||||
pretix account, they will immediately be added to the event. Otherwise, they will
|
||||
be sent an email with an invitation.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-12">
|
||||
{% bootstrap_field add_form.user layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ add_form.can_change_settings }}</td>
|
||||
<td>{{ add_form.can_change_items }}</td>
|
||||
<td>{{ add_form.can_view_orders }}</td>
|
||||
<td>{{ add_form.can_change_orders }}</td>
|
||||
<td>{{ add_form.can_change_permissions }}</td>
|
||||
<td>{{ add_form.can_view_vouchers }}</td>
|
||||
<td>{{ add_form.can_change_vouchers }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
<div class="section-moved">
|
||||
<img src="{% static "pretixcontrol/img/moved.svg" %}" class="img-moved">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Permission settings have moved and are now configured as part of an organizer account instead
|
||||
of every event on its own.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:organizer.teams" organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-link btn-lg">{% trans "Go to the organizer team settings" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{% bootstrap_field form.date_from layout="horizontal" %}
|
||||
{% bootstrap_field form.date_to layout="horizontal" %}
|
||||
{% bootstrap_field form.location layout="horizontal" %}
|
||||
{% bootstrap_field form.date_admission layout="horizontal" %}
|
||||
{% bootstrap_field form.currency layout="horizontal" %}
|
||||
{% bootstrap_field form.is_public layout="horizontal" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<h1>{% trans "Settings" %}</h1>
|
||||
<ul class="nav nav-pills">
|
||||
{% if request.eventperm.can_change_settings %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
<li {% if "event.settings" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% trans "General" %}
|
||||
@@ -41,8 +41,6 @@
|
||||
{% trans "Invoicing" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.eventperm.can_change_permissions %}
|
||||
<li {% if "event.settings.permissions" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.settings.permissions' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% trans "Permissions" %}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{% bootstrap_field form.ticket_download layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_date layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_addons layout="horizontal" %}
|
||||
{% bootstrap_field form.ticket_download_nonadm layout="horizontal" %}
|
||||
{% for provider in providers %}
|
||||
<div class="panel panel-default ticketoutput-panel">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Help center" %}</h1>
|
||||
{% block inner %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -1,68 +0,0 @@
|
||||
{% extends "pretixcontrol/help/base.html" %}
|
||||
{% block title %}Payment fee calculation{% endblock %}
|
||||
{% block inner %}
|
||||
<h2>Payment fee calculation</h2>
|
||||
<p>
|
||||
If you configure a fee for a payment method, there are two possible ways for us to calculate this. Let's
|
||||
assume that your payment provider, e.g. PayPal, charges you 5 % fees and you want to charge your users the
|
||||
same 5 %, such that for a ticket with a list price of 100 € you will get your full 100 €.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Method A: Calculate the fee from the subtotal and add it to the bill.</strong> For a ticket price of
|
||||
100 €, this will lead to the following calculation:
|
||||
<table class="table helper-width-auto">
|
||||
<tr>
|
||||
<td>Ticket price</td>
|
||||
<td class="text-right">100.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>pretix calculates the fee as 5% of 100 €</td>
|
||||
<td class="text-right">+ 5.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subtotal that will be paid by the customer</td>
|
||||
<td class="text-right">105.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PayPal calculates its fee as 5% of 105 €</td>
|
||||
<td class="text-right">- 5.25 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>End total that is on your bank account</td>
|
||||
<td class="text-right"><strong>99.75 €</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Method B (default): Calculate the fee from the total value including the fee.</strong> For a ticket
|
||||
price of 100 €, this will lead to the following calculation:
|
||||
<table class="table helper-width-auto">
|
||||
<tr>
|
||||
<td>Ticket price</td>
|
||||
<td class="text-right">100.00 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>pretix calculates the fee as 100/(100 - 5)% of 100 €</td>
|
||||
<td class="text-right">+ 5.26 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subtotal that will be paid by the customer</td>
|
||||
<td class="text-right">105.26 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PayPal calculates its fee as 5% of 105.26 €</td>
|
||||
<td class="text-right">- 5.26 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>End total that is on your bank account</td>
|
||||
<td class="text-right"><strong>100.00 €</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="alert-warning alert">
|
||||
Due to the various rounding steps performed by us and by the payment provider, the end total on
|
||||
your bank account might stil vary by one cent.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -24,6 +24,7 @@
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
@@ -31,8 +32,12 @@
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Add-On" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
|
||||
<div class="col-sm-4 text-right">
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,6 +56,7 @@
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
@@ -59,7 +65,11 @@
|
||||
<h3 class="panel-title">{% trans "Add-On" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button type="button" class="btn btn-xs btn-danger pull-right" data-formset-delete-button>
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.active layout='horizontal' %}
|
||||
{% bootstrap_field form.default_price layout='horizontal' %}
|
||||
{% bootstrap_field form.description layout='horizontal' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -69,6 +70,7 @@
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.active layout='horizontal' %}
|
||||
{% bootstrap_field formset.empty_form.default_price layout='horizontal' %}
|
||||
{% bootstrap_field formset.empty_form.description layout='horizontal' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}
|
||||
{% if request.eventperm.can_change_items %}
|
||||
{% if 'can_change_items' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.edit" event=request.event.slug organizer=request.event.organizer.slug quota=quota.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% endblocktrans %}
|
||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=order class="pull-right" %}
|
||||
</h1>
|
||||
{% if request.eventperm.can_change_orders %}
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
{% if order.status == 'n' or order.status == 'p' or order.status == 'e' %}
|
||||
<form action="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
method="post">
|
||||
@@ -151,7 +151,7 @@
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
{% if order.changable and request.eventperm.can_change_orders %}
|
||||
{% if order.changable and 'can_change_orders' in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.change" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Change products" %}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
|
||||
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
{% if 'can_change_organizer_settings' in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "organizer" == url_name %}class="active"{% endif %}>
|
||||
@@ -17,10 +19,10 @@
|
||||
{% trans "Events" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if request.orgaperm.can_change_permissions %}
|
||||
<li {% if "organizer.teams" == url_name %}class="active"{% endif %}>
|
||||
{% if 'can_change_teams' in request.orgapermset %}
|
||||
<li {% if "organizer.team" in url_name %}class="active"{% endif %}>
|
||||
<a href="{% url "control:organizer.teams" organizer=organizer.slug %}">
|
||||
{% trans "Permissions" %}
|
||||
{% trans "Teams" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
@@ -4,17 +4,22 @@
|
||||
{% block title %}{% trans "Organizer" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Organizer" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
{% bootstrap_field form.slug layout="horizontal" %}
|
||||
{% if form.domain %}
|
||||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display settings" %}</legend>
|
||||
{% bootstrap_form_errors sform %}
|
||||
{% bootstrap_field sform.organizer_logo_image layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h2>{% trans "Delete team:" %} {{ team.name }}</h2>
|
||||
{% if not possible %}
|
||||
<p>{% blocktrans %}You cannot delete the team because there would be noone left who could change team permissions afterwards.{% endblocktrans %}</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.teams" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete the team?{% endblocktrans %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.teams" organizer=request.organizer.slug%}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
{% if team %}
|
||||
<h2>{% trans "Team:" %} {{ team.name }}</h2>
|
||||
{% else %}
|
||||
<h2>{% trans "Create a new team" %}</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You will be able to add team members in the next step.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Organizer permissions" %}</legend>
|
||||
{% bootstrap_field form.can_create_events layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_teams layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_organizer_settings layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Event permissions" %}</legend>
|
||||
|
||||
{% bootstrap_field form.all_events layout="horizontal" %}
|
||||
{% bootstrap_field form.limit_events layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_event_settings layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_items layout="horizontal" %}
|
||||
{% bootstrap_field form.can_view_orders layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_orders layout="horizontal" %}
|
||||
{% bootstrap_field form.can_view_vouchers layout="horizontal" %}
|
||||
{% bootstrap_field form.can_change_vouchers layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h2>
|
||||
{% trans "Team:" %} {{ team.name }}
|
||||
<a href="{% url "control:organizer.team.edit" organizer=organizer.slug team=team.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<!-- Trick browsers into taking this as a default -->
|
||||
<button type="submit" class="btn btn-primary btn-sm btn-block nearly-gone"></button>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Member" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in team.members.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ u.email }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="submit" name="remove-member" value="{{ u.id }}"
|
||||
class="btn btn-danger btn-sm btn-block">
|
||||
<i class="fa fa-times"></i> {% trans "Remove" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for i in team.invites.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ i.email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="submit" name="remove-invite" value="{{ i.id }}"
|
||||
class="btn btn-danger btn-sm btn-block">
|
||||
<i class="fa fa-times"></i> {% trans "Remove" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>
|
||||
{% bootstrap_field add_form.user layout='inline' %}<br>
|
||||
{% blocktrans trimmed %}
|
||||
To add a new user, you can enter their email address here. If they already have a
|
||||
pretix account, they will immediately be added to the event. Otherwise, they will
|
||||
be sent an email with an invitation.
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="submit" class="btn btn-primary btn-sm btn-block">
|
||||
<i class="fa fa-plus"></i> {% trans "Add" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</form>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Team history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=team %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -2,83 +2,57 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<form action="" method="post" class="form-horizontal form-permissions">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can use the following list to control who can create new events in the name of
|
||||
this organizer and who can add more people to this list. This does <strong>not</strong>
|
||||
control who has access to a particular event. You can control the access to an
|
||||
event in the "Permissions" section of the event's settings. A user does not need to
|
||||
be on the list here to get access to an event.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Everyone on this list can control the organizer settings on this page." %}
|
||||
</p>
|
||||
|
||||
{% bootstrap_formset_errors formset %}
|
||||
{{ formset.management_form }}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Create events" %}</th>
|
||||
<th>{% trans "Change permissions" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in formset %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ form.id }}
|
||||
{% if form.instance.user %}
|
||||
{{ form.instance.user }}
|
||||
{% else %}
|
||||
{{ form.instance.invite_email }}
|
||||
<span class="fa fa-envelope-o" data-toggle="tooltip"
|
||||
title="{% trans "invited, pending response" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.can_create_events }}</td>
|
||||
<td>{{ form.can_change_permissions }}</td>
|
||||
<td>{{ form.DELETE }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<strong>{% trans "Adding a new user" %}</strong><br>
|
||||
{% blocktrans trimmed %}
|
||||
To add a new user, you can enter their email address here. If they
|
||||
already have a pretix account, they will immediately be added to the team.
|
||||
Otherwise, they will be sent an email with an invitation.
|
||||
<p>
|
||||
{% trans "The list below shows all teams that exist within this organizer." %}
|
||||
</p>
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url "control:organizer.team.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new team" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Team name" %}</th>
|
||||
<th>{% trans "Members" %}</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in teams %}
|
||||
<tr>
|
||||
<td><strong>
|
||||
<a href="{% url "control:organizer.team" organizer=request.organizer.slug team=t.id %}">
|
||||
{{ t.name }}
|
||||
</a>
|
||||
</strong></td>
|
||||
<td>
|
||||
{{ t.memcount }}
|
||||
{% if t.invcount %}
|
||||
{% blocktrans trimmed with count=t.invcount %}
|
||||
+ {{ count }} invited
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-12">
|
||||
{% bootstrap_field add_form.user layout='inline' %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ add_form.can_create_events }}</td>
|
||||
<td>{{ add_form.can_change_permissions }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if t.all_events %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
{{ t.eventcount }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.team" organizer=request.organizer.slug team=t.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-list"></i></a>
|
||||
<a href="{% url "control:organizer.team.edit" organizer=request.organizer.slug team=t.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:organizer.team.delete" organizer=request.organizer.slug team=t.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% if request.eventperm.can_change_orders %}
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
<form method="post" class="col-md-6"
|
||||
action="{% url "control:event.orders.waitinglist.auto" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
data-asynctask>
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="{% if request.eventperm.can_change_orders %}col-md-6{% else %}col-md-12{% endif %}">
|
||||
<div class="{% if 'can_change_orders' in request.eventpermset %}col-md-6{% else %}col-md-12{% endif %}">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% trans "Sales estimate" %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from pretix.control.views import (
|
||||
auth, dashboards, event, global_settings, help, item, main, orders,
|
||||
auth, checkin, dashboards, event, global_settings, item, main, orders,
|
||||
organizer, user, vouchers, waitinglist,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,14 @@ urlpatterns = [
|
||||
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.OrganizerTeamView.as_view(), name='organizer.teams'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),
|
||||
name='organizer.team'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/edit$', organizer.TeamUpdateView.as_view(),
|
||||
name='organizer.team.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(),
|
||||
name='organizer.team.delete'),
|
||||
url(r'^events/$', main.EventList.as_view(), name='events'),
|
||||
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),
|
||||
url(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
|
||||
@@ -131,6 +138,6 @@ urlpatterns = [
|
||||
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||
url(r'^waitinglist/$', waitinglist.WaitingListView.as_view(), name='event.orders.waitinglist'),
|
||||
url(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
|
||||
url(r'^checkins/$', checkin.CheckInView.as_view(), name='event.orders.checkins'),
|
||||
])),
|
||||
url(r'^help/(?P<topic>[a-zA-Z0-9_/]+)$', help.HelpView.as_view(), name='help'),
|
||||
]
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.contrib.auth import (
|
||||
)
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -23,9 +24,7 @@ from u2flib_server.utils import rand_bytes
|
||||
from pretix.base.forms.auth import (
|
||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
EventPermission, OrganizerPermission, U2FDevice, User,
|
||||
)
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
@@ -108,35 +107,30 @@ def invite(request, token):
|
||||
ctx = {}
|
||||
|
||||
try:
|
||||
perm = EventPermission.objects.get(invite_token=token)
|
||||
desc = perm.event.name
|
||||
except EventPermission.DoesNotExist:
|
||||
try:
|
||||
perm = OrganizerPermission.objects.get(invite_token=token)
|
||||
desc = perm.organizer.name
|
||||
except OrganizerPermission.DoesNotExist:
|
||||
messages.error(request, _('You used an invalid link. Please copy the link from your email to the address bar '
|
||||
'and make sure it is correct and that the link has not been used before.'))
|
||||
return redirect('control:auth.login')
|
||||
inv = TeamInvite.objects.get(token=token)
|
||||
except TeamInvite.DoesNotExist:
|
||||
messages.error(request, _('You used an invalid link. Please copy the link from your email to the address bar '
|
||||
'and make sure it is correct and that the link has not been used before.'))
|
||||
return redirect('control:auth.login')
|
||||
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
if isinstance(perm, EventPermission):
|
||||
EventPermission.objects.get(event=perm.event, user=request.user)
|
||||
else:
|
||||
OrganizerPermission.objects.get(organizer=perm.organizer, user=request.user)
|
||||
if inv.team.members.filter(pk=request.user.pk).exists():
|
||||
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
|
||||
'this team.').format(desc))
|
||||
'this team.').format(inv.team.name))
|
||||
return redirect('control:index')
|
||||
else:
|
||||
with transaction.atomic():
|
||||
inv.team.members.add(request.user)
|
||||
inv.team.log_action(
|
||||
'pretix.team.member.joined', data={
|
||||
'email': request.user.email,
|
||||
'invite_email': inv.email,
|
||||
'user': request.user.pk
|
||||
}
|
||||
)
|
||||
inv.delete()
|
||||
messages.success(request, _('You are now part of the team "{}".').format(inv.team.name))
|
||||
return redirect('control:index')
|
||||
except (EventPermission.DoesNotExist, OrganizerPermission.DoesNotExist):
|
||||
pass
|
||||
|
||||
perm.invite_token = None
|
||||
perm.invite_email = None
|
||||
perm.user = request.user
|
||||
perm.save()
|
||||
messages.success(request, _('You have now access to "{}".').format(desc))
|
||||
return redirect('control:index')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = RegistrationForm(data=request.POST)
|
||||
@@ -151,14 +145,20 @@ def invite(request, token):
|
||||
auth_login(request, user)
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
|
||||
perm.invite_token = None
|
||||
perm.invite_email = None
|
||||
perm.user = user
|
||||
perm.save()
|
||||
messages.success(request, _('Welcome to pretix! You have now access to "{}".').format(desc))
|
||||
with transaction.atomic():
|
||||
inv.team.members.add(request.user)
|
||||
inv.team.log_action(
|
||||
'pretix.team.member.joined', data={
|
||||
'email': user.email,
|
||||
'invite_email': inv.email,
|
||||
'user': user.pk
|
||||
}
|
||||
)
|
||||
inv.delete()
|
||||
messages.success(request, _('Welcome to pretix! You are now part of the team "{}".').format(inv.team.name))
|
||||
return redirect('control:index')
|
||||
else:
|
||||
form = RegistrationForm(initial={'email': perm.invite_email})
|
||||
form = RegistrationForm(initial={'email': inv.email})
|
||||
ctx['form'] = form
|
||||
return render(request, 'pretixcontrol/auth/invite.html', ctx)
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.views.generic import ListView
|
||||
|
||||
from pretix.base.models import Checkin, Item, OrderPosition
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
|
||||
|
||||
class CheckInView(EventPermissionRequiredMixin, ListView):
|
||||
model = Checkin
|
||||
context_object_name = 'entries'
|
||||
paginate_by = 30
|
||||
template_name = 'pretixcontrol/checkin/index.html'
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
qs = OrderPosition.objects.filter(order__event=self.request.event, order__status='p')
|
||||
|
||||
# if this setting is False, we check only items for admission
|
||||
if not self.request.event.settings.ticket_download_nonadm:
|
||||
qs = qs.filter(item__admission=True)
|
||||
|
||||
if self.request.GET.get("status", "") != "":
|
||||
p = self.request.GET.get("status", "")
|
||||
if p == '1':
|
||||
# records with check-in record
|
||||
qs = qs.filter(checkins__isnull=False)
|
||||
elif p == '0':
|
||||
qs = qs.filter(checkins__isnull=True)
|
||||
|
||||
if self.request.GET.get("user", "") != "":
|
||||
u = self.request.GET.get("user", "")
|
||||
qs = qs.filter(
|
||||
Q(order__email__icontains=u) | Q(attendee_name__icontains=u) | Q(attendee_email__icontains=u)
|
||||
)
|
||||
|
||||
if self.request.GET.get("item", "") != "":
|
||||
u = self.request.GET.get("item", "")
|
||||
qs = qs.filter(item_id__in=(u,))
|
||||
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.filter(position__order__event=self.request.event))
|
||||
).select_related('order', 'item', 'addon_to')
|
||||
|
||||
if self.request.GET.get("ordering", "") != "":
|
||||
p = self.request.GET.get("ordering", "")
|
||||
keys_allowed = self.get_ordering_keys_mappings()
|
||||
if p in keys_allowed:
|
||||
mapped_field = keys_allowed[p]
|
||||
if type(mapped_field) is tuple:
|
||||
qs = qs.annotate(**mapped_field[1]).order_by(mapped_field[0])
|
||||
else:
|
||||
qs = qs.order_by(mapped_field)
|
||||
|
||||
return qs.distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['items'] = Item.objects.filter(event=self.request.event)
|
||||
ctx['filtered'] = ("status" in self.request.GET or "user" in self.request.GET or "item" in self.request.GET)
|
||||
return ctx
|
||||
|
||||
@staticmethod
|
||||
def get_ordering_keys_mappings():
|
||||
return {
|
||||
'code': 'order__code',
|
||||
'-code': '-order__code',
|
||||
'email': 'order__email',
|
||||
'-email': '-order__email',
|
||||
'status': 'checkins__id',
|
||||
'-status': '-checkins__id',
|
||||
'timestamp': 'checkins__datetime',
|
||||
'-timestamp': '-checkins__datetime',
|
||||
'item': 'item__name',
|
||||
'-item': '-item__name',
|
||||
'name': ('display_name', {'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
|
||||
'-name': ('-display_name', {'display_name': Coalesce('attendee_name', 'addon_to__attendee_name')}),
|
||||
}
|
||||
@@ -13,7 +13,7 @@ from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, Order, OrderPosition, Voucher, WaitingListEntry,
|
||||
Item, Order, OrderPosition, Voucher, WaitingListEntry,
|
||||
)
|
||||
from pretix.control.signals import (
|
||||
event_dashboard_widgets, user_dashboard_widgets,
|
||||
@@ -168,6 +168,27 @@ def shop_state_widget(sender, **kwargs):
|
||||
}]
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def checkin_widget(sender, **kwargs):
|
||||
size_qs = OrderPosition.objects.filter(order__event=sender, order__status='p')
|
||||
checked_qs = OrderPosition.objects.filter(order__event=sender, order__status='p', checkins__isnull=False)
|
||||
|
||||
# if this setting is False, we check only items for admission
|
||||
if not sender.settings.ticket_download_nonadm:
|
||||
size_qs = size_qs.filter(item__admission=True)
|
||||
checked_qs = checked_qs.filter(item__admission=True)
|
||||
|
||||
return [{
|
||||
'content': NUM_WIDGET.format(num='{}/{}'.format(checked_qs.count(), size_qs.count()), text=_('Checked in')),
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
'url': reverse('control:event.orders.checkins', kwargs={
|
||||
'event': sender.slug,
|
||||
'organizer': sender.organizer.slug
|
||||
})
|
||||
}]
|
||||
|
||||
|
||||
@receiver(signal=event_dashboard_widgets)
|
||||
def welcome_wizard_widget(sender, **kwargs):
|
||||
template = get_template('pretixcontrol/event/dashboard_widget_welcome.html')
|
||||
@@ -207,11 +228,12 @@ def event_index(request, organizer, event):
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event):
|
||||
widgets.extend(result)
|
||||
|
||||
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders')
|
||||
qs = request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not request.eventperm.can_view_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||||
if not request.eventperm.can_view_vouchers:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||
|
||||
a_qs = request.event.requiredaction_set.filter(done=False)
|
||||
@@ -221,7 +243,7 @@ def event_index(request, organizer, event):
|
||||
ctx = {
|
||||
'widgets': rearrange(widgets),
|
||||
'logs': qs[:5],
|
||||
'actions': a_qs[:5] if request.eventperm.can_change_orders else [],
|
||||
'actions': a_qs[:5] if can_change_orders else [],
|
||||
'has_domain': has_domain
|
||||
}
|
||||
|
||||
@@ -242,7 +264,8 @@ def event_index(request, organizer, event):
|
||||
def user_event_widgets(**kwargs):
|
||||
user = kwargs.pop('user')
|
||||
widgets = []
|
||||
events = Event.objects.filter(permitted__id__exact=user.pk).select_related("organizer").order_by('-date_from')
|
||||
|
||||
events = user.get_events_with_any_permission().order_by('-date_from', 'name').select_related('organizer')
|
||||
for event in events:
|
||||
widgets.append({
|
||||
'content': '<div class="event">{event}<span class="from">{df}</span><span class="to">{dt}</span></div>'.format(
|
||||
|
||||
@@ -2,14 +2,12 @@ import re
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.forms import modelformset_factory
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils import translation
|
||||
@@ -22,14 +20,12 @@ from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from pytz import timezone
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import (
|
||||
CachedTicket, Event, EventPermission, Item, ItemVariation, LogEntry, Order,
|
||||
RequiredAction, User, Voucher,
|
||||
CachedTicket, Event, Item, ItemVariation, LogEntry, Order, RequiredAction,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.signals import (
|
||||
event_live_issues, register_payment_providers, register_ticket_outputs,
|
||||
)
|
||||
@@ -50,7 +46,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = Event
|
||||
form_class = EventUpdateForm
|
||||
template_name = 'pretixcontrol/event/settings.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
@cached_property
|
||||
def object(self) -> Event:
|
||||
@@ -115,7 +111,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||
model = Event
|
||||
context_object_name = 'event'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixcontrol/event/plugins.html'
|
||||
|
||||
def get_object(self, queryset=None) -> Event:
|
||||
@@ -178,7 +174,7 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
|
||||
class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||
model = Event
|
||||
context_object_name = 'event'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixcontrol/event/payment.html'
|
||||
|
||||
def get_object(self, queryset=None) -> Event:
|
||||
@@ -264,7 +260,7 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
|
||||
|
||||
class EventSettingsFormView(EventPermissionRequiredMixin, FormView):
|
||||
model = Event
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
@@ -300,7 +296,7 @@ class InvoiceSettings(EventSettingsFormView):
|
||||
model = Event
|
||||
form_class = InvoiceSettingsForm
|
||||
template_name = 'pretixcontrol/event/invoicing.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
if 'preview' in self.request.POST:
|
||||
@@ -315,12 +311,13 @@ class InvoiceSettings(EventSettingsFormView):
|
||||
|
||||
|
||||
class InvoicePreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
pdf = build_preview_invoice_pdf(request.event)
|
||||
resp = HttpResponse(pdf, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="invoice-preview.pdf"'
|
||||
resp['Content-Security-Policy'] = "style-src 'unsafe-inline'; script-src 'unsafe-inline'; object-src 'self'"
|
||||
resp['Content-Disposition'] = 'inline; filename="invoice-preview.pdf"'
|
||||
return resp
|
||||
|
||||
|
||||
@@ -328,7 +325,7 @@ class DisplaySettings(EventSettingsFormView):
|
||||
model = Event
|
||||
form_class = DisplaySettingsForm
|
||||
template_name = 'pretixcontrol/event/display.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.display', kwargs={
|
||||
@@ -364,7 +361,7 @@ class MailSettings(EventSettingsFormView):
|
||||
model = Event
|
||||
form_class = MailSettingsForm
|
||||
template_name = 'pretixcontrol/event/mail.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.mail', kwargs={
|
||||
@@ -407,7 +404,7 @@ class MailSettings(EventSettingsFormView):
|
||||
|
||||
|
||||
class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
# return the origin text if key is missing in dict
|
||||
class SafeDict(dict):
|
||||
@@ -513,7 +510,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
|
||||
|
||||
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
@cached_property
|
||||
def output(self):
|
||||
@@ -531,7 +528,11 @@ class TicketSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
fname, mimet, data = tickets.preview(self.request.event.pk, self.output.identifier)
|
||||
resp = HttpResponse(data, content_type=mimet)
|
||||
ftype = fname.split(".")[-1]
|
||||
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
|
||||
if mimet == "application/pdf":
|
||||
resp['Content-Security-Policy'] = "style-src 'unsafe-inline'; script-src 'unsafe-inline'; object-src 'self'"
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
|
||||
return resp
|
||||
|
||||
def get_error_url(self) -> str:
|
||||
@@ -545,7 +546,7 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
|
||||
model = Event
|
||||
form_class = TicketSettingsForm
|
||||
template_name = 'pretixcontrol/event/tickets.html'
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
@@ -637,140 +638,12 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
|
||||
return providers
|
||||
|
||||
|
||||
class EventPermissionForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = EventPermission
|
||||
fields = (
|
||||
'can_change_settings', 'can_change_items', 'can_change_permissions', 'can_view_orders',
|
||||
'can_change_orders', 'can_view_vouchers', 'can_change_vouchers'
|
||||
)
|
||||
|
||||
|
||||
class EventPermissionCreateForm(EventPermissionForm):
|
||||
user = forms.EmailField(required=False, label=_('User'))
|
||||
|
||||
|
||||
class EventPermissions(EventPermissionRequiredMixin, TemplateView):
|
||||
model = Event
|
||||
form_class = TicketSettingsForm
|
||||
template_name = 'pretixcontrol/event/permissions.html'
|
||||
permission = 'can_change_permissions'
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
fs = modelformset_factory(
|
||||
EventPermission,
|
||||
form=EventPermissionForm,
|
||||
can_delete=True, can_order=False, extra=0
|
||||
)
|
||||
return fs(data=self.request.POST if self.request.method == "POST" else None,
|
||||
prefix="formset",
|
||||
queryset=EventPermission.objects.filter(event=self.request.event))
|
||||
|
||||
@cached_property
|
||||
def add_form(self):
|
||||
return EventPermissionCreateForm(data=self.request.POST if self.request.method == "POST" else None,
|
||||
prefix="add")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
ctx['add_form'] = self.add_form
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.invite_email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'event': self.request.event.name,
|
||||
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||
'token': instance.invite_token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, *args, **kwargs):
|
||||
if self.formset.is_valid() and self.add_form.is_valid():
|
||||
if self.add_form.has_changed():
|
||||
logdata = {
|
||||
k: v for k, v in self.add_form.cleaned_data.items()
|
||||
}
|
||||
|
||||
try:
|
||||
self.add_form.instance.event = self.request.event
|
||||
self.add_form.instance.event_id = self.request.event.id
|
||||
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
|
||||
self.add_form.instance.user_id = self.add_form.instance.user.id
|
||||
except User.DoesNotExist:
|
||||
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
|
||||
if EventPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
|
||||
event=self.request.event).exists():
|
||||
messages.error(self.request, _('This user already has been invited for this event.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
self.add_form.save()
|
||||
self._send_invite(self.add_form.instance)
|
||||
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.invited', user=self.request.user, data=logdata
|
||||
)
|
||||
else:
|
||||
if EventPermission.objects.filter(user=self.add_form.instance.user,
|
||||
event=self.request.event).exists():
|
||||
messages.error(self.request, _('This user already has permissions for this event.'))
|
||||
return self.get(*args, **kwargs)
|
||||
self.add_form.save()
|
||||
logdata['user'] = self.add_form.instance.user_id
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.added', user=self.request.user, data=logdata
|
||||
)
|
||||
for form in self.formset.forms:
|
||||
if form.has_changed():
|
||||
changedata = {
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
changedata['user'] = form.instance.user_id
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.changed', user=self.request.user, data=changedata
|
||||
)
|
||||
if form.instance.user_id == self.request.user.pk:
|
||||
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
|
||||
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
for form in self.formset.deleted_forms:
|
||||
logdata = {
|
||||
k: v for k, v in form.cleaned_data.items()
|
||||
}
|
||||
self.request.event.log_action(
|
||||
'pretix.event.permissions.deleted', user=self.request.user, data=logdata
|
||||
)
|
||||
|
||||
self.formset.save()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.permissions', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug
|
||||
})
|
||||
|
||||
|
||||
class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_settings'
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixcontrol/event/live.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -840,9 +713,9 @@ class EventLog(EventPermissionRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not self.request.eventperm.can_view_orders:
|
||||
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||||
if not self.request.eventperm.can_view_vouchers:
|
||||
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_vouchers'):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||
|
||||
if self.request.GET.get('user') == 'yes':
|
||||
@@ -856,7 +729,7 @@ class EventLog(EventPermissionRequiredMixin, ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['userlist'] = self.request.event.user_perms.select_related('user')
|
||||
ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
from django import template
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import View
|
||||
|
||||
from pretix.base.models import Organizer
|
||||
|
||||
|
||||
class HelpView(View):
|
||||
model = Organizer
|
||||
context_object_name = 'organizers'
|
||||
template_name = 'pretixcontrol/organizers/index.html'
|
||||
paginate_by = 30
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# In a security review, this came up as a possible path traversal issue. However, the URL regex
|
||||
# does not allow any dots in the argument (which forbids traversing upwards in the directory tree).
|
||||
# Even if it *was* possbile, it'd be loaded through django's template loader and therefore limited
|
||||
# to TEMPLATE_DIR.
|
||||
try:
|
||||
locale = request.LANGUAGE_CODE
|
||||
return render(request, 'pretixcontrol/help/%s.%s.html' % (kwargs.get('topic'), locale), {})
|
||||
except template.TemplateDoesNotExist:
|
||||
try:
|
||||
return render(request, 'pretixcontrol/help/%s.html' % kwargs.get('topic'), {})
|
||||
except template.TemplateDoesNotExist:
|
||||
raise Http404('')
|
||||
@@ -4,7 +4,7 @@ from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, Q
|
||||
from django.db.models import Count, F, Max, Q
|
||||
from django.forms.models import ModelMultipleChoiceField, inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
@@ -72,7 +72,7 @@ def item_move(request, item, up=True):
|
||||
if item.position != i:
|
||||
item.position = i
|
||||
item.save()
|
||||
messages.success(request, _('The order of items as been updated.'))
|
||||
messages.success(request, _('The order of items has been updated.'))
|
||||
|
||||
|
||||
@event_permission_required("can_change_items")
|
||||
@@ -803,6 +803,8 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
||||
form.instance.min_per_order = form.cleaned_data['copy_from'].min_per_order
|
||||
form.instance.max_per_order = form.cleaned_data['copy_from'].max_per_order
|
||||
|
||||
form.instance.position = (self.request.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={
|
||||
k: (form.cleaned_data.get(k).name
|
||||
@@ -943,7 +945,7 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
formsetclass = inlineformset_factory(
|
||||
Item, ItemAddOn,
|
||||
form=ItemAddOnForm, formset=ItemAddOnsFormSet,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
can_order=True, can_delete=True, extra=0
|
||||
)
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
queryset=ItemAddOn.objects.filter(base_item=self.get_object()),
|
||||
@@ -963,12 +965,13 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
|
||||
forms = [
|
||||
ef for ef in self.formset.extra_forms + self.formset.initial_forms
|
||||
if ef not in self.formset.deleted_forms
|
||||
forms = self.formset.ordered_forms + [
|
||||
ef for ef in self.formset.extra_forms
|
||||
if ef not in self.formset.ordered_forms and ef not in self.formset.deleted_forms
|
||||
]
|
||||
for i, form in enumerate(forms):
|
||||
form.instance.base_item = self.get_object()
|
||||
form.instance.position = i
|
||||
created = not form.instance.pk
|
||||
form.save()
|
||||
if form.has_changed():
|
||||
@@ -986,7 +989,7 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self.get_object().category and self.get_object().category.is_addon:
|
||||
messages.error(self.request, _('Your cannot add addons to a product that is only available as an add-on '
|
||||
messages.error(self.request, _('You cannot add addons to a product that is only available as an add-on '
|
||||
'itself.'))
|
||||
return redirect(self.get_previous_url())
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import ListView
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
|
||||
from pretix.base.models import Event, EventPermission, OrganizerPermission
|
||||
from pretix.base.models import Event, Team
|
||||
from pretix.control.forms.event import (
|
||||
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
|
||||
)
|
||||
@@ -20,22 +20,13 @@ class EventList(ListView):
|
||||
template_name = 'pretixcontrol/events/index.html'
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
return Event.objects.all().select_related("organizer").prefetch_related(
|
||||
"_settings_objects", "organizer___settings_objects"
|
||||
)
|
||||
else:
|
||||
return Event.objects.filter(
|
||||
permitted__id__exact=self.request.user.pk
|
||||
).select_related("organizer").prefetch_related(
|
||||
"_settings_objects", "organizer___settings_objects"
|
||||
)
|
||||
return self.request.user.get_events_with_any_permission().select_related('organizer').prefetch_related(
|
||||
'_settings_objects', 'organizer___settings_objects'
|
||||
)
|
||||
|
||||
|
||||
def condition_copy(wizard):
|
||||
return EventPermission.objects.filter(
|
||||
user=wizard.request.user, can_change_settings=True, can_change_items=True
|
||||
).exists()
|
||||
return EventWizardCopyForm.copy_from_queryset(wizard.request.user).exists()
|
||||
|
||||
|
||||
class EventWizard(SessionWizardView):
|
||||
@@ -55,8 +46,7 @@ class EventWizard(SessionWizardView):
|
||||
|
||||
def get_context_data(self, form, **kwargs):
|
||||
ctx = super().get_context_data(form, **kwargs)
|
||||
ctx['has_organizer'] = OrganizerPermission.objects.filter(user=self.request.user,
|
||||
can_create_events=True).exists()
|
||||
ctx['has_organizer'] = self.request.user.teams.filter(can_create_events=True).exists()
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self, step=None):
|
||||
@@ -81,7 +71,20 @@ class EventWizard(SessionWizardView):
|
||||
event.organizer = foundation_data['organizer']
|
||||
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
||||
form_dict['basics'].save()
|
||||
EventPermission.objects.create(event=event, user=self.request.user)
|
||||
|
||||
has_control_rights = self.request.user.teams.filter(
|
||||
organizer=event.organizer, all_events=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_change_orders=True, can_change_vouchers=True
|
||||
).exists()
|
||||
if not has_control_rights:
|
||||
t = Team.objects.create(
|
||||
organizer=event.organizer, name=_('Team {event}').format(event=event.name),
|
||||
can_change_event_settings=True, can_change_items=True,
|
||||
can_view_orders=True, can_change_orders=True, can_view_vouchers=True,
|
||||
can_change_vouchers=True
|
||||
)
|
||||
t.members.add(self.request.user)
|
||||
t.limit_events.add(event)
|
||||
|
||||
logdata = {}
|
||||
for f in form_list:
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.forms import modelformset_factory
|
||||
from django.shortcuts import redirect
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import CreateView, DetailView, ListView, UpdateView
|
||||
from django.views.generic import (
|
||||
CreateView, DeleteView, DetailView, ListView, UpdateView,
|
||||
)
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models import Organizer, OrganizerPermission, User
|
||||
from pretix.base.models import Organizer, Team, TeamInvite, User
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.forms.organizer import OrganizerForm, OrganizerUpdateForm
|
||||
from pretix.control.forms.organizer import (
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
||||
)
|
||||
from pretix.control.permissions import OrganizerPermissionRequiredMixin
|
||||
from pretix.control.signals import nav_organizer
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
@@ -28,25 +32,14 @@ class OrganizerList(ListView):
|
||||
if self.request.user.is_superuser:
|
||||
return Organizer.objects.all()
|
||||
else:
|
||||
return Organizer.objects.filter(
|
||||
permitted__id__exact=self.request.user.pk
|
||||
)
|
||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||
|
||||
|
||||
class OrganizerPermissionForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = OrganizerPermission
|
||||
fields = (
|
||||
'can_create_events', 'can_change_permissions'
|
||||
)
|
||||
|
||||
|
||||
class OrganizerPermissionCreateForm(OrganizerPermissionForm):
|
||||
class InviteForm(forms.Form):
|
||||
user = forms.EmailField(required=False, label=_('User'))
|
||||
|
||||
|
||||
class OrganizerDetailViewMixin:
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['nav_organizer'] = []
|
||||
@@ -82,141 +75,55 @@ class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
|
||||
permission = 'can_change_permissions'
|
||||
context_object_name = 'organizer'
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
fs = modelformset_factory(
|
||||
OrganizerPermission,
|
||||
form=OrganizerPermissionForm,
|
||||
can_delete=True, can_order=False, extra=0
|
||||
)
|
||||
return fs(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and 'formset-TOTAL_FORMS' in self.request.POST
|
||||
else None
|
||||
),
|
||||
prefix="formset",
|
||||
queryset=OrganizerPermission.objects.filter(organizer=self.request.organizer)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def add_form(self):
|
||||
return OrganizerPermissionCreateForm(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and 'formset-TOTAL_FORMS' in self.request.POST
|
||||
else None
|
||||
),
|
||||
prefix="add"
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
ctx['add_form'] = self.add_form
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.invite_email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation_organizer.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.request.organizer.name,
|
||||
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||
'token': instance.invite_token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, *args, **kwargs):
|
||||
if self.formset.is_valid() and self.add_form.is_valid():
|
||||
if self.add_form.has_changed():
|
||||
logdata = {
|
||||
k: v for k, v in self.add_form.cleaned_data.items()
|
||||
}
|
||||
|
||||
try:
|
||||
self.add_form.instance.organizer = self.request.organizer
|
||||
self.add_form.instance.organizer_id = self.request.organizer.id
|
||||
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
|
||||
self.add_form.instance.user_id = self.add_form.instance.user.id
|
||||
except User.DoesNotExist:
|
||||
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
|
||||
if OrganizerPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
|
||||
organizer=self.request.organizer).exists():
|
||||
messages.error(self.request, _('This user already has been invited for this team.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
self.add_form.save()
|
||||
self._send_invite(self.add_form.instance)
|
||||
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.invited', user=self.request.user, data=logdata
|
||||
)
|
||||
else:
|
||||
if OrganizerPermission.objects.filter(user=self.add_form.instance.user,
|
||||
organizer=self.request.organizer).exists():
|
||||
messages.error(self.request, _('This user already has permissions for this team.'))
|
||||
return self.get(*args, **kwargs)
|
||||
self.add_form.save()
|
||||
logdata['user'] = self.add_form.instance.user_id
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.added', user=self.request.user, data=logdata
|
||||
)
|
||||
for form in self.formset.forms:
|
||||
if form.has_changed():
|
||||
changedata = {
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
changedata['user'] = form.instance.user_id
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.changed', user=self.request.user, data=changedata
|
||||
)
|
||||
if form.instance.user_id == self.request.user.pk:
|
||||
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
|
||||
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
for form in self.formset.deleted_forms:
|
||||
logdata = {
|
||||
k: v for k, v in form.cleaned_data.items()
|
||||
}
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.permissions.deleted', user=self.request.user, data=logdata
|
||||
)
|
||||
|
||||
self.formset.save()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizer.teams', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
|
||||
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = Organizer
|
||||
form_class = OrganizerUpdateForm
|
||||
template_name = 'pretixcontrol/organizers/edit.html'
|
||||
permission = None
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'organizer'
|
||||
|
||||
def get_object(self, queryset=None) -> Organizer:
|
||||
@cached_property
|
||||
def object(self) -> Organizer:
|
||||
return self.request.organizer
|
||||
|
||||
def get_object(self, queryset=None) -> Organizer:
|
||||
return self.object
|
||||
|
||||
@cached_property
|
||||
def sform(self):
|
||||
return OrganizerSettingsForm(
|
||||
obj=self.object,
|
||||
prefix='settings',
|
||||
data=self.request.POST if self.request.method == 'POST' else None,
|
||||
files=self.request.FILES if self.request.method == 'POST' else None
|
||||
)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['sform'] = self.sform
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
self.sform.save()
|
||||
if self.sform.has_changed():
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.settings',
|
||||
user=self.request.user,
|
||||
data={
|
||||
k: (self.sform.cleaned_data.get(k).name
|
||||
if isinstance(self.sform.cleaned_data.get(k), File)
|
||||
else self.sform.cleaned_data.get(k))
|
||||
for k in self.sform.changed_data
|
||||
}
|
||||
)
|
||||
if form.has_changed():
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.changed',
|
||||
user=self.request.user,
|
||||
data={k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
)
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
@@ -231,6 +138,13 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid() and self.sform.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class OrganizerCreate(CreateView):
|
||||
model = Organizer
|
||||
@@ -243,14 +157,268 @@ class OrganizerCreate(CreateView):
|
||||
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)
|
||||
OrganizerPermission.objects.create(
|
||||
organizer=form.instance, user=self.request.user,
|
||||
can_create_events=True
|
||||
t = Team.objects.create(
|
||||
organizer=form.instance, name=_('Administrators'),
|
||||
all_events=True, can_create_events=True, can_change_teams=True,
|
||||
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
|
||||
)
|
||||
t.members.add(self.request.user)
|
||||
return ret
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizers')
|
||||
|
||||
|
||||
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/teams.html'
|
||||
permission = 'can_change_teams'
|
||||
context_object_name = 'teams'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.teams.annotate(
|
||||
memcount=Count('members', distinct=True),
|
||||
eventcount=Count('limit_events', distinct=True),
|
||||
invcount=Count('invites', distinct=True)
|
||||
).all()
|
||||
|
||||
|
||||
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/team_edit.html'
|
||||
permission = 'can_change_teams'
|
||||
form_class = TeamForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.team', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'team': self.object.pk
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The team has been created. You can now add members to the team.'))
|
||||
form.instance.organizer = self.request.organizer
|
||||
ret = super().form_valid(form)
|
||||
form.instance.members.add(self.request.user)
|
||||
form.instance.log_action('pretix.team.created', user=self.request.user, data={
|
||||
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
})
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/team_edit.html'
|
||||
permission = 'can_change_teams'
|
||||
context_object_name = 'team'
|
||||
form_class = TeamForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.team', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'team': self.object.pk
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
self.object.log_action('pretix.team.changed', user=self.request.user, data={
|
||||
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
})
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/team_delete.html'
|
||||
permission = 'can_change_teams'
|
||||
context_object_name = 'team'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.teams', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['possible'] = self.is_allowed()
|
||||
return context
|
||||
|
||||
def is_allowed(self) -> bool:
|
||||
return self.request.organizer.teams.exclude(pk=self.kwargs.get('team')).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists()
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
success_url = self.get_success_url()
|
||||
self.object = self.get_object()
|
||||
if self.is_allowed():
|
||||
self.object.log_action('pretix.team.deleted', user=self.request.user)
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected team has been deleted.'))
|
||||
return redirect(success_url)
|
||||
else:
|
||||
messages.error(request, _('The selected team cannot be deleted.'))
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
template_name = 'pretixcontrol/organizers/team_members.html'
|
||||
context_object_name = 'team'
|
||||
permission = 'can_change_teams'
|
||||
model = Team
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Team, organizer=self.request.organizer, pk=self.kwargs.get('team'))
|
||||
|
||||
@cached_property
|
||||
def add_form(self):
|
||||
return InviteForm(data=self.request.POST if self.request.method == "POST" else None)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['add_form'] = self.add_form
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.request.organizer.name,
|
||||
'team': instance.team.name,
|
||||
'url': build_absolute_uri('control:auth.invite', kwargs={
|
||||
'token': instance.token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
if 'remove-member' in request.POST:
|
||||
try:
|
||||
user = User.objects.get(pk=request.POST.get('remove-member'))
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists()
|
||||
if not other_admin_teams and self.object.can_change_teams and self.object.members.count() == 1:
|
||||
messages.error(self.request, _('You cannot remove the last member from this team as noone would '
|
||||
'be left with the permission to change teams.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
self.object.members.remove(user)
|
||||
self.object.log_action(
|
||||
'pretix.team.member.removed', user=self.request.user, data={
|
||||
'email': user.email,
|
||||
'user': user.pk
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The member has been removed from the team.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
elif 'remove-invite' in request.POST:
|
||||
try:
|
||||
invite = self.object.invites.get(pk=request.POST.get('remove-invite'))
|
||||
except TeamInvite.DoesNotExist:
|
||||
messages.error(self.request, _('Invalid invite selected.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
invite.delete()
|
||||
self.object.log_action(
|
||||
'pretix.team.invite.deleted', user=self.request.user, data={
|
||||
'email': invite.email
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The invite has been revoked.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
elif self.add_form.is_valid() and self.add_form.has_changed():
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=self.add_form.cleaned_data['user'])
|
||||
except User.DoesNotExist:
|
||||
if self.object.invites.filter(email=self.add_form.cleaned_data['user']).exists():
|
||||
messages.error(self.request, _('This user already has been invited for this team.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
invite = self.object.invites.create(email=self.add_form.cleaned_data['user'])
|
||||
self._send_invite(invite)
|
||||
self.object.log_action(
|
||||
'pretix.team.invite.created', user=self.request.user, data={
|
||||
'email': self.add_form.cleaned_data['user']
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The new member has been invited to the team.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
if self.object.members.filter(pk=user.pk).exists():
|
||||
messages.error(self.request, _('This user already has permissions for this team.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
self.object.members.add(user)
|
||||
self.object.log_action(
|
||||
'pretix.team.member.added', user=self.request.user,
|
||||
data={
|
||||
'email': user.email,
|
||||
'user': user.pk,
|
||||
}
|
||||
)
|
||||
messages.success(self.request, _('The new member has been added to the team.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ class WaitingListView(EventPermissionRequiredMixin, ListView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'assign' in request.POST:
|
||||
if not request.eventperm.can_change_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
|
||||
messages.error(request, _('You do not have permission to do this'))
|
||||
return redirect(reverse('control:event.orders.waitinglist', kwargs={
|
||||
'event': request.event.slug,
|
||||
|
||||
@@ -6,12 +6,16 @@ def daterange(df, dt):
|
||||
lng = get_language()
|
||||
|
||||
if lng.startswith("de"):
|
||||
if df.year == dt.year and df.month == dt.month:
|
||||
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
|
||||
return "{}".format(_date(df, "j. F Y"))
|
||||
elif df.year == dt.year and df.month == dt.month:
|
||||
return "{}.–{}".format(_date(df, "j"), _date(dt, "j. F Y"))
|
||||
elif df.year == dt.year:
|
||||
return "{} – {}".format(_date(df, "j. F"), _date(dt, "j. F Y"))
|
||||
elif lng.startswith("en"):
|
||||
if df.year == dt.year and df.month == dt.month:
|
||||
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
|
||||
return "{}".format(_date(df, "N jS, Y"))
|
||||
elif df.year == dt.year and df.month == dt.month:
|
||||
return "{} – {}".format(_date(df, "N jS"), _date(dt, "jS, Y"))
|
||||
elif df.year == dt.year:
|
||||
return "{} – {}".format(_date(df, "N jS"), _date(dt, "N jS, Y"))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Date according to https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||
SHORT_DATE_FORMAT = 'Y-m-d'
|
||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
||||
TIME_FORMAT = 'H:i'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-04-17 15:09+0000\n"
|
||||
"PO-Revision-Date: 2017-04-17 17:10+0200\n"
|
||||
"POT-Creation-Date: 2017-05-11 10:12+0000\n"
|
||||
"PO-Revision-Date: 2017-05-11 12:13+0200\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.8.11\n"
|
||||
"X-Generator: Poedit 2.0.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
||||
@@ -46,6 +46,96 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:21
|
||||
msgid "QR Code"
|
||||
msgstr "QR-Code"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:157
|
||||
msgid "Sample product"
|
||||
msgstr "Beispielprodukt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:158
|
||||
msgid "Sample variation"
|
||||
msgstr "Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:159
|
||||
msgid "Sample product – sample variation"
|
||||
msgstr "Beispielprodukt – Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:160
|
||||
msgid "123.45 EUR"
|
||||
msgstr "123,45 EUR"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:161
|
||||
msgid "John Doe"
|
||||
msgstr "Max Mustermann"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:162
|
||||
msgid "Sample event name"
|
||||
msgstr "Beispielevent"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:163
|
||||
msgid "May 31st, 2017"
|
||||
msgstr "31. Mai 2017"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:164
|
||||
msgid "20:00"
|
||||
msgstr "20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:165
|
||||
msgid "19:00"
|
||||
msgstr "19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:166
|
||||
msgid "2017-05-31 20:00"
|
||||
msgstr "31.05.2016 20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:167
|
||||
msgid "2017-05-31 19:00"
|
||||
msgstr "31.05.2016 19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:168
|
||||
msgid "Random City"
|
||||
msgstr "Musterstadt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:213
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:362
|
||||
msgid "Group of objects"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:368
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:370
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:372
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:376
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:612
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:644
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:657
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asyncdownload.js:28
|
||||
#: pretix/static/pretixbase/js/asynctask.js:42
|
||||
#: pretix/static/pretixbase/js/asynctask.js:94
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-04-17 15:09+0000\n"
|
||||
"PO-Revision-Date: 2017-04-17 17:09+0200\n"
|
||||
"POT-Creation-Date: 2017-05-11 10:12+0000\n"
|
||||
"PO-Revision-Date: 2017-05-11 12:13+0200\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.8.11\n"
|
||||
"X-Generator: Poedit 2.0.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:53
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:59
|
||||
@@ -46,6 +46,96 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:21
|
||||
msgid "QR Code"
|
||||
msgstr "QR-Code"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:157
|
||||
msgid "Sample product"
|
||||
msgstr "Beispielprodukt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:158
|
||||
msgid "Sample variation"
|
||||
msgstr "Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:159
|
||||
msgid "Sample product – sample variation"
|
||||
msgstr "Beispielprodukt – Beispielvariante"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:160
|
||||
msgid "123.45 EUR"
|
||||
msgstr "123,45 EUR"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:161
|
||||
msgid "John Doe"
|
||||
msgstr "Max Mustermann"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:162
|
||||
msgid "Sample event name"
|
||||
msgstr "Beispielevent"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:163
|
||||
msgid "May 31st, 2017"
|
||||
msgstr "31. Mai 2017"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:164
|
||||
msgid "20:00"
|
||||
msgstr "20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:165
|
||||
msgid "19:00"
|
||||
msgstr "19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:166
|
||||
msgid "2017-05-31 20:00"
|
||||
msgstr "31.05.2016 20:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:167
|
||||
msgid "2017-05-31 19:00"
|
||||
msgstr "31.05.2016 19:00"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:168
|
||||
msgid "Random City"
|
||||
msgstr "Musterstadt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:213
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:362
|
||||
msgid "Group of objects"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:368
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:370
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:372
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:376
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:612
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:644
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js:657
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asyncdownload.js:28
|
||||
#: pretix/static/pretixbase/js/asynctask.js:42
|
||||
#: pretix/static/pretixbase/js/asynctask.js:94
|
||||
|
||||
@@ -17,7 +17,7 @@ def register_payment_provider(sender, **kwargs):
|
||||
@receiver(nav_event, dispatch_uid="payment_banktransfer_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.eventperm.can_change_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -39,6 +39,10 @@ class Paypal(BasePaymentProvider):
|
||||
('client_id',
|
||||
forms.CharField(
|
||||
label=_('Client ID'),
|
||||
help_text=_('<a target="_blank" href="{docs_url}">{text}</a>').format(
|
||||
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html'
|
||||
)
|
||||
)),
|
||||
('secret',
|
||||
forms.CharField(
|
||||
|
||||
@@ -57,7 +57,19 @@ def success(request, *args, **kwargs):
|
||||
@event_view(require_live=False)
|
||||
def abort(request, *args, **kwargs):
|
||||
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||
|
||||
if request.session.get('payment_paypal_order'):
|
||||
order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
|
||||
else:
|
||||
order = None
|
||||
|
||||
if order:
|
||||
return redirect(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}) + ('?paid=yes' if order.status == Order.STATUS_PAID else ''))
|
||||
else:
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import json
|
||||
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.signals import logentry_display
|
||||
@@ -11,7 +14,7 @@ from pretix.control.signals import nav_event
|
||||
@receiver(nav_event, dispatch_uid="pretixdroid_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.eventperm.can_change_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
@@ -32,11 +35,33 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
return
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
|
||||
show_dt = False
|
||||
if 'datetime' in data:
|
||||
dt = dateutil.parser.parse(data.get('datetime'))
|
||||
show_dt = abs((logentry.datetime - dt).total_seconds()) > 60 or 'forced' in data
|
||||
tz = pytz.timezone(sender.settings.timezone)
|
||||
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
|
||||
|
||||
if data.get('first'):
|
||||
return _('Position #{posid} has been scanned.'.format(
|
||||
posid=data.get('positionid')
|
||||
))
|
||||
if show_dt:
|
||||
return _('Position #{posid} has been scanned at {datetime}.').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
)
|
||||
else:
|
||||
return _('Position #{posid} has been scanned.').format(
|
||||
posid=data.get('positionid')
|
||||
)
|
||||
else:
|
||||
if data.get('forced'):
|
||||
return _(
|
||||
'A scan for position #{posid} at {datetime} has been uploaded even though it has '
|
||||
'been scanned already.'.format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted
|
||||
)
|
||||
)
|
||||
return _('Position #{posid} has been scanned and rejected because it has already been scanned before.'.format(
|
||||
posid=data.get('positionid')
|
||||
))
|
||||
|
||||
@@ -9,6 +9,8 @@ urlpatterns = [
|
||||
name='api.redeem'),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/search/', views.ApiSearchView.as_view(),
|
||||
name='api.search'),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/download/', views.ApiDownloadView.as_view(),
|
||||
name='api.download'),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/status/', views.ApiStatusView.as_view(),
|
||||
name='api.status'),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
import logging
|
||||
import string
|
||||
|
||||
import dateutil.parser
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Q
|
||||
from django.http import (
|
||||
@@ -9,6 +10,7 @@ from django.http import (
|
||||
)
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import now
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
@@ -19,7 +21,7 @@ from pretix.multidomain.urlreverse import \
|
||||
build_absolute_uri as event_absolute_uri
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.pretixdroid')
|
||||
API_VERSION = 2
|
||||
API_VERSION = 3
|
||||
|
||||
|
||||
class ConfigView(EventPermissionRequiredMixin, TemplateView):
|
||||
@@ -67,37 +69,55 @@ class ApiView(View):
|
||||
class ApiRedeemView(ApiView):
|
||||
def post(self, request, **kwargs):
|
||||
secret = request.POST.get('secret', '!INVALID!')
|
||||
force = request.POST.get('force', 'false') in ('true', 'True')
|
||||
nonce = request.POST.get('nonce')
|
||||
response = {
|
||||
'version': API_VERSION
|
||||
}
|
||||
|
||||
if 'datetime' in request.POST:
|
||||
dt = dateutil.parser.parse(request.POST.get('datetime'))
|
||||
else:
|
||||
dt = now()
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
created = False
|
||||
op = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').get(
|
||||
order__event=self.event, secret=secret
|
||||
)
|
||||
if op.order.status == Order.STATUS_PAID:
|
||||
ci, created = Checkin.objects.get_or_create(position=op)
|
||||
if op.order.status == Order.STATUS_PAID or force:
|
||||
ci, created = Checkin.objects.get_or_create(position=op, defaults={
|
||||
'datetime': dt,
|
||||
'nonce': nonce,
|
||||
})
|
||||
else:
|
||||
response['status'] = 'error'
|
||||
response['reason'] = 'unpaid'
|
||||
|
||||
if 'status' not in response:
|
||||
if created:
|
||||
if created or (nonce and nonce == ci.nonce):
|
||||
response['status'] = 'ok'
|
||||
op.order.log_action('pretix.plugins.pretixdroid.scan', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': True,
|
||||
})
|
||||
if created:
|
||||
op.order.log_action('pretix.plugins.pretixdroid.scan', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': True,
|
||||
'forced': op.order.status != Order.STATUS_PAID,
|
||||
'datetime': dt,
|
||||
})
|
||||
else:
|
||||
response['status'] = 'error'
|
||||
response['reason'] = 'already_redeemed'
|
||||
if force:
|
||||
response['status'] = 'ok'
|
||||
else:
|
||||
response['status'] = 'error'
|
||||
response['reason'] = 'already_redeemed'
|
||||
op.order.log_action('pretix.plugins.pretixdroid.scan', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': False,
|
||||
'forced': force,
|
||||
'datetime': dt,
|
||||
})
|
||||
|
||||
response['data'] = {
|
||||
@@ -115,6 +135,18 @@ class ApiRedeemView(ApiView):
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
def serialize_op(op):
|
||||
return {
|
||||
'secret': op.secret,
|
||||
'order': op.order.code,
|
||||
'item': str(op.item),
|
||||
'variation': str(op.variation) if op.variation else None,
|
||||
'attendee_name': op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''),
|
||||
'redeemed': bool(op.checkin_cnt),
|
||||
'paid': op.order.status == Order.STATUS_PAID,
|
||||
}
|
||||
|
||||
|
||||
class ApiSearchView(ApiView):
|
||||
def get(self, request, **kwargs):
|
||||
query = request.GET.get('query', '!INVALID!')
|
||||
@@ -123,30 +155,34 @@ class ApiSearchView(ApiView):
|
||||
}
|
||||
|
||||
if len(query) >= 4:
|
||||
ops = OrderPosition.objects.select_related('item', 'variation', 'order').filter(
|
||||
ops = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
|
||||
Q(order__event=self.event)
|
||||
& Q(
|
||||
Q(secret__istartswith=query) | Q(attendee_name__icontains=query) | Q(order__code__istartswith=query)
|
||||
)
|
||||
).prefetch_related('checkins')[:25]
|
||||
).annotate(checkin_cnt=Count('checkins'))[:25]
|
||||
|
||||
response['results'] = [
|
||||
{
|
||||
'secret': op.secret,
|
||||
'order': op.order.code,
|
||||
'item': str(op.item),
|
||||
'variation': str(op.variation) if op.variation else None,
|
||||
'attendee_name': op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''),
|
||||
'redeemed': bool(op.checkins.all()),
|
||||
'paid': op.order.status == Order.STATUS_PAID,
|
||||
} for op in ops
|
||||
]
|
||||
response['results'] = [serialize_op(op) for op in ops]
|
||||
else:
|
||||
response['results'] = []
|
||||
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
class ApiDownloadView(ApiView):
|
||||
def get(self, request, **kwargs):
|
||||
response = {
|
||||
'version': API_VERSION
|
||||
}
|
||||
|
||||
ops = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
|
||||
Q(order__event=self.event)
|
||||
).annotate(checkin_cnt=Count('checkins'))
|
||||
response['results'] = [serialize_op(op) for op in ops]
|
||||
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
class ApiStatusView(ApiView):
|
||||
def get(self, request, **kwargs):
|
||||
response = {
|
||||
|
||||
@@ -9,7 +9,7 @@ from pretix.control.signals import nav_event
|
||||
@receiver(nav_event, dispatch_uid="sendmail_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.eventperm.can_change_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<pre>{{ value.message|linebreaksbr }}</pre>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<a href="{% url 'plugins:sendmail:send' organizer=request.event.organizer.slug event=request.event.slug %}?from_log={{ log.id }}">{% trans "Send a new email based on this" %}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -4,18 +4,18 @@ from datetime import timedelta
|
||||
import pytz
|
||||
from django.contrib import messages
|
||||
from django.db.models import Q
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import FormView, ListView
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import LogEntry, Order
|
||||
from pretix.base.i18n import LazyI18nString, language
|
||||
from pretix.base.models import InvoiceAddress, LogEntry, Order
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
from . import forms
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.sendmail')
|
||||
@@ -29,6 +29,21 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
if 'from_log' in self.request.GET:
|
||||
try:
|
||||
from_log_id = self.request.GET.get('from_log')
|
||||
logentry = LogEntry.objects.get(
|
||||
id=from_log_id,
|
||||
event=self.request.event,
|
||||
action_type='pretix.plugins.sendmail.sent'
|
||||
)
|
||||
kwargs['initial'] = {
|
||||
'message': LazyI18nString(logentry.parsed_data['message']),
|
||||
'subject': LazyI18nString(logentry.parsed_data['subject']),
|
||||
'sendto': logentry.parsed_data['sendto'],
|
||||
}
|
||||
except LogEntry.DoesNotExist:
|
||||
raise Http404(_('You supplied an invalid log entry ID'))
|
||||
return kwargs
|
||||
|
||||
def form_invalid(self, form):
|
||||
@@ -49,54 +64,71 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
if not orders:
|
||||
messages.error(self.request, _('There are no orders matching this selection.'))
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
|
||||
if self.request.POST.get("action") == "preview":
|
||||
for l in self.request.event.settings.locales:
|
||||
with language(l):
|
||||
self.output[l] = []
|
||||
self.output[l].append(
|
||||
_('Subject: {subject}').format(subject=form.cleaned_data['subject'].localize(l)))
|
||||
message = form.cleaned_data['message'].localize(l)
|
||||
preview_text = message.format(
|
||||
order='ORDER1234',
|
||||
event=self.request.event.name,
|
||||
order_date=date_format(now(), 'SHORT_DATE_FORMAT'),
|
||||
due_date=date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'),
|
||||
order_url=build_absolute_uri(self.request.event, 'presale:event.order', kwargs={
|
||||
'order': 'ORDER1234',
|
||||
'secret': 'longrandomsecretabcdef123456'
|
||||
}),
|
||||
invoice_name=_('John Doe'),
|
||||
invoice_company=_('Sample Company LLC'),
|
||||
)
|
||||
self.output[l].append(preview_text)
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
|
||||
for o in orders:
|
||||
if self.request.POST.get("action") == "preview":
|
||||
for l in self.request.event.settings.locales:
|
||||
with language(l):
|
||||
self.output[l] = []
|
||||
self.output[l].append(_('Subject: {subject}').format(subject=form.cleaned_data['subject'].localize(l)))
|
||||
message = form.cleaned_data['message'].localize(l)
|
||||
preview_text = message.format(
|
||||
order='ORDER1234',
|
||||
event=self.request.event.name,
|
||||
order_date=date_format(now(), 'SHORT_DATE_FORMAT'),
|
||||
due_date=date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'),
|
||||
order_url=build_absolute_uri(self.request.event, 'presale:event.order', kwargs={
|
||||
'order': 'ORDER1234',
|
||||
'secret': 'longrandomsecretabcdef123456'
|
||||
}))
|
||||
self.output[l].append(preview_text)
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
else:
|
||||
try:
|
||||
with language(o.locale):
|
||||
mail(o.email, form.cleaned_data['subject'], form.cleaned_data['message'],
|
||||
{
|
||||
'event': o.event,
|
||||
'order': o.code,
|
||||
'order_date': date_format(o.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||
'due_date': date_format(o.expires, 'SHORT_DATE_FORMAT'),
|
||||
'order_url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
||||
'order': o.code,
|
||||
'secret': o.secret
|
||||
})},
|
||||
self.request.event, locale=o.locale, order=o)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
user=self.request.user,
|
||||
data={
|
||||
'subject': form.cleaned_data['subject'],
|
||||
'message': form.cleaned_data['message'],
|
||||
'recipient': o.email
|
||||
}
|
||||
)
|
||||
except SendMailException:
|
||||
failures.append(o.email)
|
||||
try:
|
||||
invoice_name = o.invoice_address.name
|
||||
invoice_company = o.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
|
||||
try:
|
||||
with language(o.locale):
|
||||
mail(
|
||||
o.email, form.cleaned_data['subject'], form.cleaned_data['message'],
|
||||
{
|
||||
'event': o.event,
|
||||
'order': o.code,
|
||||
'order_date': date_format(o.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||
'due_date': date_format(o.expires, 'SHORT_DATE_FORMAT'),
|
||||
'order_url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
||||
'order': o.code,
|
||||
'secret': o.secret
|
||||
}),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
},
|
||||
self.request.event, locale=o.locale, order=o)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
user=self.request.user,
|
||||
data={
|
||||
'subject': form.cleaned_data['subject'],
|
||||
'message': form.cleaned_data['message'],
|
||||
'recipient': o.email
|
||||
}
|
||||
)
|
||||
except SendMailException:
|
||||
failures.append(o.email)
|
||||
self.request.event.log_action('pretix.plugins.sendmail.sent',
|
||||
user=self.request.user,
|
||||
data=dict(form.cleaned_data))
|
||||
if failures:
|
||||
messages.error(self.request, _('Failed to send mails to the following users: {}'.format(' '.join(failures))))
|
||||
messages.error(self.request,
|
||||
_('Failed to send mails to the following users: {}'.format(' '.join(failures))))
|
||||
else:
|
||||
messages.success(self.request, _('Your message has been queued and will be sent to the selected users.'))
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from pretix.control.signals import nav_event
|
||||
@receiver(nav_event, dispatch_uid="statistics_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.eventperm.can_view_orders:
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders'):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -28,6 +28,10 @@ class Stripe(BasePaymentProvider):
|
||||
('secret_key',
|
||||
forms.CharField(
|
||||
label=_('Secret key'),
|
||||
help_text=_('<a target="_blank" href="{docs_url}">{text}</a>').format(
|
||||
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/stripe.html'
|
||||
)
|
||||
)),
|
||||
('publishable_key',
|
||||
forms.CharField(
|
||||
|
||||
@@ -1,9 +1,56 @@
|
||||
from django.dispatch import receiver
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.template.loader import get_template
|
||||
from django.urls import resolve
|
||||
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.control.signals import html_head
|
||||
|
||||
|
||||
@receiver(register_ticket_outputs, dispatch_uid="output_pdf")
|
||||
def register_ticket_outputs(sender, **kwargs):
|
||||
from .ticketoutput import PdfTicketOutput
|
||||
return PdfTicketOutput
|
||||
|
||||
|
||||
@receiver(html_head, dispatch_uid="ticketoutputpdf_html_head")
|
||||
def html_head_presale(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if url.namespace == 'plugins:ticketoutputpdf':
|
||||
template = get_template('pretixplugins/ticketoutputpdf/control_head.html')
|
||||
return template.render({
|
||||
'request': request
|
||||
})
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
register_fonts = Signal()
|
||||
"""
|
||||
Return a dictionaries of the following structure. Paths should be relative to static root.
|
||||
|
||||
{
|
||||
"font name": {
|
||||
"regular": {
|
||||
"truetype": "….ttf",
|
||||
"woff": "…",
|
||||
"woff2": "…"
|
||||
},
|
||||
"bold": {
|
||||
...
|
||||
},
|
||||
"italic": {
|
||||
...
|
||||
},
|
||||
"bolditalic": {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def get_fonts():
|
||||
f = {}
|
||||
for recv, value in register_fonts.send(0):
|
||||
f.update(value)
|
||||
return f
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#fabric-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#editor-canvas-area {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
}
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#toolbox .control-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#toolbox .position, #toolbox .squaresize, #toolbox[data-type] .pdf-info, #toolbox .text, #toolbox .object-buttons {
|
||||
display: none;
|
||||
}
|
||||
#toolbox[data-type] .position, #toolbox[data-type=barcodearea] .squaresize, #toolbox[data-type=text] .text, #toolbox[data-type=textarea] .text,
|
||||
#toolbox[data-type] .object-buttons {
|
||||
display: block;
|
||||
}
|
||||
#loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
#loading-container > div {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
#loading-upload {
|
||||
display: none;
|
||||
}
|
||||
.preload-font {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
/*globals $, gettext, fabric, PDFJS*/
|
||||
fabric.Barcodearea = fabric.util.createClass(fabric.Rect, {
|
||||
type: 'barcodearea',
|
||||
|
||||
initialize: function (text, options) {
|
||||
options || (options = {});
|
||||
|
||||
this.callSuper('initialize', text, options);
|
||||
this.set('label', options.label || '');
|
||||
},
|
||||
|
||||
toObject: function () {
|
||||
return fabric.util.object.extend(this.callSuper('toObject'), {});
|
||||
},
|
||||
|
||||
_render: function (ctx) {
|
||||
this.callSuper('_render', ctx);
|
||||
|
||||
ctx.font = '16px Helvetica';
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillText(gettext('QR Code'), -this.width / 2, -this.height / 2 + 20);
|
||||
},
|
||||
});
|
||||
fabric.Barcodearea.fromObject = function (object, callback, forceAsync) {
|
||||
return fabric.Object._fromObject('Barcodearea', object, callback, forceAsync);
|
||||
};
|
||||
fabric.Textarea = fabric.util.createClass(fabric.Textbox, {
|
||||
type: 'textarea',
|
||||
|
||||
initialize: function (text, options) {
|
||||
options || (options = {});
|
||||
|
||||
this.callSuper('initialize', text, options);
|
||||
this.set('content', options.content || '');
|
||||
},
|
||||
|
||||
toObject: function(propertiesToInclude) {
|
||||
return this.callSuper('toObject', ['content'].concat(propertiesToInclude));
|
||||
}
|
||||
});
|
||||
fabric.Textarea.fromObject = function (object, callback, forceAsync) {
|
||||
return fabric.Object._fromObject('Textarea', object, callback, forceAsync, 'text');
|
||||
};
|
||||
|
||||
|
||||
var editor = {
|
||||
$pdfcv: null,
|
||||
$fcv: null,
|
||||
$cva: null,
|
||||
$fabric: null,
|
||||
objects: [],
|
||||
history: [],
|
||||
clipboard: [],
|
||||
pdf_page: null,
|
||||
pdf_scale: 1,
|
||||
pdf_viewport: null,
|
||||
_history_pos: 0,
|
||||
_history_modification_in_progress: false,
|
||||
dirty: false,
|
||||
pdf_url: null,
|
||||
uploaded_file_id: null,
|
||||
_window_loaded: false,
|
||||
_fabric_loaded: false,
|
||||
|
||||
_px2mm: function (v) {
|
||||
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
|
||||
},
|
||||
|
||||
_mm2px: function (v) {
|
||||
return v * editor.pdf_scale * 72 / editor.pdf_page.userUnit / 25.4;
|
||||
},
|
||||
|
||||
_px2pt: function (v) {
|
||||
return v / editor.pdf_scale * editor.pdf_page.userUnit;
|
||||
},
|
||||
|
||||
_pt2px: function (v) {
|
||||
return v * editor.pdf_scale / editor.pdf_page.userUnit;
|
||||
},
|
||||
|
||||
dump: function (objs) {
|
||||
var d = [];
|
||||
objs = objs || editor.fabric.getObjects();
|
||||
for (var i in objs) {
|
||||
var o = objs[i];
|
||||
if (o.type === "textarea") {
|
||||
var col = (new fabric.Color(o.getFill()))._source;
|
||||
d.push({
|
||||
type: "textarea",
|
||||
left: editor._px2mm(o.left).toFixed(2),
|
||||
bottom: editor._px2mm(editor.pdf_viewport.height - o.height - o.top).toFixed(2),
|
||||
fontsize: editor._px2pt(o.getFontSize()).toFixed(1),
|
||||
color: col,
|
||||
//lineheight: o.lineHeight,
|
||||
fontfamily: o.fontFamily,
|
||||
bold: o.fontWeight === 'bold',
|
||||
italic: o.fontStyle === 'italic',
|
||||
width: editor._px2mm(o.width).toFixed(2),
|
||||
content: o.content,
|
||||
text: o.text,
|
||||
align: o.textAlign,
|
||||
});
|
||||
} else if (o.type === "barcodearea") {
|
||||
d.push({
|
||||
type: "barcodearea",
|
||||
left: editor._px2mm(o.left).toFixed(2),
|
||||
bottom: editor._px2mm(editor.pdf_viewport.height - o.height * o.scaleY - o.top).toFixed(2),
|
||||
size: editor._px2mm(o.height * o.scaleY).toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
return d;
|
||||
},
|
||||
|
||||
_add_from_data: function (d) {
|
||||
if (d.type === "barcodearea") {
|
||||
o = editor._add_qrcode();
|
||||
o.scaleToHeight(editor._mm2px(d.size));
|
||||
} else if (d.type === "textarea" || o.type === "text") {
|
||||
o = editor._add_text();
|
||||
o.setColor('rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')');
|
||||
o.setFontSize(editor._pt2px(d.fontsize));
|
||||
//o.setLineHeight(d.lineheight);
|
||||
o.setFontFamily(d.fontfamily);
|
||||
o.setFontWeight(d.bold ? 'bold' : 'normal');
|
||||
o.setFontStyle(d.italic ? 'italic' : 'normal');
|
||||
o.setWidth(editor._mm2px(d.width));
|
||||
o.content = d.content;
|
||||
o.setTextAlign(d.align);
|
||||
if (d.content === "other") {
|
||||
o.setText(d.text);
|
||||
} else {
|
||||
o.setText(editor.text_samples[d.content]);
|
||||
}
|
||||
}
|
||||
|
||||
var new_top = editor.pdf_viewport.height - editor._mm2px(d.bottom) - (o.height * o.scaleY);
|
||||
o.set('left', editor._mm2px(d.left));
|
||||
o.set('top', new_top);
|
||||
o.setCoords();
|
||||
return o;
|
||||
},
|
||||
|
||||
load: function(data) {
|
||||
editor.fabric.clear();
|
||||
for (var i in data) {
|
||||
var d = data[i], o;
|
||||
editor._add_from_data(d);
|
||||
}
|
||||
editor.fabric.renderAll();
|
||||
editor._update_toolbox_values();
|
||||
},
|
||||
|
||||
text_samples: {
|
||||
"secret": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
|
||||
"order": "A1B2C",
|
||||
"item": gettext("Sample product"),
|
||||
"variation": gettext("Sample variation"),
|
||||
"itemvar": gettext("Sample product – sample variation"),
|
||||
"price": gettext("123.45 EUR"),
|
||||
"attendee_name": gettext("John Doe"),
|
||||
"event_name": gettext("Sample event name"),
|
||||
"event_date": gettext("May 31st, 2017"),
|
||||
"event_begin_time": gettext("20:00"),
|
||||
"event_admission_time": gettext("19:00"),
|
||||
"event_begin": gettext("2017-05-31 20:00"),
|
||||
"event_admission": gettext("2017-05-31 19:00"),
|
||||
"event_location": gettext("Random City")
|
||||
},
|
||||
|
||||
_load_pdf: function (dump) {
|
||||
// TODO: Loading indicators
|
||||
var url = editor.pdf_url;
|
||||
// TODO: Handle cross-origin issues if static files are on a different origin
|
||||
PDFJS.workerSrc = editor.$pdfcv.attr("data-worker-url");
|
||||
|
||||
// Asynchronous download of PDF
|
||||
var loadingTask = PDFJS.getDocument(url);
|
||||
loadingTask.promise.then(function (pdf) {
|
||||
console.log('PDF loaded');
|
||||
|
||||
// Fetch the first page
|
||||
var pageNumber = 1;
|
||||
pdf.getPage(pageNumber).then(function (page) {
|
||||
console.log('Page loaded');
|
||||
var canvas = document.getElementById('pdf-canvas');
|
||||
|
||||
var scale = editor.$cva.width() / page.getViewport(1.0).width;
|
||||
var viewport = page.getViewport(scale);
|
||||
|
||||
// Prepare canvas using PDF page dimensions
|
||||
var context = canvas.getContext('2d');
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
editor.pdf_page = page;
|
||||
editor.pdf_scale = scale;
|
||||
editor.pdf_viewport = viewport;
|
||||
|
||||
// Render PDF page into canvas context
|
||||
var renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
};
|
||||
var renderTask = page.render(renderContext);
|
||||
renderTask.then(function () {
|
||||
console.log('Page rendered');
|
||||
editor._init_fabric(dump);
|
||||
});
|
||||
});
|
||||
}, function (reason) {
|
||||
var msg = gettext('The PDF background file could not be loaded for the following reason:');
|
||||
editor._error(msg + ' ' + reason);
|
||||
});
|
||||
},
|
||||
|
||||
_init_fabric: function (dump) {
|
||||
editor.$fcv.get(0).width = editor.$pdfcv.get(0).width;
|
||||
editor.$fcv.get(0).height = editor.$pdfcv.get(0).height;
|
||||
editor.fabric = new fabric.Canvas('fabric-canvas');
|
||||
|
||||
editor.fabric.on('object:modified', editor._create_savepoint);
|
||||
editor.fabric.on('object:added', editor._create_savepoint);
|
||||
editor.fabric.on('selection:cleared', editor._update_toolbox);
|
||||
editor.fabric.on('selection:created', editor._update_toolbox);
|
||||
editor.fabric.on('object:selected', editor._update_toolbox);
|
||||
editor.fabric.on('object:moving', editor._update_toolbox_values);
|
||||
editor.fabric.on('object:modified', editor._update_toolbox_values);
|
||||
editor.fabric.on('object:scaling', editor._update_toolbox_values);
|
||||
editor._update_toolbox();
|
||||
|
||||
$("#toolbox-content-other").hide();
|
||||
$(".add-buttons button").prop('disabled', false);
|
||||
|
||||
if (dump) {
|
||||
editor.load(dump);
|
||||
} else {
|
||||
var data = $.trim($("#editor-data").text());
|
||||
if (data) {
|
||||
editor.load(JSON.parse(data));
|
||||
}
|
||||
}
|
||||
editor.history = [];
|
||||
editor._create_savepoint();
|
||||
editor.dirty = !!dump;
|
||||
|
||||
if ($("#loading-upload").is(":visible")) {
|
||||
$("#loading-container, #loading-upload").hide();
|
||||
}
|
||||
|
||||
editor._fabric_loaded = true;
|
||||
console.log("Fabric loaded");
|
||||
if (editor._window_loaded) {
|
||||
editor._ready();
|
||||
}
|
||||
},
|
||||
|
||||
_window_load_event: function () {
|
||||
editor._window_loaded = true;
|
||||
console.log("Window loaded");
|
||||
if (editor._fabric_loaded) {
|
||||
editor._ready();
|
||||
}
|
||||
},
|
||||
|
||||
_ready: function () {
|
||||
$("#editor-loading").hide();
|
||||
$("#editor-start").removeClass("sr-only");
|
||||
$("#editor-start").click(function () {
|
||||
$("#loading-container").hide();
|
||||
$("#loading-initial").remove();
|
||||
});
|
||||
},
|
||||
|
||||
_update_toolbox_values: function () {
|
||||
var o = editor.fabric.getActiveObject();
|
||||
if (!o) {
|
||||
o = editor.fabric.getActiveGroup();
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$("#toolbox-position-x").val(editor._px2mm(o.left).toFixed(2));
|
||||
$("#toolbox-position-y").val(editor._px2mm(editor.pdf_viewport.height - o.height * o.scaleY - o.top).toFixed(2));
|
||||
|
||||
if (o.type === "barcodearea") {
|
||||
$("#toolbox-squaresize").val(editor._px2mm(o.height * o.scaleY).toFixed(2));
|
||||
} else if (o.type === "text" || o.type === "textarea") {
|
||||
var col = (new fabric.Color(o.getFill()))._source;
|
||||
$("#toolbox-col").val("#" + ((1 << 24) + (col[0] << 16) + (col[1] << 8) + col[2]).toString(16).slice(1));
|
||||
$("#toolbox-fontsize").val(editor._px2pt(o.fontSize).toFixed(1));
|
||||
//$("#toolbox-lineheight").val(o.lineHeight);
|
||||
$("#toolbox-fontfamily").val(o.fontFamily);
|
||||
$("#toolbox").find("button[data-action=bold]").toggleClass('active', o.fontWeight === 'bold');
|
||||
$("#toolbox").find("button[data-action=italic]").toggleClass('active', o.fontStyle === 'italic');
|
||||
$("#toolbox").find("button[data-action=left]").toggleClass('active', o.textAlign === 'left');
|
||||
$("#toolbox").find("button[data-action=center]").toggleClass('active', o.textAlign === 'center');
|
||||
$("#toolbox").find("button[data-action=right]").toggleClass('active', o.textAlign === 'right');
|
||||
$("#toolbox-textwidth").val(editor._px2mm(o.width).toFixed(2));
|
||||
if (o.type === "textarea") {
|
||||
$("#toolbox-content").val(o.content);
|
||||
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
|
||||
if (o.content === "other") {
|
||||
$("#toolbox-content-other").val(o.text);
|
||||
} else {
|
||||
$("#toolbox-content-other").val("");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_update_values_from_toolbox: function () {
|
||||
var o = editor.fabric.getActiveObject();
|
||||
if (!o) {
|
||||
o = editor.fabric.getActiveGroup();
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var new_top = editor.pdf_viewport.height - editor._mm2px($("#toolbox-position-y").val()) - o.height * o.scaleY;
|
||||
o.set('left', editor._mm2px($("#toolbox-position-x").val()));
|
||||
o.set('top', new_top);
|
||||
|
||||
if (o.type === "barcodearea") {
|
||||
var new_h = editor._mm2px($("#toolbox-squaresize").val());
|
||||
new_top += o.height * o.scaleY - new_h;
|
||||
o.setHeight(new_h);
|
||||
o.setWidth(new_h);
|
||||
o.setScaleX(1);
|
||||
o.setScaleY(1);
|
||||
o.set('top', new_top)
|
||||
} else if (o.type === "textarea" || o.type === "text") {
|
||||
o.setColor($("#toolbox-col").val());
|
||||
o.setFontSize(editor._pt2px($("#toolbox-fontsize").val()));
|
||||
//o.setLineHeight($("#toolbox-lineheight").val());
|
||||
o.setFontFamily($("#toolbox-fontfamily").val());
|
||||
o.setFontWeight($("#toolbox").find("button[data-action=bold]").is('.active') ? 'bold' : 'normal');
|
||||
o.setFontStyle($("#toolbox").find("button[data-action=italic]").is('.active') ? 'italic' : 'normal');
|
||||
var align = $("#toolbox-align").find(".active").attr("data-action");
|
||||
if (align) {
|
||||
o.setTextAlign(align);
|
||||
}
|
||||
o.setWidth(editor._mm2px($("#toolbox-textwidth").val()));
|
||||
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
|
||||
o.content = $("#toolbox-content").val();
|
||||
if ($("#toolbox-content").val() === "other") {
|
||||
o.setText($("#toolbox-content-other").val());
|
||||
} else {
|
||||
o.setText(editor.text_samples[$("#toolbox-content").val()]);
|
||||
}
|
||||
}
|
||||
|
||||
o.setCoords();
|
||||
editor.fabric.renderAll();
|
||||
},
|
||||
|
||||
_update_toolbox: function () {
|
||||
if (editor.fabric.getActiveGroup()) {
|
||||
$("#toolbox").attr("data-type", "group");
|
||||
$("#toolbox-heading").text(gettext("Group of objects"));
|
||||
var g = editor.fabric.getActiveGroup();
|
||||
} else if (editor.fabric.getActiveObject()) {
|
||||
var o = editor.fabric.getActiveObject();
|
||||
$("#toolbox").attr("data-type", o.type);
|
||||
if (o.type === "textarea" || o.type === "text") {
|
||||
$("#toolbox-heading").text(gettext("Text object"));
|
||||
} else if (o.type === "barcodearea") {
|
||||
$("#toolbox-heading").text(gettext("Barcode area"));
|
||||
} else {
|
||||
$("#toolbox-heading").text(gettext("Object"));
|
||||
}
|
||||
} else {
|
||||
$("#toolbox").removeAttr("data-type");
|
||||
$("#toolbox-heading").text(gettext("Ticket design"));
|
||||
$("#pdf-info-width").val(editor._px2mm(editor.pdf_viewport.width).toFixed(2));
|
||||
$("#pdf-info-height").val(editor._px2mm(editor.pdf_viewport.height).toFixed(2));
|
||||
}
|
||||
editor._update_toolbox_values();
|
||||
},
|
||||
|
||||
_error: function (msg) {
|
||||
editor.$cva.before("<div class='alert alert-danger'>" + msg + "</div>");
|
||||
},
|
||||
|
||||
_add_text: function () {
|
||||
var text = new fabric.Textarea(editor.text_samples['item'], {
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: editor._mm2px(50),
|
||||
lockRotation: true,
|
||||
fontFamily: 'Open Sans',
|
||||
lineHeight: 1,
|
||||
content: 'item',
|
||||
editable: false,
|
||||
fontSize: editor._pt2px(13)
|
||||
});
|
||||
text.setControlsVisibility({
|
||||
'tr': false,
|
||||
'tl': false,
|
||||
'mt': false,
|
||||
'br': false,
|
||||
'bl': false,
|
||||
'mb': false,
|
||||
'mr': true,
|
||||
'ml': true,
|
||||
'mtr': false
|
||||
});
|
||||
editor.fabric.add(text);
|
||||
editor._create_savepoint();
|
||||
return text;
|
||||
},
|
||||
|
||||
_add_qrcode: function () {
|
||||
var rect = new fabric.Barcodearea({
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
lockRotation: true,
|
||||
lockUniScaling: true,
|
||||
fill: '#666',
|
||||
});
|
||||
rect.setControlsVisibility({'mtr': false});
|
||||
editor.fabric.add(rect);
|
||||
editor._create_savepoint();
|
||||
return rect;
|
||||
},
|
||||
|
||||
_cut: function () {
|
||||
editor._history_modification_in_progress = true;
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
if (thing.type === "group") {
|
||||
editor.clipboard = editor.dump(thing._objects);
|
||||
thing.forEachObject(function (o) {
|
||||
o.remove();
|
||||
});
|
||||
thing.remove();
|
||||
} else {
|
||||
editor.clipboard = editor.dump([thing]);
|
||||
thing.remove();
|
||||
}
|
||||
editor.fabric.discardActiveGroup();
|
||||
editor.fabric.discardActiveObject();
|
||||
editor._history_modification_in_progress = false;
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_copy: function () {
|
||||
editor._history_modification_in_progress = true;
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
if (thing.type === "group") {
|
||||
editor.clipboard = editor.dump(thing._objects);
|
||||
} else {
|
||||
editor.clipboard = editor.dump([thing]);
|
||||
}
|
||||
editor._history_modification_in_progress = false;
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_paste: function () {
|
||||
if (editor.clipboard.length < 1) {
|
||||
return;
|
||||
}
|
||||
editor._history_modification_in_progress = true;
|
||||
var objs = [];
|
||||
for (var i in editor.clipboard) {
|
||||
objs.push(editor._add_from_data(editor.clipboard[i]));
|
||||
}
|
||||
editor.fabric.discardActiveObject();
|
||||
editor.fabric.discardActiveGroup();
|
||||
if (editor.clipboard.length > 1) {
|
||||
var group = new fabric.Group(objs, {
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
left: 100,
|
||||
top: 100,
|
||||
});
|
||||
group.setCoords();
|
||||
editor.fabric.setActiveGroup(group);
|
||||
} else {
|
||||
editor.fabric.setActiveObject(objs[0]);
|
||||
}
|
||||
editor._history_modification_in_progress = false;
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_delete: function () {
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
if (thing.type === "group") {
|
||||
thing.forEachObject(function (o) {
|
||||
o.remove();
|
||||
});
|
||||
thing.remove();
|
||||
editor.fabric.discardActiveGroup();
|
||||
} else {
|
||||
thing.remove();
|
||||
editor.fabric.discardActiveObject();
|
||||
}
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_on_keydown: function (e) {
|
||||
var step = e.shiftKey ? editor._mm2px(10) : editor._mm2px(1);
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
switch (e.keyCode) {
|
||||
case 38: /* Up arrow */
|
||||
thing.set('top', thing.get('top') - step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 40: /* Down arrow */
|
||||
thing.set('top', thing.get('top') + step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 37: /* Left arrow */
|
||||
thing.set('left', thing.get('left') - step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 39: /* Right arrow */
|
||||
thing.set('left', thing.get('left') + step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 46: /* Delete */
|
||||
editor._delete();
|
||||
break;
|
||||
case 89: /* Y */
|
||||
if (e.ctrlKey) {
|
||||
editor._redo();
|
||||
}
|
||||
break;
|
||||
case 90: /* Z */
|
||||
if (e.ctrlKey) {
|
||||
editor._undo();
|
||||
}
|
||||
break;
|
||||
case 88: /* X */
|
||||
if (e.ctrlKey) {
|
||||
editor._cut();
|
||||
}
|
||||
break;
|
||||
case 86: /* V */
|
||||
if (e.ctrlKey) {
|
||||
editor._paste();
|
||||
}
|
||||
break;
|
||||
case 67: /* C */
|
||||
if (e.ctrlKey) {
|
||||
editor._copy();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
editor.fabric.renderAll();
|
||||
editor._update_toolbox_values();
|
||||
},
|
||||
|
||||
_create_savepoint: function () {
|
||||
if (editor._history_modification_in_progress) {
|
||||
return;
|
||||
}
|
||||
var state = editor.dump();
|
||||
if (editor._history_pos > 0) {
|
||||
editor.history.splice(-1 * editor._history_pos, editor._history_pos);
|
||||
editor._history_pos = 0;
|
||||
}
|
||||
editor.history.push(state);
|
||||
editor.dirty = true;
|
||||
},
|
||||
|
||||
_undo: function undo() {
|
||||
if (editor._history_pos < editor.history.length - 1) {
|
||||
editor._history_modification_in_progress = true;
|
||||
editor._history_pos += 1;
|
||||
editor.fabric.clear().renderAll();
|
||||
editor.load(editor.history[editor.history.length - 1 - editor._history_pos]);
|
||||
editor._history_modification_in_progress = false;
|
||||
editor.dirty = true;
|
||||
}
|
||||
},
|
||||
|
||||
_redo: function redo() {
|
||||
if (editor._history_pos > 0) {
|
||||
editor._history_modification_in_progress = true;
|
||||
editor._history_pos -= 1;
|
||||
editor.load(editor.history[editor.history.length - 1 - editor._history_pos]);
|
||||
editor._history_modification_in_progress = false;
|
||||
editor.dirty = true;
|
||||
}
|
||||
},
|
||||
|
||||
_save: function () {
|
||||
$("#editor-save").prop('disabled', true).prepend('<span class="fa fa-cog fa-spin"></span>');
|
||||
var dump = editor.dump();
|
||||
$.post(window.location.href, {
|
||||
'data': JSON.stringify(dump),
|
||||
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val(),
|
||||
'background': editor.uploaded_file_id,
|
||||
}, function (data) {
|
||||
if (data.status === 'ok') {
|
||||
$("#editor-save span").remove();
|
||||
$("#editor-save").prop('disabled', false);
|
||||
editor.dirty = false;
|
||||
editor.uploaded_file_id = null;
|
||||
} else {
|
||||
alert(gettext('Saving failed.'));
|
||||
}
|
||||
}, 'json');
|
||||
},
|
||||
|
||||
_preview: function () {
|
||||
$("#preview-form input[name=data]").val(JSON.stringify(editor.dump()));
|
||||
$("#preview-form input[name=background]").val(editor.uploaded_file_id);
|
||||
$("#preview-form").get(0).submit();
|
||||
},
|
||||
|
||||
_replace_pdf_file: function (url) {
|
||||
editor.pdf_url = url;
|
||||
d = editor.dump();
|
||||
editor.fabric.dispose();
|
||||
editor._load_pdf(d);
|
||||
},
|
||||
|
||||
init: function () {
|
||||
editor.$pdfcv = $("#pdf-canvas");
|
||||
editor.pdf_url = editor.$pdfcv.attr("data-pdf-url");
|
||||
editor.$fcv = $("#fabric-canvas");
|
||||
editor.$cva = $("#editor-canvas-area");
|
||||
editor._load_pdf();
|
||||
$("#editor-add-qrcode").click(editor._add_qrcode);
|
||||
$("#editor-add-text").click(editor._add_text);
|
||||
editor.$cva.get(0).tabIndex = 1000;
|
||||
editor.$cva.on("keydown", editor._on_keydown);
|
||||
$("#editor-save").on("click", editor._save);
|
||||
$("#editor-preview").on("click", editor._preview);
|
||||
window.onbeforeunload = function () {
|
||||
if (editor.dirty) {
|
||||
return gettext("Do you really want to leave the editor without saving your changes?");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$('#fileupload').fileupload({
|
||||
url: location.href,
|
||||
dataType: 'json',
|
||||
done: function (e, data) {
|
||||
if (data.result.status === "ok") {
|
||||
editor.uploaded_file_id = data.result.id;
|
||||
editor._replace_pdf_file(data.result.url);
|
||||
} else {
|
||||
alert(data.result.error || gettext("Error while uploading your PDF file, please try again."));
|
||||
$("#loading-container, #loading-upload").hide();
|
||||
}
|
||||
$("#fileupload").prop('disabled', false);
|
||||
$(".fileinput-button").removeClass("disabled");
|
||||
},
|
||||
add: function (e, data) {
|
||||
data.formData = {
|
||||
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val()
|
||||
};
|
||||
$("#loading-container, #loading-upload").show();
|
||||
$("#loading-upload .progress").show();
|
||||
$('#loading-upload .progress-bar').css('width', 0);
|
||||
$("#fileupload").prop('disabled', true);
|
||||
$(".fileinput-button").addClass("disabled");
|
||||
data.process().done(function () {
|
||||
data.submit();
|
||||
});
|
||||
},
|
||||
progressall: function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#loading-upload .progress-bar').css('width', progress + '%');
|
||||
}
|
||||
}).prop('disabled', !$.support.fileInput).parent().addClass($.support.fileInput ? undefined : 'disabled');
|
||||
|
||||
$("#toolbox input[type=number], #toolbox textarea, #toolbox input[type=text]").bind('change keydown keyup' +
|
||||
' input', editor._update_values_from_toolbox);
|
||||
$("#toolbox input[type=number], #toolbox textarea, #toolbox input[type=text], #toolbox input[type=radio]").bind('change', editor._create_savepoint);
|
||||
$("#toolbox label.btn").bind('click change', editor._update_values_from_toolbox);
|
||||
$("#toolbox select").bind('change', editor._update_values_from_toolbox);
|
||||
$("#toolbox select").bind('change', editor._create_savepoint);
|
||||
$("#toolbox button.toggling").bind('click change', function () {
|
||||
if ($(this).is(".option")) {
|
||||
$(this).addClass("active");
|
||||
$(this).parent().siblings().find("button").removeClass("active");
|
||||
} else {
|
||||
$(this).toggleClass("active");
|
||||
}
|
||||
editor._update_values_from_toolbox();
|
||||
editor._create_savepoint();
|
||||
});
|
||||
$("#toolbox .colorpickerfield").bind('changeColor', editor._update_values_from_toolbox);
|
||||
$("#toolbox-copy").bind('click', editor._copy);
|
||||
$("#toolbox-cut").bind('click', editor._cut);
|
||||
$("#toolbox-delete").bind('click', editor._delete);
|
||||
$("#toolbox-paste").bind('click', editor._paste);
|
||||
$("#toolbox-undo").bind('click', editor._undo);
|
||||
$("#toolbox-redo").bind('click', editor._redo);
|
||||
}
|
||||
};
|
||||
|
||||
$(function () {
|
||||
editor.init();
|
||||
});
|
||||
$(window).bind('load', editor._window_load_event);
|
||||
+26834
File diff suppressed because it is too large
Load Diff
+9
File diff suppressed because one or more lines are too long
+9052
File diff suppressed because it is too large
Load Diff
+50501
File diff suppressed because it is too large
Load Diff
+7
@@ -0,0 +1,7 @@
|
||||
{% load staticfiles %}
|
||||
{% load compress %}
|
||||
|
||||
{% compress css %}
|
||||
<link type="text/css" rel="stylesheet" href="{% static "pretixplugins/ticketoutputpdf/editor.css" %}">
|
||||
{% endcompress %}
|
||||
<link type="text/css" rel="stylesheet" href="{% url "plugins:ticketoutputpdf:css" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
@@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Ticket design" %}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can customize the ticket design with our PDF ticket editor. There, you can upload a PDF file used
|
||||
as a background for the tickets and then place various texts and QR codes on the background at the
|
||||
positions of your choice. The editor is easy to use thanks to its drag-and-drop user interface, but it
|
||||
requires a modern browser and a decent internet connection.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn-default" target="_blank"
|
||||
href="{% url "plugins:ticketoutputpdf:editor" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
{% trans "Open the PDF editor in a new tab" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% block title %}{% trans "PDF Ticket Editor" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "PDF Ticket Editor" %}</h1>
|
||||
|
||||
<script type="application/json" id="editor-data">
|
||||
{{ layout|safe }}
|
||||
</script>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default btn-xs" id="toolbox-paste"
|
||||
title="{% trans "Paste" %}">
|
||||
<span class="fa fa-paste"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-xs" id="toolbox-undo"
|
||||
title="{% trans "Undo" %}">
|
||||
<span class="fa fa-undo"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-xs" id="toolbox-redo"
|
||||
title="{% trans "Redo" %}">
|
||||
<span class="fa fa-repeat"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% trans "Editor" %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="editor-canvas-area">
|
||||
<canvas id="pdf-canvas"
|
||||
{% if request.event.settings.ticketoutput_pdf_background %}
|
||||
data-pdf-url="{{ request.event.settings.ticketoutput_pdf_background.url }}"
|
||||
{% else %}
|
||||
data-pdf-url="{% static "pretixpresale/pdf/ticket_default_a4.pdf" %}"
|
||||
{% endif %}
|
||||
data-worker-url="{% static "pretixplugins/ticketoutputpdf/pdf.worker.js" %}">
|
||||
|
||||
</canvas>
|
||||
<div id="fabric-container">
|
||||
<canvas id="fabric-canvas">
|
||||
</canvas>
|
||||
</div>
|
||||
<div id="loading-container">
|
||||
<div id="loading-upload">
|
||||
<span class="fa fa-cog big-rotating-icon"></span>
|
||||
<p>
|
||||
{% trans "Uploading new PDF background…" %}
|
||||
</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 0%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loading-initial">
|
||||
<h2>{% trans "Welcome to the PDF ticket editor!" %}</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This editor allows you to create a design for the PDF tickets of your event.
|
||||
You can upload a background PDF and then use this tool to place texts and
|
||||
a QR code on the ticket.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p> </p>
|
||||
<p>
|
||||
<span class="fa fa-eye fa-2x"></span>
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Please note that the editor can only provide a rough preview. Some details,
|
||||
for example in text rendering, might look slightly different in the final
|
||||
tickets. You can use the "Preview" button on the right for a more precise
|
||||
preview.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p> </p>
|
||||
<p>
|
||||
<span class="fa fa-chrome fa-2x"></span>
|
||||
<span class="fa fa-firefox fa-2x"></span>
|
||||
<span class="fa fa-opera fa-2x"></span>
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The editor is tested with recent versions of Google Chome, Mozilla Firefox
|
||||
and Opera. Other browsers, especially Internet Explorer or Microsoft Edge, might
|
||||
have problems displaying your background PDF or loading the correct fonts.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<noscript>
|
||||
<div class="alert alert-danger">
|
||||
{% blocktrans trimmed %}
|
||||
The editor requires JavaScript to work. Please enable JavaScript in your
|
||||
browser to continue.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</noscript>
|
||||
<p> </p>
|
||||
<p>
|
||||
<em id="editor-loading">
|
||||
<span class="fa fa-cog fa-spin"></span>
|
||||
{% trans "Loading…" %}
|
||||
</em>
|
||||
<button id="editor-start" class="btn btn-primary sr-only">
|
||||
{% trans "Start editing" %}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3" id="editor-toolbox-area">
|
||||
<div class="panel panel-default" id="toolbox">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right object-buttons">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default btn-xs" id="toolbox-cut"
|
||||
title="{% trans "Cut" %}">
|
||||
<span class="fa fa-cut"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-xs" id="toolbox-copy"
|
||||
title="{% trans "Copy" %}">
|
||||
<span class="fa fa-copy"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-xs" id="toolbox-delete"
|
||||
title="{% trans "Delete" %}">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span id="toolbox-heading">
|
||||
{% trans "Loading…" %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body" id="toolbox-body">
|
||||
<div class="row control-group pdf-info">
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Width (mm)" %}</label><br>
|
||||
<input type="number" id="pdf-info-width" class="input-block-level form-control" disabled>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Height (mm)" %}</label><br>
|
||||
<input type="number" id="pdf-info-height" class="input-block-level form-control" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group pdf-info">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Background PDF" %}</label><br>
|
||||
<span class="btn btn-default fileinput-button">
|
||||
<i class="fa fa-upload"></i>
|
||||
<span>{% trans "Upload new background" %}</span>
|
||||
<input id="fileupload" type="file" name="background">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group position">
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "x (mm)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.01"
|
||||
id="toolbox-position-x">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "y (mm)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.01"
|
||||
id="toolbox-position-y">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group squaresize">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Size (mm)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.01"
|
||||
id="toolbox-squaresize">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group squaresize">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The final QR code will be slightly smaller because some whitespace is required
|
||||
for proper scanning.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group text">
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Font size (pt)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.1"
|
||||
id="toolbox-fontsize">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label> </label><br>
|
||||
<div class="btn-group btn-group-justified" role="group">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default toggling" data-action="bold">
|
||||
<span class="fa fa-bold"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default toggling" data-action="italic">
|
||||
<span class="fa fa-italic"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group text">
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Text color" %}</label><br>
|
||||
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
|
||||
id="toolbox-col">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label> </label><br>
|
||||
<div class="btn-group btn-group-justified" id="toolbox-align">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default option toggling" data-action="left">
|
||||
<span class="fa fa-align-left"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default option toggling" data-action="center">
|
||||
<span class="fa fa-align-center"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default option toggling" data-action="right">
|
||||
<span class="fa fa-align-right"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group text">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Font" %}</label><br>
|
||||
<select class="input-block-level form-control" id="toolbox-fontfamily">
|
||||
<option>Open Sans</option>
|
||||
{% for family in fonts.keys %}
|
||||
<option>{{ family }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group text">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Width (mm)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.01"
|
||||
id="toolbox-textwidth">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group text">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Text content" %}</label><br>
|
||||
<select class="input-block-level form-control" id="toolbox-content">
|
||||
<option value="secret">{% trans "Ticket code (barcode content)" %}</option>
|
||||
<option value="order">{% trans "Order code" %}</option>
|
||||
<option value="item">{% trans "Product name" %}</option>
|
||||
<option value="variation">{% trans "Variation name" %}</option>
|
||||
<option value="itemvar">{% trans "Product name and variation" %}</option>
|
||||
<option value="price">{% trans "Price" %}</option>
|
||||
<option value="attendee_name">{% trans "Attendee name" %}</option>
|
||||
<option value="event_name">{% trans "Event name" %}</option>
|
||||
<option value="event_date">{% trans "Event date" %}</option>
|
||||
<option value="event_begin">{% trans "Event begin date and time" %}</option>
|
||||
<option value="event_begin_time">{% trans "Event begin time" %}</option>
|
||||
<option value="event_admission">{% trans "Event admission date and time" %}</option>
|
||||
<option value="event_admission_time">{% trans "Event admission time" %}</option>
|
||||
<option value="event_location">{% trans "Event location" %}</option>
|
||||
<option value="other">{% trans "Other…" %}</option>
|
||||
</select>
|
||||
<textarea type="text" value="" class="input-block-level form-control"
|
||||
id="toolbox-content-other"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-toolbox-text panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% trans "Add a new object" %}
|
||||
</div>
|
||||
<div class="panel-body add-buttons">
|
||||
<button class="btn btn-default btn-block" id="editor-add-text" disabled>
|
||||
<span class="fa fa-font"></span>
|
||||
{% trans "Text" %}
|
||||
</button>
|
||||
<button class="btn btn-default btn-block" id="editor-add-qrcode" disabled>
|
||||
<span class="fa fa-qrcode"></span>
|
||||
{% trans "QR code area" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="" id="preview-form" target="_blank">
|
||||
<div class="form-group submit-group">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" value="" name="data">
|
||||
<input type="hidden" value="" name="background">
|
||||
<input type="hidden" value="true" name="preview">
|
||||
<button type="submit" class="btn btn-default btn-lg" id="editor-preview">
|
||||
{% trans "Preview" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save" id="editor-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/pdf.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/fabric.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/editor.js" %}"></script>
|
||||
{% for family, styles in fonts.items %}
|
||||
{% for style, formats in styles.items %}
|
||||
<span class="preload-font" data-family="{{ family }}" data-style="{{ style }}">
|
||||
giItT1WQy@!-/#
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{% load staticfiles %}
|
||||
|
||||
{% for family, styles in fonts.items %}
|
||||
{% for style, formats in styles.items %}
|
||||
@font-face {
|
||||
font-family: '{{ family }}';
|
||||
{% if style == "italic" or style == "bolditalic" %}
|
||||
font-style: italic;
|
||||
{% else %}
|
||||
font-style: normal;
|
||||
{% endif %}
|
||||
{% if style == "bold" or style == "bolditalic" %}
|
||||
font-weight: bold;
|
||||
{% else %}
|
||||
font-weight: normal;
|
||||
{% endif %}
|
||||
src: {% if "woff2" in formats %}url('{% static formats.woff2 %}') format('woff2'),{% endif %}
|
||||
{% if "woff" in formats %}url('{% static formats.woff %}') format('woff'),{% endif %}
|
||||
{% if "truetype" in formats %}url('{% static formats.truetype %}') format('truetype'){% endif %};
|
||||
}
|
||||
.preload-font[data-family="{{family}}"][data-style="{{style}}"] {
|
||||
font-family: '{{ family }}';
|
||||
{% if style == "italic" or style == "bolditalic" %}
|
||||
font-style: italic;
|
||||
{% else %}
|
||||
font-style: normal;
|
||||
{% endif %}
|
||||
{% if style == "bold" or style == "bolditalic" %}
|
||||
font-weight: bold;
|
||||
{% else %}
|
||||
font-weight: normal;
|
||||
{% endif %}
|
||||
|
||||
}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
@@ -1,17 +1,32 @@
|
||||
import copy
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
|
||||
from django import forms
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pytz import timezone
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.lib.colors import Color
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import getAscentDescent
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.ticketoutput import BaseTicketOutput
|
||||
from pretix.control.forms import ExtFileField
|
||||
from pretix.plugins.ticketoutputpdf.signals import get_fonts
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
|
||||
|
||||
@@ -21,71 +36,107 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
verbose_name = _('PDF output')
|
||||
download_button_text = _('PDF')
|
||||
|
||||
def _draw_page(self, p, op, order):
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.lib import units
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics import renderPDF
|
||||
def __init__(self, event, override_layout=None, override_background=None):
|
||||
self.override_layout = override_layout
|
||||
self.override_background = override_background
|
||||
super().__init__(event)
|
||||
|
||||
event_s = self.settings.get('event_s', default=22, as_type=float)
|
||||
if event_s:
|
||||
p.setFont("Helvetica", event_s)
|
||||
event_x = self.settings.get('event_x', default=15, as_type=float)
|
||||
event_y = self.settings.get('event_y', default=235, as_type=float)
|
||||
p.drawString(event_x * units.mm, event_y * units.mm, str(self.event.name))
|
||||
def _register_fonts(self):
|
||||
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
|
||||
|
||||
order_s = self.settings.get('order_s', default=17, as_type=float)
|
||||
if order_s:
|
||||
p.setFont("Helvetica", order_s)
|
||||
order_x = self.settings.get('order_x', default=15, as_type=float)
|
||||
order_y = self.settings.get('order_y', default=220, as_type=float)
|
||||
p.drawString(order_x * units.mm, order_y * units.mm, _('Order code: {code}').format(code=order.code))
|
||||
for family, styles in get_fonts().items():
|
||||
print(family, finders.find(styles['regular']['truetype']))
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||
|
||||
name_s = self.settings.get('name_s', default=17, as_type=float)
|
||||
if name_s:
|
||||
p.setFont("Helvetica", name_s)
|
||||
name_x = self.settings.get('name_x', default=15, as_type=float)
|
||||
name_y = self.settings.get('name_y', default=210, as_type=float)
|
||||
item = str(op.item.name)
|
||||
if op.variation:
|
||||
item += " – " + str(op.variation)
|
||||
p.drawString(name_x * units.mm, name_y * units.mm, item)
|
||||
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
|
||||
reqs = float(o['size']) * mm
|
||||
qrw = QrCodeWidget(op.secret, barLevel='H', barHeight=reqs, barWidth=reqs)
|
||||
d = Drawing(reqs, reqs)
|
||||
d.add(qrw)
|
||||
qr_x = float(o['left']) * mm
|
||||
qr_y = float(o['bottom']) * mm
|
||||
renderPDF.draw(d, canvas, qr_x, qr_y)
|
||||
|
||||
price_s = self.settings.get('price_s', default=17, as_type=float)
|
||||
if price_s:
|
||||
p.setFont("Helvetica", price_s)
|
||||
price_x = self.settings.get('price_x', default=15, as_type=float)
|
||||
price_y = self.settings.get('price_y', default=200, as_type=float)
|
||||
p.drawString(price_x * units.mm, price_y * units.mm, "%s %s" % (str(op.price), self.event.currency))
|
||||
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
|
||||
if o['content'] == 'other':
|
||||
return o['text'].replace("\n", "<br/>\n")
|
||||
elif o['content'] == 'order':
|
||||
return order.code
|
||||
elif o['content'] == 'item':
|
||||
return str(op.item)
|
||||
elif o['content'] == 'secret':
|
||||
return op.secret
|
||||
elif o['content'] == 'variation':
|
||||
return str(op.variation) if op.variation else ''
|
||||
elif o['content'] == 'itemvar':
|
||||
return '{} - {}'.format(op.item, op.variation) if op.variation else str(op.item)
|
||||
elif o['content'] == 'price':
|
||||
return '{} {}'.format(order.event.currency, localize(op.price))
|
||||
elif o['content'] == 'attendee_name':
|
||||
return op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
|
||||
elif o['content'] == 'event_name':
|
||||
return str(order.event)
|
||||
elif o['content'] == 'event_location':
|
||||
return str(order.event.location).replace("\n", "<br/>\n")
|
||||
elif o['content'] == 'event_date':
|
||||
return order.event.get_date_from_display(show_times=False)
|
||||
elif o['content'] == 'event_begin':
|
||||
return order.event.get_date_from_display(show_times=True)
|
||||
elif o['content'] == 'event_begin_time':
|
||||
return order.event.get_time_from_display()
|
||||
elif o['content'] == 'event_admission':
|
||||
if order.event.date_admission:
|
||||
tz = timezone(order.event.settings.timezone)
|
||||
return date_format(order.event.date_admission.astimezone(tz), "SHORT_DATETIME_FORMAT")
|
||||
elif o['content'] == 'event_admission_time':
|
||||
if order.event.date_admission:
|
||||
tz = timezone(order.event.settings.timezone)
|
||||
return date_format(order.event.date_admission.astimezone(tz), "TIME_FORMAT")
|
||||
return ''
|
||||
|
||||
qr_s = self.settings.get('qr_s', default=80, as_type=float)
|
||||
if qr_s:
|
||||
reqs = qr_s * units.mm
|
||||
qrw = QrCodeWidget(op.secret, barLevel='H')
|
||||
b = qrw.getBounds()
|
||||
w = b[2] - b[0]
|
||||
h = b[3] - b[1]
|
||||
d = Drawing(reqs, reqs, transform=[reqs / w, 0, 0, reqs / h, 0, 0])
|
||||
d.add(qrw)
|
||||
qr_x = self.settings.get('qr_x', default=10, as_type=float)
|
||||
qr_y = self.settings.get('qr_y', default=120, as_type=float)
|
||||
renderPDF.draw(d, p, qr_x * units.mm, qr_y * units.mm)
|
||||
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||
font = o['fontfamily']
|
||||
if o['bold']:
|
||||
font += ' B'
|
||||
if o['italic']:
|
||||
font += ' I'
|
||||
|
||||
code_s = self.settings.get('code_s', default=11, as_type=float)
|
||||
if code_s:
|
||||
p.setFont("Helvetica", code_s)
|
||||
code_x = self.settings.get('code_x', default=15, as_type=float)
|
||||
code_y = self.settings.get('code_y', default=120, as_type=float)
|
||||
p.drawString(code_x * units.mm, code_y * units.mm, op.secret)
|
||||
align_map = {
|
||||
'left': TA_LEFT,
|
||||
'center': TA_CENTER,
|
||||
'right': TA_RIGHT
|
||||
}
|
||||
style = ParagraphStyle(
|
||||
name=uuid.uuid4().hex,
|
||||
fontName=font,
|
||||
fontSize=float(o['fontsize']),
|
||||
leading=float(o['fontsize']),
|
||||
autoLeading="max",
|
||||
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
|
||||
alignment=align_map[o['align']]
|
||||
)
|
||||
|
||||
attendee_s = self.settings.get('attendee_s', default=0, as_type=float)
|
||||
if code_s and op.attendee_name:
|
||||
p.setFont("Helvetica", attendee_s)
|
||||
attendee_x = self.settings.get('attendee_x', default=15, as_type=float)
|
||||
attendee_y = self.settings.get('attendee_y', default=90, as_type=float)
|
||||
p.drawString(attendee_x * units.mm, attendee_y * units.mm, op.attendee_name)
|
||||
p = Paragraph(self._get_text_content(op, order, o), style=style)
|
||||
p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
|
||||
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
|
||||
ad = getAscentDescent(font, float(o['fontsize']))
|
||||
p.drawOn(canvas, float(o['left']) * mm, float(o['bottom']) * mm - ad[1])
|
||||
|
||||
p.showPage()
|
||||
def _draw_page(self, canvas: Canvas, op: OrderPosition, order: Order):
|
||||
objs = self.override_layout or self.settings.get('layout', as_type=list) or self._legacy_layout()
|
||||
for o in objs:
|
||||
if o['type'] == "barcodearea":
|
||||
self._draw_barcodearea(canvas, op, o)
|
||||
elif o['type'] == "textarea":
|
||||
self._draw_textarea(canvas, op, order, o)
|
||||
|
||||
canvas.showPage()
|
||||
|
||||
def generate_order(self, order: Order):
|
||||
buffer = BytesIO()
|
||||
@@ -93,6 +144,8 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
for op in order.positions.all():
|
||||
if op.addon_to_id and not self.event.settings.ticket_download_addons:
|
||||
continue
|
||||
if not op.item.admission and not self.event.settings.ticket_download_nonadm:
|
||||
continue
|
||||
self._draw_page(p, op, order)
|
||||
p.save()
|
||||
outbuffer = self._render_with_background(buffer)
|
||||
@@ -111,97 +164,198 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib import pagesizes
|
||||
|
||||
pagesize = self.settings.get('pagesize', default='A4')
|
||||
if hasattr(pagesizes, pagesize):
|
||||
pagesize = getattr(pagesizes, pagesize)
|
||||
else:
|
||||
pagesize = pagesizes.A4
|
||||
orientation = self.settings.get('orientation', default='portrait')
|
||||
if hasattr(pagesizes, orientation):
|
||||
pagesize = getattr(pagesizes, orientation)(pagesize)
|
||||
# Doesn't matter as we'll overpaint it over a background later
|
||||
pagesize = pagesizes.A4
|
||||
|
||||
self._register_fonts()
|
||||
return canvas.Canvas(buffer, pagesize=pagesize)
|
||||
|
||||
def _render_with_background(self, buffer):
|
||||
def _render_with_background(self, buffer, title=_('Ticket')):
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfFileReader(buffer)
|
||||
output = PdfFileWriter()
|
||||
bg_file = self.settings.get('background', as_type=File)
|
||||
if isinstance(bg_file, File):
|
||||
if self.override_background:
|
||||
bgf = default_storage.open(self.override_background.name, "rb")
|
||||
elif isinstance(bg_file, File):
|
||||
bgf = default_storage.open(bg_file.name, "rb")
|
||||
else:
|
||||
bgf = open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
|
||||
bg_pdf = PdfFileReader(bgf)
|
||||
|
||||
for page in new_pdf.pages:
|
||||
bg_page = copy.copy(bg_pdf.getPage(0))
|
||||
bg_page.mergePage(page)
|
||||
output.addPage(bg_page)
|
||||
|
||||
output.addMetadata({
|
||||
'/Title': str(title),
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
outbuffer = BytesIO()
|
||||
output.write(outbuffer)
|
||||
outbuffer.seek(0)
|
||||
return outbuffer
|
||||
|
||||
@property
|
||||
def settings_form_fields(self) -> dict:
|
||||
return OrderedDict(
|
||||
list(super().settings_form_fields.items()) + [
|
||||
('paper_size',
|
||||
forms.ChoiceField(
|
||||
label=_('Paper size'),
|
||||
choices=(
|
||||
('A4', 'A4'),
|
||||
('A5', 'A5'),
|
||||
('B4', 'B4'),
|
||||
('B5', 'B5'),
|
||||
('letter', 'Letter'),
|
||||
('legal', 'Legal'),
|
||||
),
|
||||
required=False
|
||||
)),
|
||||
('orientation',
|
||||
forms.ChoiceField(
|
||||
label=_('Paper orientation'),
|
||||
choices=(
|
||||
('portrait', _('Portrait')),
|
||||
('landscape', _('Landscape')),
|
||||
),
|
||||
required=False
|
||||
)),
|
||||
('background',
|
||||
ExtFileField(
|
||||
label=_('Background PDF'),
|
||||
ext_whitelist=(".pdf", ),
|
||||
required=False
|
||||
)),
|
||||
('qr_x', forms.FloatField(label=_('QR-Code x position (mm)'), required=False)),
|
||||
('qr_y', forms.FloatField(label=_('QR-Code y position (mm)'), required=False)),
|
||||
('qr_s', forms.FloatField(label=_('QR-Code size (mm)'), required=False)),
|
||||
('code_x', forms.FloatField(label=_('Ticket code x position (mm)'), required=False)),
|
||||
('code_y', forms.FloatField(label=_('Ticket code y position (mm)'), required=False)),
|
||||
('code_s', forms.FloatField(label=_('Ticket code size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('order_x', forms.FloatField(label=_('Order x position (mm)'), required=False)),
|
||||
('order_y', forms.FloatField(label=_('Order y position (mm)'), required=False)),
|
||||
('order_s', forms.FloatField(label=_('Order size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('name_x', forms.FloatField(label=_('Product name x position (mm)'), required=False)),
|
||||
('name_y', forms.FloatField(label=_('Product name y position (mm)'), required=False)),
|
||||
('name_s', forms.FloatField(label=_('Product name size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('price_x', forms.FloatField(label=_('Price x position (mm)'), required=False)),
|
||||
('price_y', forms.FloatField(label=_('Price y position (mm)'), required=False)),
|
||||
('price_s', forms.FloatField(label=_('Price size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('event_x', forms.FloatField(label=_('Event name x position (mm)'), required=False)),
|
||||
('event_y', forms.FloatField(label=_('Event name y position (mm)'), required=False)),
|
||||
('event_s', forms.FloatField(label=_('Event name size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('attendee_x', forms.FloatField(label=_('Attendee name x position (mm)'), required=False)),
|
||||
('attendee_y', forms.FloatField(label=_('Attendee name y position (mm)'), required=False)),
|
||||
('attendee_s', forms.FloatField(label=_('Attendee name size (mm)'), required=False,
|
||||
help_text=_('Invisible by default, set this to a number greater than 0 '
|
||||
'to show.')))
|
||||
]
|
||||
)
|
||||
def settings_content_render(self, request: HttpRequest) -> str:
|
||||
"""
|
||||
When the event's administrator visits the event configuration
|
||||
page, this method is called. It may return HTML containing additional information
|
||||
that is displayed below the form fields configured in ``settings_form_fields``.
|
||||
"""
|
||||
template = get_template('pretixplugins/ticketoutputpdf/form.html')
|
||||
return template.render({
|
||||
'request': request
|
||||
})
|
||||
|
||||
def _legacy_layout(self):
|
||||
if self.settings.get('background'):
|
||||
return self._migrate_from_old_settings()
|
||||
else:
|
||||
return self._default_layout()
|
||||
|
||||
def _default_layout(self):
|
||||
return [
|
||||
{"type": "textarea", "left": "17.50", "bottom": "274.60", "fontsize": "16.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "175.00", "content": "event_name",
|
||||
"text": "Sample event name", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "262.90", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "itemvar",
|
||||
"text": "Sample product – sample variation", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "252.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "attendee_name",
|
||||
"text": "John Doe", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "242.10", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "event_date",
|
||||
"text": "May 31st, 2017", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "234.30", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "event_location",
|
||||
"text": "Random City", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "30.00", "content": "order",
|
||||
"text": "A1B2C", "align": "left"},
|
||||
{"type": "textarea", "left": "52.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "45.00", "content": "price",
|
||||
"text": "123.45 EUR", "align": "right"},
|
||||
{"type": "textarea", "left": "102.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "90.00", "content": "secret",
|
||||
"text": "tdmruoekvkpbv1o2mv8xccvqcikvr58u", "align": "left"},
|
||||
{"type": "barcodearea", "left": "130.40", "bottom": "204.50", "size": "64.00"}
|
||||
]
|
||||
|
||||
def _migrate_from_old_settings(self):
|
||||
l = []
|
||||
|
||||
event_s = self.settings.get('event_s', default=22, as_type=float)
|
||||
if event_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('event_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('event_y', default=235, as_type=float),
|
||||
'fontsize': event_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'event_name',
|
||||
'text': 'Sample event',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
order_s = self.settings.get('order_s', default=17, as_type=float)
|
||||
if order_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('order_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('order_y', default=220, as_type=float),
|
||||
'fontsize': order_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'order',
|
||||
'text': 'AB1C2',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
name_s = self.settings.get('name_s', default=17, as_type=float)
|
||||
if name_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('name_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('name_y', default=210, as_type=float),
|
||||
'fontsize': name_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'itemvar',
|
||||
'text': 'Sample Producs - XS',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
price_s = self.settings.get('price_s', default=17, as_type=float)
|
||||
if price_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('price_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('price_y', default=200, as_type=float),
|
||||
'fontsize': price_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'price',
|
||||
'text': 'EUR 12,34',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
qr_s = self.settings.get('qr_s', default=80, as_type=float)
|
||||
if qr_s:
|
||||
l.append({
|
||||
'type': 'barcodearea',
|
||||
'left': self.settings.get('qr_x', default=10, as_type=float),
|
||||
'bottom': self.settings.get('qr_y', default=120, as_type=float),
|
||||
'size': qr_s,
|
||||
})
|
||||
|
||||
code_s = self.settings.get('code_s', default=11, as_type=float)
|
||||
if code_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('code_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('code_y', default=120, as_type=float),
|
||||
'fontsize': code_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'secret',
|
||||
'text': 'asdsdgjfgbgkjdastjrxfdg',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
attendee_s = self.settings.get('attendee_s', default=0, as_type=float)
|
||||
if attendee_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('attendee_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('attendee_y', default=90, as_type=float),
|
||||
'fontsize': attendee_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'attendee_name',
|
||||
'text': 'John Doe',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
return l
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/$', views.EditorView.as_view(),
|
||||
name='editor'),
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/webfonts.css',
|
||||
views.FontsCSSView.as_view(),
|
||||
name='css'),
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/(?P<filename>[^/]+).pdf$',
|
||||
views.PdfView.as_view(), name='pdf'),
|
||||
]
|
||||
@@ -0,0 +1,154 @@
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import (
|
||||
FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedFile
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import ChartContainingView
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.plugins.ticketoutputpdf.signals import get_fonts
|
||||
|
||||
from .ticketoutput import PdfTicketOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView):
|
||||
template_name = 'pretixplugins/ticketoutputpdf/index.html'
|
||||
permission = 'can_change_settings'
|
||||
accepted_formats = (
|
||||
'application/pdf',
|
||||
)
|
||||
maxfilesize = 1024 * 1024 * 10
|
||||
minfilesize = 10
|
||||
|
||||
def process_upload(self):
|
||||
f = self.request.FILES.get('background')
|
||||
error = False
|
||||
if f.size > self.maxfilesize:
|
||||
error = _('The uploaded PDF file is to large.')
|
||||
if f.size < self.minfilesize:
|
||||
error = _('The uploaded PDF file is to small.')
|
||||
if mimetypes.guess_type(f.name)[0] not in self.accepted_formats:
|
||||
error = _('Please only upload PDF files.')
|
||||
# if there was an error, add error message to response_data and return
|
||||
if error:
|
||||
return error, None
|
||||
return None, f
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "background" in request.FILES:
|
||||
error, fileobj = self.process_upload()
|
||||
if error:
|
||||
return JsonResponse({
|
||||
"status": "error",
|
||||
"error": error
|
||||
})
|
||||
c = CachedFile()
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
c.type = 'application/pdf'
|
||||
c.file = fileobj
|
||||
c.save()
|
||||
c.refresh_from_db()
|
||||
return JsonResponse({
|
||||
"status": "ok",
|
||||
"id": c.id,
|
||||
"url": reverse('plugins:ticketoutputpdf:pdf', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
'filename': str(c.id)
|
||||
})
|
||||
})
|
||||
|
||||
cf = None
|
||||
if request.POST.get("background", "").strip():
|
||||
try:
|
||||
cf = CachedFile.objects.get(id=request.POST.get("background"))
|
||||
except CachedFile.DoesNotExist:
|
||||
pass
|
||||
|
||||
if "preview" in request.POST:
|
||||
with rolledback_transaction(), language(request.event.settings.locale):
|
||||
item = request.event.items.create(name=_("Sample product"), default_price=42.23)
|
||||
|
||||
from pretix.base.models import Order
|
||||
order = request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||
email='sample@pretix.eu',
|
||||
expires=now(), code="PREVIEW1234", total=119)
|
||||
|
||||
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
|
||||
|
||||
prov = PdfTicketOutput(request.event,
|
||||
override_layout=json.loads(request.POST.get("data")),
|
||||
override_background=cf.file if cf else None)
|
||||
fname, mimet, data = prov.generate(p)
|
||||
|
||||
resp = HttpResponse(data, content_type=mimet)
|
||||
ftype = fname.split(".")[-1]
|
||||
resp['Content-Security-Policy'] = "style-src 'unsafe-inline'; script-src 'unsafe-inline'; object-src 'self'"
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
return resp
|
||||
elif "data" in request.POST:
|
||||
if cf:
|
||||
fexisting = request.event.settings.get('ticketoutput_pdf_layout', as_type=File)
|
||||
if fexisting:
|
||||
try:
|
||||
default_storage.delete(fexisting.name)
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fexisting.name)
|
||||
|
||||
# Create new file
|
||||
nonce = get_random_string(length=8)
|
||||
fname = '%s-%s/%s/%s.%s.%s' % (
|
||||
'event', 'settings', self.request.event.pk, 'ticketoutput_pdf_layout', nonce, 'pdf'
|
||||
)
|
||||
newname = default_storage.save(fname, cf.file)
|
||||
request.event.settings.set('ticketoutput_pdf_background', 'file://' + newname)
|
||||
|
||||
request.event.settings.set('ticketoutput_pdf_layout', request.POST.get("data"))
|
||||
return JsonResponse({'status': 'ok'})
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
prov = PdfTicketOutput(self.request.event)
|
||||
ctx['fonts'] = get_fonts()
|
||||
ctx['layout'] = json.dumps(
|
||||
self.request.event.settings.get('ticketoutput_pdf_layout', as_type=list)
|
||||
or prov._default_layout()
|
||||
)
|
||||
return ctx
|
||||
|
||||
|
||||
class FontsCSSView(TemplateView):
|
||||
content_type = 'text/css'
|
||||
template_name = 'pretixplugins/ticketoutputpdf/webfonts.css'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['fonts'] = get_fonts()
|
||||
return ctx
|
||||
|
||||
|
||||
class PdfView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
|
||||
resp = FileResponse(cf.file, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
|
||||
return resp
|
||||
@@ -27,6 +27,8 @@ from pretix.presale.views.questions import QuestionsViewMixin
|
||||
|
||||
|
||||
class BaseCheckoutFlowStep:
|
||||
requires_valid_cart = True
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
self.request = None
|
||||
@@ -135,6 +137,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
template_name = "pretixpresale/event/checkout_addons.html"
|
||||
task = set_cart_addons
|
||||
known_errortypes = ['CartError']
|
||||
requires_valid_cart = False
|
||||
|
||||
def is_applicable(self, request):
|
||||
return get_cart(request).filter(item__addons__isnull=False).exists()
|
||||
@@ -378,6 +381,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['providers'] = self.provider_forms
|
||||
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
|
||||
ctx['selected'] = self.request.POST.get('payment', self.request.session.get('payment', ''))
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ def contextprocessor(request):
|
||||
ctx['event_logo'] = request.event.settings.get('logo_image', as_type=str, default='')[7:]
|
||||
ctx['event'] = request.event
|
||||
|
||||
if hasattr(request, 'organizer'):
|
||||
ctx['organizer_logo'] = request.organizer.settings.get('organizer_logo_image', as_type=str, default='')[7:]
|
||||
ctx['organizer'] = request.organizer
|
||||
|
||||
ctx['html_head'] = "".join(_html_head)
|
||||
ctx['html_foot'] = "".join(_html_foot)
|
||||
ctx['footer'] = _footer
|
||||
|
||||
@@ -3,12 +3,17 @@ from decimal import Decimal
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Prefetch, Q
|
||||
from django.forms.widgets import RadioChoiceInput, RadioFieldRenderer
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.formats import number_format
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import ItemVariation, Question
|
||||
from pretix.base.models.orders import InvoiceAddress
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
|
||||
|
||||
class ContactForm(forms.Form):
|
||||
@@ -146,6 +151,63 @@ class QuestionsForm(forms.Form):
|
||||
self.fields['question_%s' % q.id] = field
|
||||
|
||||
|
||||
# The following will get totally different once Django 1.11 is integrated
|
||||
class AddOnVariationSelectInput(RadioChoiceInput):
|
||||
|
||||
def __init__(self, name, value, attrs, choice, index):
|
||||
super().__init__(name, value, attrs, choice, index)
|
||||
self.description = force_text(choice[2])
|
||||
|
||||
def render(self, name=None, value=None, attrs=None):
|
||||
if self.id_for_label:
|
||||
label_for = format_html(' for="{}"', self.id_for_label)
|
||||
else:
|
||||
label_for = ''
|
||||
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
|
||||
if self.description:
|
||||
return format_html(
|
||||
'<label{}>{} {}</label> <span class="fa fa-info-circle toggle-variation-description"></span>'
|
||||
'<div class="variation-description addon-variation-description">{}</div>',
|
||||
label_for, self.tag(attrs), self.choice_label,
|
||||
rich_text(str(self.description))
|
||||
)
|
||||
else:
|
||||
return format_html(
|
||||
'<label{}>{} {}</label>',
|
||||
label_for, self.tag(attrs), self.choice_label,
|
||||
)
|
||||
|
||||
|
||||
class AddOnVariationSelectRenderer(RadioFieldRenderer):
|
||||
choice_input_class = AddOnVariationSelectInput
|
||||
|
||||
def render(self):
|
||||
id_ = self.attrs.get('id')
|
||||
output = []
|
||||
for i, choice in enumerate(self.choices):
|
||||
w = self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i)
|
||||
output.append(format_html(self.inner_html, choice_value=force_text(w), sub_widgets=''))
|
||||
return format_html(
|
||||
self.outer_html,
|
||||
id_attr=format_html(' id="{}"', id_) if id_ else '',
|
||||
content=mark_safe('\n'.join(output)),
|
||||
)
|
||||
|
||||
|
||||
class AddOnVariationSelect(forms.RadioSelect):
|
||||
renderer = AddOnVariationSelectRenderer
|
||||
|
||||
|
||||
class AddOnVariationField(forms.ChoiceField):
|
||||
|
||||
def valid_value(self, value):
|
||||
text_value = force_text(value)
|
||||
for k, v, d in self.choices:
|
||||
if value == k or text_value == force_text(k):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AddOnsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for selecting add-ons to a product in the cart.
|
||||
@@ -232,18 +294,18 @@ class AddOnsForm(forms.Form):
|
||||
|
||||
for i in items:
|
||||
if i.has_variations:
|
||||
choices = [('', _('no selection'))]
|
||||
choices = [('', _('no selection'), '')]
|
||||
for v in i.available_variations:
|
||||
cached_availability = v.check_quotas(_cache=quota_cache)
|
||||
choices.append((v.pk, self._label(event, v, cached_availability)))
|
||||
choices.append((v.pk, self._label(event, v, cached_availability), v.description))
|
||||
|
||||
field = forms.ChoiceField(
|
||||
field = AddOnVariationField(
|
||||
choices=choices,
|
||||
label=i.name,
|
||||
required=False,
|
||||
widget=forms.RadioSelect,
|
||||
help_text=i.description,
|
||||
initial=current_addons.get(i.pk)
|
||||
widget=AddOnVariationSelect,
|
||||
help_text=rich_text(str(i.description)),
|
||||
initial=current_addons.get(i.pk),
|
||||
)
|
||||
else:
|
||||
cached_availability = i.check_quotas(_cache=quota_cache)
|
||||
@@ -251,7 +313,7 @@ class AddOnsForm(forms.Form):
|
||||
label=self._label(event, i, cached_availability),
|
||||
required=False,
|
||||
initial=i.pk in current_addons,
|
||||
help_text=i.description
|
||||
help_text=rich_text(str(i.description)),
|
||||
)
|
||||
|
||||
self.fields['item_%s' % i.pk] = field
|
||||
|
||||
@@ -20,10 +20,10 @@ def regenerate_css(event_id: int):
|
||||
event = Event.objects.select_related('organizer').get(pk=event_id)
|
||||
sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss')
|
||||
|
||||
sassrules = [
|
||||
'$brand-primary: {};'.format(event.settings.get('primary_color')),
|
||||
'@import "main.scss";',
|
||||
]
|
||||
sassrules = []
|
||||
if event.settings.get('primary_color'):
|
||||
sassrules.append('$brand-primary: {};'.format(event.settings.get('primary_color')))
|
||||
sassrules.append('@import "main.scss";')
|
||||
|
||||
css = sass.compile(
|
||||
string="\n".join(sassrules),
|
||||
|
||||
@@ -22,16 +22,18 @@
|
||||
{% endblock %}
|
||||
{% block page %}
|
||||
<div class="page-header">
|
||||
<h1 class="pull-left">
|
||||
<div class="pull-left">
|
||||
{% if event_logo %}
|
||||
<a href="{% eventurl event "presale:event.index" %}" title="{{ event.name }}">
|
||||
<img src="{{ event_logo|thumbnail_url:'logo' }}" alt="{{ event.name }}" class="event-logo" />
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% eventurl event "presale:event.index" %}">{{ event.name }}</a>
|
||||
<small>{{ event.get_date_range_display }}</small>
|
||||
<h1>
|
||||
<a href="{% eventurl event "presale:event.index" %}">{{ event.name }}</a>
|
||||
<small>{{ event.get_date_range_display }}</small>
|
||||
</h1>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="pull-right loginbox">
|
||||
{% if request.event.settings.locales|length > 1 %}
|
||||
<div class="locales">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" href="#cp{{ form.pos.id }}">
|
||||
<a data-toggle="collapse" href="#cp{{ form.cartpos.pk }}">
|
||||
<strong>{{ form.item.name }}</strong>
|
||||
{% if form.variation %}
|
||||
– {{ form.variation }}
|
||||
@@ -22,7 +22,7 @@
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="cp{{ form.pos.id }}"
|
||||
<div id="cp{{ form.cartpos.pk }}"
|
||||
class="panel-collapse collapsed in">
|
||||
<div class="panel-body">
|
||||
{% for c in form.categories %}
|
||||
@@ -35,7 +35,7 @@
|
||||
{% endblocktrans %}
|
||||
{% elif c.min_count == 0 %}
|
||||
{% blocktrans trimmed with max_count=c.max_count %}
|
||||
You can to choose up to {{ max_count }} options from this category.
|
||||
You can choose up to {{ max_count }} options from this category.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<label class="radio">
|
||||
<strong class="pull-right">+ {{ p.fee|floatformat:2 }} {{ event.currency }}</strong>
|
||||
{% if show_fees %}
|
||||
<strong class="pull-right">+ {{ p.fee|floatformat:2 }} {{ event.currency }}</strong>
|
||||
{% endif %}
|
||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||
data-parent="#payment_accordion"
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% for line in cart.positions %}
|
||||
<div class="row cart-row {% if download %}has-downloads{% endif %}">
|
||||
<div class="row cart-row {% if download and line.item.admission|default:event.settings.ticket_download_nonadm %}has-downloads{% endif %}">
|
||||
<div class="product">
|
||||
{% if line.addon_to %}
|
||||
<span class="addon-signifier">+</span>
|
||||
@@ -33,7 +33,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if download %}
|
||||
{% if download and line.item.admission|default:event.settings.ticket_download_nonadm %}
|
||||
<div class="download-desktop">
|
||||
{% if not line.addon_to or event.settings.ticket_download_addons %}
|
||||
{% for b in download_buttons %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% block title %}{% trans "Presale" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if cart.positions %}
|
||||
{% if cart.positions and event.presale_is_running %}
|
||||
<div class="panel panel-primary cart">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Your cart" %}</h3>
|
||||
@@ -28,13 +28,19 @@
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-block btn-default btn-lg" type="submit"><i class="fa fa-close"></i> {% trans "Empty cart" %}</button>
|
||||
<button class="btn btn-block btn-default btn-lg" type="submit">
|
||||
<i class="fa fa-close"></i> {% trans "Empty cart" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4 col-xs-12">
|
||||
<a class="btn btn-block btn-primary btn-lg"
|
||||
href="{% eventurl request.event "presale:event.checkout.start" %}">
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
|
||||
{% if has_addon_choices %}
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Continue" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
@@ -61,15 +67,57 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if frontpage_text %}
|
||||
<div>
|
||||
{{ frontpage_text|rich_text }}
|
||||
</div>
|
||||
<div>
|
||||
{{ frontpage_text|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-right">
|
||||
<a href="{% eventurl event "presale:event.ical.download" %}" class="btn btn-link btn-xs">
|
||||
<i class="fa fa-calendar"></i> {% trans "Add to Calendar" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
{% if event.location %}
|
||||
<div class="info-row">
|
||||
<span class="fa fa-map-marker fa-fw"></span>
|
||||
<p>
|
||||
{{ event.location|linebreaksbr }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.settings.show_times or event.date_admission %}
|
||||
<div class="info-row">
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
<p>
|
||||
{% if event.settings.show_times %}
|
||||
{% blocktrans trimmed with time=event.date_from|date:"TIME_FORMAT" %}
|
||||
Begin: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% if event.date_admission and event.settings.show_times %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if event.date_admission %}
|
||||
{% if event.date_admission|date:"SHORT_DATE_FORMAT" == event.date_from|date:"SHORT_DATE_FORMAT" %}
|
||||
{% blocktrans trimmed with time=event.date_admission|date:"TIME_FORMAT" %}
|
||||
Admission: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with datetime=event.date_admission|date:"SHORT_DATETIME_FORMAT" %}
|
||||
Admission: {{ datetime }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>
|
||||
<a href="{% eventurl event "presale:event.ical.download" %}">
|
||||
{% trans "Add to Calendar" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-right">
|
||||
<a href="{% eventurl event "presale:event.ical.download" %}" class="btn-xs">
|
||||
<i class="fa fa-calendar"></i> {% trans "Add to Calendar" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% eventsignal event "pretix.presale.signals.front_page_top" %}
|
||||
@@ -101,14 +149,19 @@
|
||||
<a href="#" data-toggle="variations">
|
||||
<strong>{{ item.name }}</strong>
|
||||
</a>
|
||||
{% if item.description %}<p>{{ item.description|localize|rich_text }}</p>
|
||||
{% if item.description %}
|
||||
<div class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.min_per_order %}
|
||||
<p><small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small></p>
|
||||
<p>
|
||||
<small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
@@ -134,6 +187,11 @@
|
||||
<div class="row-fluid product-row variation">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{{ var }}
|
||||
{% if var.description %}
|
||||
<div class="variation-description">
|
||||
{{ var.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.settings.show_quota_left %}
|
||||
{% include "pretixpresale/event/fragment_quota_left.html" with avail=var.cached_availability %}
|
||||
{% endif %}
|
||||
@@ -201,16 +259,21 @@
|
||||
{% endif %}
|
||||
<strong>{{ item.name }}</strong>
|
||||
{% if item.description %}
|
||||
<p class="description">{{ item.description|localize|rich_text }}</p>{% endif %}
|
||||
<div class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.settings.show_quota_left %}
|
||||
{% include "pretixpresale/event/fragment_quota_left.html" with avail=item.cached_availability %}
|
||||
{% endif %}
|
||||
{% if item.min_per_order %}
|
||||
<p><small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small></p>
|
||||
<p>
|
||||
<small>
|
||||
{% blocktrans trimmed with num=item.min_per_order %}
|
||||
minimum amount to order: {{ num }}
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
{% endif %}
|
||||
<strong>{{ item.name }}</strong>
|
||||
{% if item.description %}
|
||||
<p>{{ item.description|localize|rich_text }}</p>{% endif %}
|
||||
<div class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if item.min_price != item.max_price or item.free_price %}
|
||||
@@ -57,6 +60,11 @@
|
||||
<div class="row-fluid product-row variation">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{{ var }}
|
||||
{% if var.description %}
|
||||
<div class="variation-description">
|
||||
{{ var.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if item.free_price %}
|
||||
@@ -116,7 +124,10 @@
|
||||
{% endif %}
|
||||
<strong>{{ item.name }}</strong>
|
||||
{% if item.description %}
|
||||
<p class="description">{{ item.description|localize|rich_text }}</p>{% endif %}
|
||||
<div class="product-description">
|
||||
{{ item.description|localize|rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{% if item.free_price %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user