forked from CGM_Public/pretix_original
@@ -11,7 +11,8 @@ Core
|
|||||||
----
|
----
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||||
|
item_copy_data
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -56,6 +57,12 @@ Backend
|
|||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
|
.. automodule:: pretix.control.signals
|
||||||
|
:members: item_forms
|
||||||
|
|
||||||
|
Vouchers
|
||||||
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
:members: voucher_form_class, voucher_form_html, voucher_form_validation
|
:members: voucher_form_class, voucher_form_html, voucher_form_validation
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
teams = self._get_teams_for_event(organizer, event)
|
teams = self._get_teams_for_event(organizer, event)
|
||||||
if teams:
|
if teams:
|
||||||
self._teamcache['e{}'.format(event.pk)] = teams
|
self._teamcache['e{}'.format(event.pk)] = teams
|
||||||
|
if isinstance(perm_name, (tuple, list)):
|
||||||
|
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
|
||||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -266,6 +268,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
return True
|
return True
|
||||||
teams = self._get_teams_for_organizer(organizer)
|
teams = self._get_teams_for_organizer(organizer)
|
||||||
if teams:
|
if teams:
|
||||||
|
if isinstance(perm_name, (tuple, list)):
|
||||||
|
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
|
||||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ class TeamAPIToken(models.Model):
|
|||||||
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
|
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
|
||||||
event in self.team.limit_events.all()
|
event in self.team.limit_events.all()
|
||||||
)
|
)
|
||||||
|
if isinstance(perm_name, (tuple, list)):
|
||||||
|
return has_event_access and any(self.team.has_permission(p) for p in perm_name)
|
||||||
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
|
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
|
||||||
|
|
||||||
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||||
@@ -290,6 +292,8 @@ class TeamAPIToken(models.Model):
|
|||||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
|
if isinstance(perm_name, (tuple, list)):
|
||||||
|
return organizer == self.team.organizer and any(self.team.has_permission(p) for p in perm_name)
|
||||||
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
|
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
|
||||||
|
|
||||||
def get_events_with_any_permission(self):
|
def get_events_with_any_permission(self):
|
||||||
|
|||||||
@@ -1,12 +1,36 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import bleach
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from PyPDF2 import PdfFileReader
|
||||||
from pytz import timezone
|
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, OrderPosition
|
||||||
from pretix.base.signals import layout_text_variables
|
from pretix.base.signals import layout_text_variables
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_VARIABLES = OrderedDict((
|
DEFAULT_VARIABLES = OrderedDict((
|
||||||
("secret", {
|
("secret", {
|
||||||
@@ -157,3 +181,118 @@ def get_variables(event):
|
|||||||
for recv, res in layout_text_variables.send(sender=event):
|
for recv, res in layout_text_variables.send(sender=event):
|
||||||
v.update(res)
|
v.update(res)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class Renderer:
|
||||||
|
|
||||||
|
def __init__(self, event, layout, background_file):
|
||||||
|
self.layout = layout
|
||||||
|
self.background_file = background_file
|
||||||
|
self.variables = get_variables(event)
|
||||||
|
if self.background_file:
|
||||||
|
self.bg_pdf = PdfFileReader(BytesIO(self.background_file.read()))
|
||||||
|
else:
|
||||||
|
self.bg_pdf = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _register_fonts(cls):
|
||||||
|
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')))
|
||||||
|
|
||||||
|
for family, styles in get_fonts().items():
|
||||||
|
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||||
|
if 'italic' in styles:
|
||||||
|
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||||
|
if 'bold' in styles:
|
||||||
|
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||||
|
if 'bolditalic' in styles:
|
||||||
|
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
|
||||||
|
ev = op.subevent or order.event
|
||||||
|
if not o['content']:
|
||||||
|
return '(error)'
|
||||||
|
if o['content'] == 'other':
|
||||||
|
return o['text'].replace("\n", "<br/>\n")
|
||||||
|
elif o['content'].startswith('meta:'):
|
||||||
|
return ev.meta_data.get(o['content'][5:]) or ''
|
||||||
|
elif o['content'] in self.variables:
|
||||||
|
try:
|
||||||
|
return self.variables[o['content']]['evaluate'](op, order, ev)
|
||||||
|
except:
|
||||||
|
logger.exception('Failed to process variable.')
|
||||||
|
return '(error)'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
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']]
|
||||||
|
)
|
||||||
|
text = re.sub(
|
||||||
|
"<br[^>]*>", "<br/>",
|
||||||
|
bleach.clean(
|
||||||
|
self._get_text_content(op, order, o) or "",
|
||||||
|
tags=["br"], attributes={}, styles=[], strip=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
p = Paragraph(text, 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])
|
||||||
|
|
||||||
|
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition):
|
||||||
|
for o in self.layout:
|
||||||
|
if o['type'] == "barcodearea":
|
||||||
|
self._draw_barcodearea(canvas, op, o)
|
||||||
|
elif o['type'] == "textarea":
|
||||||
|
self._draw_textarea(canvas, op, order, o)
|
||||||
|
canvas.showPage()
|
||||||
|
|
||||||
|
def render_background(self, buffer, title=_('Ticket')):
|
||||||
|
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||||
|
buffer.seek(0)
|
||||||
|
new_pdf = PdfFileReader(buffer)
|
||||||
|
output = PdfFileWriter()
|
||||||
|
|
||||||
|
for page in new_pdf.pages:
|
||||||
|
bg_page = copy.copy(self.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
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
event_copy_data = EventPluginSignal(
|
event_copy_data = EventPluginSignal(
|
||||||
providing_args=["other"]
|
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map"]
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
This signal is sent out when a new event is created as a clone of an existing event, i.e.
|
This signal is sent out when a new event is created as a clone of an existing event, i.e.
|
||||||
@@ -275,6 +275,18 @@ mappings from object IDs in the original event to objects in the new event of th
|
|||||||
types.
|
types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
item_copy_data = EventPluginSignal(
|
||||||
|
providing_args=["source", "target"]
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
This signal is sent out when a new product is created as a clone of an existing product, i.e.
|
||||||
|
the settings from the older product are copied to the newer one. You can listen to this
|
||||||
|
signal to copy data or configuration stored within your plugin's models as well.
|
||||||
|
|
||||||
|
The ``sender`` keyword argument will contain the event. The ``target`` will contain the item to
|
||||||
|
copy to, the ``source`` keyword argument will contain the product to **copy from**.
|
||||||
|
"""
|
||||||
|
|
||||||
periodic_task = django.dispatch.Signal()
|
periodic_task = django.dispatch.Signal()
|
||||||
"""
|
"""
|
||||||
This is a regular django signal (no pretix event signal) that we send out every
|
This is a regular django signal (no pretix event signal) that we send out every
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from pretix.base.models import (
|
|||||||
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
|
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.models.items import ItemAddOn
|
from pretix.base.models.items import ItemAddOn
|
||||||
|
from pretix.base.signals import item_copy_data
|
||||||
from pretix.control.forms import SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimePickerWidget
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.helpers.money import change_decimal_field
|
from pretix.helpers.money import change_decimal_field
|
||||||
@@ -255,6 +256,8 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
for question in self.cleaned_data['copy_from'].questions.all():
|
for question in self.cleaned_data['copy_from'].questions.all():
|
||||||
question.items.add(instance)
|
question.items.add(instance)
|
||||||
|
|
||||||
|
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ Deprecated signal, no longer works. We just keep the definition so old plugins d
|
|||||||
break the installation.
|
break the installation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
nav_organizer = Signal(
|
nav_organizer = Signal(
|
||||||
providing_args=['organizer', 'request']
|
providing_args=['organizer', 'request']
|
||||||
)
|
)
|
||||||
@@ -192,7 +191,6 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
|||||||
Additionally, the argument ``order`` and ``request`` are available.
|
Additionally, the argument ``order`` and ``request`` are available.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
nav_event_settings = EventPluginSignal(
|
nav_event_settings = EventPluginSignal(
|
||||||
providing_args=['request']
|
providing_args=['request']
|
||||||
)
|
)
|
||||||
@@ -221,3 +219,16 @@ that allows generating a pretix Widget code.
|
|||||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
A second keyword argument ``request`` will contain the request object.
|
A second keyword argument ``request`` will contain the request object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
item_forms = EventPluginSignal(
|
||||||
|
providing_args=['request', 'item']
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
This signal allows you to return additional forms that should be rendered on the product
|
||||||
|
modification page. You are passed ``request`` and ``item`` arguments and are expected to return
|
||||||
|
an instance of a form class that you bind yourself when appropriate. Your form will be executed
|
||||||
|
as part of the standard validation and rendering cycle and rendered using default bootstrap
|
||||||
|
styles. It is advisable to set a prefix for your form to avoid clashes with other plugins.
|
||||||
|
|
||||||
|
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -35,6 +35,14 @@
|
|||||||
<legend>{% trans "Check-in" %}</legend>
|
<legend>{% trans "Check-in" %}</legend>
|
||||||
{% bootstrap_field form.checkin_attention layout="control" %}
|
{% bootstrap_field form.checkin_attention layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
{% if plugin_forms %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Additional settings" %}</legend>
|
||||||
|
{% for f in plugin_forms %}
|
||||||
|
{% bootstrap_form f layout="control" %}
|
||||||
|
{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-lg-2">
|
<div class="col-xs-12 col-lg-2">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from pretix.control.forms.item import (
|
|||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
EventPermissionRequiredMixin, event_permission_required,
|
EventPermissionRequiredMixin, event_permission_required,
|
||||||
)
|
)
|
||||||
|
from pretix.control.signals import item_forms
|
||||||
|
|
||||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||||
|
|
||||||
@@ -332,7 +333,6 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class QuestionMixin:
|
class QuestionMixin:
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def formset(self):
|
def formset(self):
|
||||||
formsetclass = inlineformset_factory(
|
formsetclass = inlineformset_factory(
|
||||||
@@ -420,8 +420,8 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
elif self.object.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
elif self.object.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||||
qs = qs.order_by('options').values('options', 'options__answer')\
|
qs = qs.order_by('options').values('options', 'options__answer') \
|
||||||
.annotate(count=Count('id')).order_by('-count')
|
.annotate(count=Count('id')).order_by('-count')
|
||||||
for a in qs:
|
for a in qs:
|
||||||
a['answer'] = str(a['options__answer'])
|
a['answer'] = str(a['options__answer'])
|
||||||
del a['options__answer']
|
del a['options__answer']
|
||||||
@@ -684,8 +684,8 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if ((form.initial.get('subevent') and not form.instance.subevent)
|
if ((form.initial.get('subevent') and not form.instance.subevent) or
|
||||||
or (form.instance.subevent and form.initial.get('subevent') != form.instance.subevent.pk)):
|
(form.instance.subevent and form.initial.get('subevent') != form.instance.subevent.pk)):
|
||||||
|
|
||||||
if form.initial.get('subevent'):
|
if form.initial.get('subevent'):
|
||||||
se = SubEvent.objects.get(event=self.request.event, pk=form.initial.get('subevent'))
|
se = SubEvent.objects.get(event=self.request.event, pk=form.initial.get('subevent'))
|
||||||
@@ -817,6 +817,13 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
|
|||||||
template_name = 'pretixcontrol/item/index.html'
|
template_name = 'pretixcontrol/item/index.html'
|
||||||
permission = 'can_change_items'
|
permission = 'can_change_items'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def plugin_forms(self):
|
||||||
|
forms = []
|
||||||
|
for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
|
||||||
|
forms.append(resp)
|
||||||
|
return forms
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
return reverse('control:event.item', kwargs={
|
return reverse('control:event.item', kwargs={
|
||||||
'organizer': self.request.event.organizer.slug,
|
'organizer': self.request.event.organizer.slug,
|
||||||
@@ -824,25 +831,48 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
|
|||||||
'item': self.get_object().id,
|
'item': self.get_object().id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.get_object()
|
||||||
|
form = self.get_form()
|
||||||
|
if form.is_valid() and all(f.is_valid() for f in self.plugin_forms):
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
messages.success(self.request, _('Your changes have been saved.'))
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
if form.has_changed():
|
if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
|
||||||
|
data = {
|
||||||
|
k: (form.cleaned_data.get(k).name
|
||||||
|
if isinstance(form.cleaned_data.get(k), File)
|
||||||
|
else form.cleaned_data.get(k))
|
||||||
|
for k in form.changed_data
|
||||||
|
}
|
||||||
|
for f in self.plugin_forms:
|
||||||
|
data.update({
|
||||||
|
k: (f.cleaned_data.get(k).name
|
||||||
|
if isinstance(f.cleaned_data.get(k), File)
|
||||||
|
else f.cleaned_data.get(k))
|
||||||
|
for k in f.changed_data
|
||||||
|
})
|
||||||
self.object.log_action(
|
self.object.log_action(
|
||||||
'pretix.event.item.changed', user=self.request.user, data={
|
'pretix.event.item.changed', user=self.request.user, data=data
|
||||||
k: (form.cleaned_data.get(k).name
|
|
||||||
if isinstance(form.cleaned_data.get(k), File)
|
|
||||||
else form.cleaned_data.get(k))
|
|
||||||
for k in form.changed_data
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
CachedTicket.objects.filter(order_position__item=self.item).delete()
|
CachedTicket.objects.filter(order_position__item=self.item).delete()
|
||||||
|
for f in self.plugin_forms:
|
||||||
|
f.save()
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data()
|
||||||
|
ctx['plugin_forms'] = self.plugin_forms
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||||
permission = 'can_change_items'
|
permission = 'can_change_items'
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
|
from pretix import __version__ as version
|
||||||
|
|
||||||
|
|
||||||
|
class BadgesApp(AppConfig):
|
||||||
|
name = 'pretix.plugins.badges'
|
||||||
|
verbose_name = _("Badges")
|
||||||
|
|
||||||
|
class PretixPluginMeta:
|
||||||
|
name = _("Badges")
|
||||||
|
author = _("the pretix team")
|
||||||
|
version = version
|
||||||
|
description = _("This plugin allows you to generate badges or name tags for your attendees.")
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals # NOQA
|
||||||
|
|
||||||
|
def installed(self, event):
|
||||||
|
if not event.badge_layouts.exists():
|
||||||
|
event.badge_layouts.create(
|
||||||
|
name=ugettext('Default'),
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
default_app_config = 'pretix.plugins.badges.BadgesApp'
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import json
|
||||||
|
from collections import OrderedDict
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.core.files import File
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from PyPDF2 import PdfFileMerger
|
||||||
|
from reportlab.lib import pagesizes
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
|
||||||
|
from pretix.base.exporter import BaseExporter
|
||||||
|
from pretix.base.i18n import language
|
||||||
|
from pretix.base.models import Order, OrderPosition
|
||||||
|
from pretix.base.pdf import Renderer
|
||||||
|
from pretix.plugins.badges.models import BadgeItem, BadgeLayout
|
||||||
|
|
||||||
|
|
||||||
|
def _renderer(event, layout):
|
||||||
|
if isinstance(layout.background, File) and layout.background.name:
|
||||||
|
bgf = default_storage.open(layout.background.name, "rb")
|
||||||
|
else:
|
||||||
|
bgf = open(finders.find('pretixplugins/badges/badge_default_a6l.pdf'), "rb")
|
||||||
|
return Renderer(event, json.loads(layout.layout), bgf)
|
||||||
|
|
||||||
|
|
||||||
|
def render_pdf(event, positions):
|
||||||
|
Renderer._register_fonts()
|
||||||
|
|
||||||
|
renderermap = {
|
||||||
|
bi.item_id: _renderer(event, bi.layout)
|
||||||
|
for bi in BadgeItem.objects.select_related('layout').filter(item__event=event)
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
default_renderer = _renderer(event, event.badge_layouts.get(default=True))
|
||||||
|
except BadgeLayout.DoesNotExist:
|
||||||
|
default_renderer = None
|
||||||
|
merger = PdfFileMerger()
|
||||||
|
|
||||||
|
for op in positions:
|
||||||
|
r = renderermap.get(op.item_id, default_renderer)
|
||||||
|
if not r:
|
||||||
|
continue
|
||||||
|
|
||||||
|
with language(op.order.locale):
|
||||||
|
buffer = BytesIO()
|
||||||
|
p = canvas.Canvas(buffer, pagesize=pagesizes.A4)
|
||||||
|
r.draw_page(p, op.order, op)
|
||||||
|
p.save()
|
||||||
|
outbuffer = r.render_background(buffer, 'Badge')
|
||||||
|
merger.append(ContentFile(outbuffer.read()))
|
||||||
|
|
||||||
|
outbuffer = BytesIO()
|
||||||
|
merger.write(outbuffer)
|
||||||
|
merger.close()
|
||||||
|
outbuffer.seek(0)
|
||||||
|
return outbuffer
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeExporter(BaseExporter):
|
||||||
|
identifier = "badges"
|
||||||
|
verbose_name = _("Attendee badges")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def export_form_fields(self):
|
||||||
|
d = OrderedDict(
|
||||||
|
[
|
||||||
|
('items',
|
||||||
|
forms.ModelMultipleChoiceField(
|
||||||
|
queryset=self.event.items.all(),
|
||||||
|
label=_('Limit to products'),
|
||||||
|
widget=forms.CheckboxSelectMultiple(
|
||||||
|
attrs={'class': 'scrolling-multiple-choice'}
|
||||||
|
),
|
||||||
|
initial=self.event.items.filter(admission=True)
|
||||||
|
)),
|
||||||
|
('include_pending',
|
||||||
|
forms.BooleanField(
|
||||||
|
label=_('Include pending orders'),
|
||||||
|
required=False
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def render(self, form_data: dict) -> Tuple[str, str, str]:
|
||||||
|
qs = OrderPosition.objects.filter(
|
||||||
|
order__event=self.event, item_id__in=form_data['items']
|
||||||
|
).prefetch_related(
|
||||||
|
'answers', 'answers__question'
|
||||||
|
).select_related('order', 'item', 'variation', 'addon_to')
|
||||||
|
|
||||||
|
if form_data.get('include_pending'):
|
||||||
|
qs = qs.filter(order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING])
|
||||||
|
else:
|
||||||
|
qs = qs.filter(order__status__in=[Order.STATUS_PAID])
|
||||||
|
|
||||||
|
outbuffer = render_pdf(self.event, qs)
|
||||||
|
return 'badges.pdf', 'application/pdf', outbuffer.read()
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.plugins.badges.models import BadgeItem, BadgeLayout
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeLayoutForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = BadgeLayout
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeItemForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = BadgeItem
|
||||||
|
fields = ('layout',)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
event = kwargs.pop('event')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['layout'].label = _('Badge layout')
|
||||||
|
self.fields['layout'].queryset = event.badge_layouts.all()
|
||||||
|
self.fields['layout'].required = False
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
if self.cleaned_data['layout'] is None:
|
||||||
|
if self.instance.pk:
|
||||||
|
self.instance.delete()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return super().save(commit=commit)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.11 on 2018-04-16 08:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.base
|
||||||
|
import pretix.plugins.badges.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0088_auto_20180328_1217'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BadgeItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='badge_assignment', to='pretixbase.Item')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BadgeLayout',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('default', models.BooleanField(default=True, verbose_name='Default')),
|
||||||
|
('name', models.CharField(max_length=190, verbose_name='Name')),
|
||||||
|
('layout', models.TextField()),
|
||||||
|
('background', models.FileField(blank=True, max_length=255, null=True, upload_to=pretix.plugins.badges.models.bg_name)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_layouts', to='pretixbase.Event')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('-default', 'name'),
|
||||||
|
},
|
||||||
|
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='badgeitem',
|
||||||
|
name='layout',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='badges.BadgeLayout'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import string
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.models import LoggedModel
|
||||||
|
|
||||||
|
|
||||||
|
def bg_name(instance, filename: str) -> str:
|
||||||
|
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
||||||
|
return 'pub/{org}/{ev}/badges/{id}-{secret}.pdf'.format(
|
||||||
|
org=instance.event.organizer.slug,
|
||||||
|
ev=instance.event.slug,
|
||||||
|
id=instance.pk,
|
||||||
|
secret=secret
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeLayout(LoggedModel):
|
||||||
|
event = models.ForeignKey(
|
||||||
|
'pretixbase.Event',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='badge_layouts'
|
||||||
|
)
|
||||||
|
default = models.BooleanField(
|
||||||
|
verbose_name=_('Default'),
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=190,
|
||||||
|
verbose_name=_('Name')
|
||||||
|
)
|
||||||
|
layout = models.TextField(
|
||||||
|
default='[{"type":"textarea","left":"13.09","bottom":"49.73","fontsize":"23.6","color":[0,0,0,1],'
|
||||||
|
'"fontfamily":"Open Sans","bold":true,"italic":false,"width":"121.83","content":"attendee_name",'
|
||||||
|
'"text":"Max Mustermann","align":"center"}]'
|
||||||
|
)
|
||||||
|
background = models.FileField(null=True, blank=True, upload_to=bg_name, max_length=255)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeItem(models.Model):
|
||||||
|
item = models.OneToOneField('pretixbase.Item', null=True, blank=True, related_name='badge_assignment',
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
layout = models.ForeignKey('BadgeLayout', on_delete=models.CASCADE, related_name='item_assignments')
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import copy
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.urls import resolve, reverse
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.models import Event, Order
|
||||||
|
from pretix.base.signals import (
|
||||||
|
event_copy_data, item_copy_data, logentry_display, logentry_object_link,
|
||||||
|
register_data_exporters,
|
||||||
|
)
|
||||||
|
from pretix.control.signals import item_forms, nav_event, order_info
|
||||||
|
from pretix.plugins.badges.forms import BadgeItemForm
|
||||||
|
from pretix.plugins.badges.models import BadgeItem, BadgeLayout
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(nav_event, dispatch_uid="badges_nav")
|
||||||
|
def control_nav_import(sender, request=None, **kwargs):
|
||||||
|
url = resolve(request.path_info)
|
||||||
|
p = (
|
||||||
|
request.user.has_event_permission(request.organizer, request.event, 'can_change_settings')
|
||||||
|
or request.user.has_event_permission(request.organizer, request.event, 'can_view_orders')
|
||||||
|
)
|
||||||
|
if not p:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'label': _('Badges'),
|
||||||
|
'url': reverse('plugins:badges:index', kwargs={
|
||||||
|
'event': request.event.slug,
|
||||||
|
'organizer': request.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'active': url.namespace == 'plugins:badges',
|
||||||
|
'icon': 'id-card',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(item_forms, dispatch_uid="badges_item_forms")
|
||||||
|
def control_item_forms(sender, request, item, **kwargs):
|
||||||
|
try:
|
||||||
|
inst = BadgeItem.objects.get(item=item)
|
||||||
|
except BadgeItem.DoesNotExist:
|
||||||
|
inst = BadgeItem(item=item)
|
||||||
|
return BadgeItemForm(
|
||||||
|
instance=inst,
|
||||||
|
event=sender,
|
||||||
|
data=(request.POST if request.method == "POST" else None),
|
||||||
|
prefix="badgeitem"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(item_copy_data, dispatch_uid="badges_item_copy")
|
||||||
|
def copy_item(sender, source, target, **kwargs):
|
||||||
|
try:
|
||||||
|
inst = BadgeItem.objects.get(item=source)
|
||||||
|
BadgeItem.objects.create(item=target, layout=inst.layout)
|
||||||
|
except BadgeItem.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signal=event_copy_data, dispatch_uid="badges_copy_data")
|
||||||
|
def event_copy_data_receiver(sender, other, item_map, **kwargs):
|
||||||
|
layout_map = {}
|
||||||
|
for bl in other.badge_layouts.all():
|
||||||
|
oldid = bl.pk
|
||||||
|
bl = copy.copy(bl)
|
||||||
|
bl.pk = None
|
||||||
|
bl.event = sender
|
||||||
|
bl.save()
|
||||||
|
layout_map[oldid] = bl
|
||||||
|
|
||||||
|
for bi in BadgeItem.objects.filter(item__event=other):
|
||||||
|
BadgeItem.objects.create(item=item_map.get(bi.item_id), layout=layout_map.get(bi.layout_id))
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_data_exporters, dispatch_uid="badges_export_all")
|
||||||
|
def register_pdf(sender, **kwargs):
|
||||||
|
from .exporters import BadgeExporter
|
||||||
|
return BadgeExporter
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(order_info, dispatch_uid="badges_control_order_info")
|
||||||
|
def control_order_info(sender: Event, request, order: Order, **kwargs):
|
||||||
|
|
||||||
|
template = get_template('pretixplugins/badges/control_order_info.html')
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
'order': order,
|
||||||
|
'request': request,
|
||||||
|
'event': sender,
|
||||||
|
}
|
||||||
|
return template.render(ctx, request=request)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signal=logentry_display, dispatch_uid="badges_logentry_display")
|
||||||
|
def badges_logentry_display(sender, logentry, **kwargs):
|
||||||
|
if not logentry.action_type.startswith('pretix.plugins.badges'):
|
||||||
|
return
|
||||||
|
|
||||||
|
plains = {
|
||||||
|
'pretix.plugins.badges.layout.added': _('Badge layout created.'),
|
||||||
|
'pretix.plugins.badges.layout.deleted': _('Badge layout deleted.'),
|
||||||
|
'pretix.plugins.badges.layout.changed': _('Badge layout changed.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if logentry.action_type in plains:
|
||||||
|
return plains[logentry.action_type]
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signal=logentry_object_link, dispatch_uid="badges_logentry_object_link")
|
||||||
|
def badges_logentry_object_link(sender, logentry, **kwargs):
|
||||||
|
if not logentry.action_type.startswith('pretix.plugins.badges.layout') or not isinstance(logentry.content_object,
|
||||||
|
BadgeLayout):
|
||||||
|
return
|
||||||
|
|
||||||
|
a_text = _('Badge layout {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('plugins:badges:edit', kwargs={
|
||||||
|
'event': sender.slug,
|
||||||
|
'organizer': sender.organizer.slug,
|
||||||
|
'layout': logentry.content_object.id
|
||||||
|
}),
|
||||||
|
'val': escape(logentry.content_object.name),
|
||||||
|
}
|
||||||
|
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
|
||||||
|
return a_text.format_map(a_map)
|
||||||
Binary file not shown.
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="148mm"
|
||||||
|
height="105mm"
|
||||||
|
viewBox="0 0 148 105"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1236"
|
||||||
|
inkscape:version="0.92.2 2405546, 2018-03-11"
|
||||||
|
sodipodi:docname="badge_default_a6l.svg">
|
||||||
|
<defs
|
||||||
|
id="defs1230" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.35"
|
||||||
|
inkscape:cx="-357.14286"
|
||||||
|
inkscape:cy="560"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1023"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="36"
|
||||||
|
inkscape:window-maximized="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1233">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Ebene 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-192)" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
|
from pretix.base.models import (
|
||||||
|
CachedFile, Event, OrderPosition, cachedfile_name,
|
||||||
|
)
|
||||||
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
from .exporters import render_pdf
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task()
|
||||||
|
def badges_create_pdf(fileid: int, event: int, orders: List[int]) -> int:
|
||||||
|
file = CachedFile.objects.get(id=fileid)
|
||||||
|
event = Event.objects.get(id=event)
|
||||||
|
|
||||||
|
pdfcontent = render_pdf(event, OrderPosition.objects.filter(order_id__in=orders))
|
||||||
|
file.file.save(cachedfile_name(file, file.filename), ContentFile(pdfcontent.read()))
|
||||||
|
file.save()
|
||||||
|
return file.pk
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load eventurl %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
{% trans "Badges" %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form action="{% url "plugins:badges:print" event=event.slug organizer=event.organizer.slug %}?code={{ order.code }}"
|
||||||
|
method="post" data-asynctask data-asynctask-download data-asynctask-long class="form-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<span class="fa fa-print"></span>
|
||||||
|
{% trans "Print badges" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Badges" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Badges" %}</h1>
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{% blocktrans %}Are you sure you want to delete the badge layout <strong>{{ layout }}</strong>?{% endblocktrans %}</p>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<a href="{% url "plugins:badges:index" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn
|
||||||
|
btn-default btn-cancel">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-danger btn-save">
|
||||||
|
{% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}
|
||||||
|
{% if layout %}
|
||||||
|
{% blocktrans with name=layout.name %}Badge layout: {{ name }}{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Badge layout" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% if layout %}
|
||||||
|
<h1>{% blocktrans with name=layout.name %}Badge layout: {{ name }}{% endblocktrans %}</h1>
|
||||||
|
{% else %}
|
||||||
|
<h1>{% trans "Badge layout" %}</h1>
|
||||||
|
{% endif %}
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
{% bootstrap_field form.name layout="control" %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">
|
||||||
|
{% trans "Badge design" %}
|
||||||
|
</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can modify the design after you saved this page.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load money %}
|
||||||
|
{% block title %}{% trans "Badges" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Badges" %}</h1>
|
||||||
|
{% if layouts|length == 0 %}
|
||||||
|
<div class="empty-collection">
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You haven't created any badge layouts yet.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
|
<a href="{% url "plugins:badges:add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
|
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new badge layout" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
|
<a href="{% url "plugins:badges:add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new badge layout" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url "control:event.orders.export" organizer=request.event.organizer.slug event=request.event.slug %}?identifier=badges" class="btn btn-primary"><i class="fa fa-print"></i> {% trans "Print badges" %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-quotas">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Name" %}</th>
|
||||||
|
<th>{% trans "Default" %}</th>
|
||||||
|
<th class="action-col-2"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for l in layouts %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
|
<strong><a href="{% url "plugins:badges:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}">
|
||||||
|
{{ l.name }}
|
||||||
|
</a></strong>
|
||||||
|
{% else %}
|
||||||
|
<strong>{{ l.name }}</strong>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if l.default %}
|
||||||
|
<span class="text-success">
|
||||||
|
<span class="fa fa-check"></span>
|
||||||
|
{% trans "Default" %}
|
||||||
|
</span>
|
||||||
|
{% elif "can_change_event_settings" in request.eventpermset %}
|
||||||
|
<form class="form-inline" method="post"
|
||||||
|
action="{% url "plugins:badges:default" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="btn btn-default btn-sm">
|
||||||
|
{% trans "Make default" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% if "can_change_event_settings" in request.eventpermset %}
|
||||||
|
<a href="{% url "plugins:badges:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
|
<a href="{% url "plugins:badges:delete" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% include "pretixcontrol/pagination.html" %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .views import (
|
||||||
|
LayoutCreate, LayoutDelete, LayoutEditorView, LayoutListView,
|
||||||
|
LayoutSetDefault, OrderPrintDo,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/$',
|
||||||
|
LayoutListView.as_view(), name='index'),
|
||||||
|
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/print$',
|
||||||
|
OrderPrintDo.as_view(), name='print'),
|
||||||
|
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/add$',
|
||||||
|
LayoutCreate.as_view(), name='add'),
|
||||||
|
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/(?P<layout>\d+)/default$',
|
||||||
|
LayoutSetDefault.as_view(), name='default'),
|
||||||
|
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/(?P<layout>\d+)/delete$',
|
||||||
|
LayoutDelete.as_view(), name='delete'),
|
||||||
|
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/(?P<layout>\d+)/editor',
|
||||||
|
LayoutEditorView.as_view(), name='edit'),
|
||||||
|
]
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.core.files import File
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.db import transaction
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
|
from django.views.generic import CreateView, DeleteView, DetailView, ListView
|
||||||
|
from reportlab.lib import pagesizes
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
|
||||||
|
from pretix.base.models import CachedFile, OrderPosition
|
||||||
|
from pretix.base.pdf import Renderer
|
||||||
|
from pretix.base.views.async import AsyncAction
|
||||||
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
|
from pretix.control.views.pdf import BaseEditorView
|
||||||
|
from pretix.plugins.badges.forms import BadgeLayoutForm
|
||||||
|
from pretix.plugins.badges.tasks import badges_create_pdf
|
||||||
|
|
||||||
|
from .models import BadgeLayout
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutListView(EventPermissionRequiredMixin, ListView):
|
||||||
|
model = BadgeLayout
|
||||||
|
permission = ('can_change_event_settings', 'can_view_orders')
|
||||||
|
template_name = 'pretixplugins/badges/index.html'
|
||||||
|
context_object_name = 'layouts'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.request.event.badge_layouts.prefetch_related('item_assignments')
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutCreate(EventPermissionRequiredMixin, CreateView):
|
||||||
|
model = BadgeLayout
|
||||||
|
form_class = BadgeLayoutForm
|
||||||
|
template_name = 'pretixplugins/badges/edit.html'
|
||||||
|
permission = 'can_change_event_settings'
|
||||||
|
context_object_name = 'layout'
|
||||||
|
success_url = '/ignored'
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.event = self.request.event
|
||||||
|
if not self.request.event.badge_layouts.filter(default=True).exists():
|
||||||
|
form.instance.default = True
|
||||||
|
messages.success(self.request, _('The new badge layout has been created.'))
|
||||||
|
super().form_valid(form)
|
||||||
|
form.instance.log_action('pretix.plugins.badges.layout.added', user=self.request.user,
|
||||||
|
data=dict(form.cleaned_data))
|
||||||
|
return redirect(reverse('plugins:badges:edit', kwargs={
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
'layout': form.instance.pk
|
||||||
|
}))
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||||
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutSetDefault(EventPermissionRequiredMixin, DetailView):
|
||||||
|
model = BadgeLayout
|
||||||
|
permission = 'can_change_event_settings'
|
||||||
|
|
||||||
|
def get_object(self, queryset=None) -> BadgeLayout:
|
||||||
|
try:
|
||||||
|
return self.request.event.badge_layouts.get(
|
||||||
|
id=self.kwargs['layout']
|
||||||
|
)
|
||||||
|
except BadgeLayout.DoesNotExist:
|
||||||
|
raise Http404(_("The requested badge layout does not exist."))
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
|
obj = self.get_object()
|
||||||
|
self.request.event.badge_layouts.exclude(pk=obj.pk).update(default=False)
|
||||||
|
obj.default = True
|
||||||
|
obj.save(update_fields=['default'])
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
return reverse('plugins:badges:index', kwargs={
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutDelete(EventPermissionRequiredMixin, DeleteView):
|
||||||
|
model = BadgeLayout
|
||||||
|
template_name = 'pretixplugins/badges/delete.html'
|
||||||
|
permission = 'can_change_event_settings'
|
||||||
|
context_object_name = 'layout'
|
||||||
|
|
||||||
|
def get_object(self, queryset=None) -> BadgeLayout:
|
||||||
|
try:
|
||||||
|
return self.request.event.badge_layouts.get(
|
||||||
|
id=self.kwargs['layout']
|
||||||
|
)
|
||||||
|
except BadgeLayout.DoesNotExist:
|
||||||
|
raise Http404(_("The requested badge layout does not exist."))
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
self.object.log_action(action='pretix.plugins.badges.layout.deleted', user=request.user)
|
||||||
|
self.object.delete()
|
||||||
|
if not self.request.event.badge_layouts.filter(default=True).exists():
|
||||||
|
f = self.request.event.badge_layouts.first()
|
||||||
|
if f:
|
||||||
|
f.default = True
|
||||||
|
f.save(update_fields=['default'])
|
||||||
|
messages.success(self.request, _('The selected badge layout been deleted.'))
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
return reverse('plugins:badges:index', kwargs={
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutEditorView(BaseEditorView):
|
||||||
|
@cached_property
|
||||||
|
def layout(self):
|
||||||
|
try:
|
||||||
|
return self.request.event.badge_layouts.get(
|
||||||
|
id=self.kwargs['layout']
|
||||||
|
)
|
||||||
|
except BadgeLayout.DoesNotExist:
|
||||||
|
raise Http404(_("The requested badge layout does not exist."))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self):
|
||||||
|
return _('Badge layout: {}').format(self.layout)
|
||||||
|
|
||||||
|
def save_layout(self):
|
||||||
|
self.layout.layout = self.request.POST.get("data")
|
||||||
|
self.layout.save(update_fields=['layout'])
|
||||||
|
self.layout.log_action(action='pretix.plugins.badges.layout.changed', user=self.request.user,
|
||||||
|
data={'layout': self.request.POST.get("data")})
|
||||||
|
|
||||||
|
def get_default_background(self):
|
||||||
|
return static('pretixplugins/badges/badge_default_a6l.pdf')
|
||||||
|
|
||||||
|
def generate(self, op: OrderPosition, override_layout=None, override_background=None):
|
||||||
|
Renderer._register_fonts()
|
||||||
|
|
||||||
|
buffer = BytesIO()
|
||||||
|
if override_background:
|
||||||
|
bgf = default_storage.open(override_background.name, "rb")
|
||||||
|
elif isinstance(self.layout.background, File) and self.layout.background.name:
|
||||||
|
bgf = default_storage.open(self.layout.background.name, "rb")
|
||||||
|
else:
|
||||||
|
bgf = open(finders.find('pretixplugins/badges/badge_default_a6l.pdf'), "rb")
|
||||||
|
r = Renderer(
|
||||||
|
self.request.event,
|
||||||
|
override_layout or self.get_current_layout(),
|
||||||
|
bgf,
|
||||||
|
)
|
||||||
|
p = canvas.Canvas(buffer, pagesize=pagesizes.A4)
|
||||||
|
r.draw_page(p, op.order, op)
|
||||||
|
p.save()
|
||||||
|
outbuffer = r.render_background(buffer, 'Badge')
|
||||||
|
return 'badge.pdf', 'application/pdf', outbuffer.read()
|
||||||
|
|
||||||
|
def get_current_layout(self):
|
||||||
|
return json.loads(self.layout.layout)
|
||||||
|
|
||||||
|
def get_current_background(self):
|
||||||
|
return self.layout.background.url if self.layout.background else self.get_default_background()
|
||||||
|
|
||||||
|
def save_background(self, f: CachedFile):
|
||||||
|
if self.layout.background:
|
||||||
|
self.layout.background.delete()
|
||||||
|
self.layout.background.save('background.pdf', f.file)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderPrintDo(EventPermissionRequiredMixin, AsyncAction, View):
|
||||||
|
task = badges_create_pdf
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_success_message(self, value):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_success_url(self, value):
|
||||||
|
return reverse('cachedfile.download', kwargs={'id': str(value)})
|
||||||
|
|
||||||
|
def get_error_url(self):
|
||||||
|
return reverse('control:event.index', kwargs={
|
||||||
|
'organizer': self.request.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_error_message(self, exception):
|
||||||
|
if isinstance(exception, str):
|
||||||
|
return exception
|
||||||
|
return super().get_error_message(exception)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
order = get_object_or_404(self.request.event.orders, code=request.GET.get("code"))
|
||||||
|
cf = CachedFile()
|
||||||
|
cf.date = now()
|
||||||
|
cf.type = 'application/pdf'
|
||||||
|
cf.expires = now() + timedelta(days=3)
|
||||||
|
cf.save()
|
||||||
|
return self.do(
|
||||||
|
str(cf.id),
|
||||||
|
self.request.event.pk,
|
||||||
|
[order.pk],
|
||||||
|
)
|
||||||
@@ -1,34 +1,18 @@
|
|||||||
import copy
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import uuid
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import bleach
|
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
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.pdfgen.canvas import Canvas
|
||||||
from reportlab.platypus import Paragraph
|
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Order, OrderPosition
|
from pretix.base.models import Order, OrderPosition
|
||||||
from pretix.base.pdf import get_variables
|
from pretix.base.pdf import Renderer
|
||||||
from pretix.base.ticketoutput import BaseTicketOutput
|
from pretix.base.ticketoutput import BaseTicketOutput
|
||||||
from pretix.plugins.ticketoutputpdf.signals import get_fonts
|
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
|
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
|
||||||
|
|
||||||
@@ -41,92 +25,14 @@ class PdfTicketOutput(BaseTicketOutput):
|
|||||||
def __init__(self, event, override_layout=None, override_background=None):
|
def __init__(self, event, override_layout=None, override_background=None):
|
||||||
self.override_layout = override_layout
|
self.override_layout = override_layout
|
||||||
self.override_background = override_background
|
self.override_background = override_background
|
||||||
self.variables = get_variables(event)
|
|
||||||
super().__init__(event)
|
super().__init__(event)
|
||||||
|
|
||||||
def _register_fonts(self):
|
def _register_fonts(self):
|
||||||
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
|
Renderer._register_fonts()
|
||||||
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')))
|
|
||||||
|
|
||||||
for family, styles in get_fonts().items():
|
|
||||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
|
||||||
if 'italic' in styles:
|
|
||||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
|
||||||
if 'bold' in styles:
|
|
||||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
|
||||||
if 'bolditalic' in styles:
|
|
||||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
|
|
||||||
ev = op.subevent or order.event
|
|
||||||
if not o['content']:
|
|
||||||
return '(error)'
|
|
||||||
if o['content'] == 'other':
|
|
||||||
return o['text'].replace("\n", "<br/>\n")
|
|
||||||
elif o['content'].startswith('meta:'):
|
|
||||||
return ev.meta_data.get(o['content'][5:]) or ''
|
|
||||||
elif o['content'] in self.variables:
|
|
||||||
try:
|
|
||||||
return self.variables[o['content']]['evaluate'](op, order, ev)
|
|
||||||
except:
|
|
||||||
logger.exception('Failed to process variable.')
|
|
||||||
return '(error)'
|
|
||||||
return ''
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
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']]
|
|
||||||
)
|
|
||||||
text = re.sub(
|
|
||||||
"<br[^>]*>", "<br/>",
|
|
||||||
bleach.clean(
|
|
||||||
self._get_text_content(op, order, o) or "",
|
|
||||||
tags=["br"], attributes={}, styles=[], strip=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
p = Paragraph(text, 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])
|
|
||||||
|
|
||||||
def _draw_page(self, canvas: Canvas, op: OrderPosition, order: Order):
|
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()
|
objs = self.override_layout or self.settings.get('layout', as_type=list) or self._legacy_layout()
|
||||||
for o in objs:
|
Renderer(self.event, objs, None).draw_page(canvas, order, op)
|
||||||
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):
|
def generate_order(self, order: Order):
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
@@ -166,10 +72,6 @@ class PdfTicketOutput(BaseTicketOutput):
|
|||||||
return open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
|
return open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
|
||||||
|
|
||||||
def _render_with_background(self, buffer, title=_('Ticket')):
|
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)
|
bg_file = self.settings.get('background', as_type=File)
|
||||||
if self.override_background:
|
if self.override_background:
|
||||||
bgf = default_storage.open(self.override_background.name, "rb")
|
bgf = default_storage.open(self.override_background.name, "rb")
|
||||||
@@ -177,21 +79,7 @@ class PdfTicketOutput(BaseTicketOutput):
|
|||||||
bgf = default_storage.open(bg_file.name, "rb")
|
bgf = default_storage.open(bg_file.name, "rb")
|
||||||
else:
|
else:
|
||||||
bgf = self._get_default_background()
|
bgf = self._get_default_background()
|
||||||
bg_pdf = PdfFileReader(BytesIO(bgf.read()))
|
return Renderer(self.event, None, bgf).render_background(buffer, title)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def settings_content_render(self, request: HttpRequest) -> str:
|
def settings_content_render(self, request: HttpRequest) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ INSTALLED_APPS = [
|
|||||||
'pretix.plugins.reports',
|
'pretix.plugins.reports',
|
||||||
'pretix.plugins.checkinlists',
|
'pretix.plugins.checkinlists',
|
||||||
'pretix.plugins.pretixdroid',
|
'pretix.plugins.pretixdroid',
|
||||||
|
'pretix.plugins.badges',
|
||||||
'django_markup',
|
'django_markup',
|
||||||
'django_otp',
|
'django_otp',
|
||||||
'django_otp.plugins.otp_totp',
|
'django_otp.plugins.otp_totp',
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ def test_specific_event_permission_limited(event, user):
|
|||||||
assert user.has_event_permission(event.organizer, event, 'can_change_orders')
|
assert user.has_event_permission(event.organizer, event, 'can_change_orders')
|
||||||
assert not user.has_event_permission(event.organizer, event, 'can_change_event_settings')
|
assert not user.has_event_permission(event.organizer, event, 'can_change_event_settings')
|
||||||
|
|
||||||
|
assert user.has_event_permission(event.organizer, event, ('can_change_orders', 'can_change_event_settings'))
|
||||||
|
assert not user.has_event_permission(event.organizer, event, ('can_change_teams', 'can_change_event_settings'))
|
||||||
|
|
||||||
team.can_change_orders = False
|
team.can_change_orders = False
|
||||||
team.save()
|
team.save()
|
||||||
user._teamcache = {}
|
user._teamcache = {}
|
||||||
@@ -187,6 +190,7 @@ def test_specific_organizer_permission(event, user):
|
|||||||
team.members.add(user)
|
team.members.add(user)
|
||||||
user._teamcache = {}
|
user._teamcache = {}
|
||||||
assert user.has_organizer_permission(event.organizer, 'can_create_events')
|
assert user.has_organizer_permission(event.organizer, 'can_create_events')
|
||||||
|
assert user.has_organizer_permission(event.organizer, ('can_create_events', 'can_change_organizer_settings'))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from tests.base import SoupTest, extract_form_fields
|
||||||
|
|
||||||
|
from pretix.base.models import Event, Item, Organizer, Team, User
|
||||||
|
from pretix.plugins.badges.models import BadgeItem
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeLayoutFormTest(SoupTest):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
|
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
|
||||||
|
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
|
||||||
|
self.event1 = Event.objects.create(
|
||||||
|
organizer=self.orga1, name='30C3', slug='30c3',
|
||||||
|
plugins='pretix.plugins.badges',
|
||||||
|
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
self.item1 = Item.objects.create(event=self.event1, name="Standard", default_price=0, position=1)
|
||||||
|
t = Team.objects.create(organizer=self.orga1, can_change_event_settings=True, can_view_orders=True,
|
||||||
|
can_change_items=True, all_events=True, can_create_events=True)
|
||||||
|
t.members.add(self.user)
|
||||||
|
t.limit_events.add(self.event1)
|
||||||
|
self.client.login(email='dummy@dummy.dummy', password='dummy')
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
doc = self.get_doc('/control/event/%s/%s/badges/add' % (self.orga1.slug, self.event1.slug))
|
||||||
|
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
|
||||||
|
form_data['name'] = 'Layout 1'
|
||||||
|
doc = self.post_doc('/control/event/%s/%s/badges/add' % (self.orga1.slug, self.event1.slug), form_data)
|
||||||
|
assert doc.select(".alert-success")
|
||||||
|
self.assertIn("Layout 1", doc.select("#page-wrapper")[0].text)
|
||||||
|
assert self.event1.badge_layouts.get(
|
||||||
|
default=True, name='Layout 1'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_default(self):
|
||||||
|
bl1 = self.event1.badge_layouts.create(name="Layout 1", default=True)
|
||||||
|
bl2 = self.event1.badge_layouts.create(name="Layout 2")
|
||||||
|
self.post_doc('/control/event/%s/%s/badges/%s/default' % (self.orga1.slug, self.event1.slug, bl2.id), {})
|
||||||
|
bl1.refresh_from_db()
|
||||||
|
assert not bl1.default
|
||||||
|
bl2.refresh_from_db()
|
||||||
|
assert bl2.default
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
bl1 = self.event1.badge_layouts.create(name="Layout 1", default=True)
|
||||||
|
bl2 = self.event1.badge_layouts.create(name="Layout 2")
|
||||||
|
doc = self.get_doc('/control/event/%s/%s/badges/%s/delete' % (self.orga1.slug, self.event1.slug, bl1.id))
|
||||||
|
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
|
||||||
|
doc = self.post_doc('/control/event/%s/%s/badges/%s/delete' % (self.orga1.slug, self.event1.slug, bl1.id),
|
||||||
|
form_data)
|
||||||
|
assert doc.select(".alert-success")
|
||||||
|
self.assertNotIn("Layout 1", doc.select("#page-wrapper")[0].text)
|
||||||
|
assert self.event1.badge_layouts.count() == 1
|
||||||
|
bl2.refresh_from_db()
|
||||||
|
assert bl2.default
|
||||||
|
|
||||||
|
def test_set_on_item(self):
|
||||||
|
self.event1.badge_layouts.create(name="Layout 1", default=True)
|
||||||
|
bl2 = self.event1.badge_layouts.create(name="Layout 2")
|
||||||
|
self.client.post('/control/event/%s/%s/items/%d/' % (self.orga1.slug, self.event1.slug, self.item1.id), {
|
||||||
|
'name_0': 'Standard',
|
||||||
|
'default_price': '23.00',
|
||||||
|
'tax_rate': '19.00',
|
||||||
|
'active': 'yes',
|
||||||
|
'allow_cancel': 'yes',
|
||||||
|
'badgeitem-layout': bl2.pk
|
||||||
|
})
|
||||||
|
assert BadgeItem.objects.get(item=self.item1, layout=bl2)
|
||||||
|
self.client.post('/control/event/%s/%s/items/%d/' % (self.orga1.slug, self.event1.slug, self.item1.id), {
|
||||||
|
'name_0': 'Standard',
|
||||||
|
'default_price': '23.00',
|
||||||
|
'tax_rate': '19.00',
|
||||||
|
'active': 'yes',
|
||||||
|
'allow_cancel': 'yes',
|
||||||
|
})
|
||||||
|
assert not BadgeItem.objects.filter(item=self.item1, layout=bl2).exists()
|
||||||
|
|
||||||
|
def test_item_copy(self):
|
||||||
|
bl2 = self.event1.badge_layouts.create(name="Layout 2")
|
||||||
|
BadgeItem.objects.create(item=self.item1, layout=bl2)
|
||||||
|
self.client.post('/control/event/%s/%s/items/add' % (self.orga1.slug, self.event1.slug), {
|
||||||
|
'name_0': 'Intermediate',
|
||||||
|
'default_price': '23.00',
|
||||||
|
'tax_rate': '19.00',
|
||||||
|
'copy_from': str(self.item1.pk),
|
||||||
|
'has_variations': '1'
|
||||||
|
})
|
||||||
|
i_new = Item.objects.get(name__icontains='Intermediate')
|
||||||
|
assert BadgeItem.objects.get(item=i_new, layout=bl2)
|
||||||
|
assert BadgeItem.objects.get(item=self.item1, layout=bl2)
|
||||||
|
|
||||||
|
def test_copy_event(self):
|
||||||
|
bl2 = self.event1.badge_layouts.create(name="Layout 2")
|
||||||
|
BadgeItem.objects.create(item=self.item1, layout=bl2)
|
||||||
|
self.post_doc('/control/events/add', {
|
||||||
|
'event_wizard-current_step': 'foundation',
|
||||||
|
'foundation-organizer': self.orga1.pk,
|
||||||
|
'foundation-locales': ('en',)
|
||||||
|
})
|
||||||
|
self.post_doc('/control/events/add', {
|
||||||
|
'event_wizard-current_step': 'basics',
|
||||||
|
'basics-name_0': '33C3',
|
||||||
|
'basics-slug': '33c3',
|
||||||
|
'basics-date_from_0': '2016-12-27',
|
||||||
|
'basics-date_from_1': '10:00:00',
|
||||||
|
'basics-date_to_0': '2016-12-30',
|
||||||
|
'basics-date_to_1': '19:00:00',
|
||||||
|
'basics-location_0': 'Hamburg',
|
||||||
|
'basics-currency': 'EUR',
|
||||||
|
'basics-tax_rate': '19.00',
|
||||||
|
'basics-locale': 'en',
|
||||||
|
'basics-timezone': 'Europe/Berlin',
|
||||||
|
})
|
||||||
|
self.post_doc('/control/events/add', {
|
||||||
|
'event_wizard-current_step': 'copy',
|
||||||
|
'copy-copy_from_event': self.event1.pk
|
||||||
|
})
|
||||||
|
|
||||||
|
ev = Event.objects.get(slug='33c3')
|
||||||
|
i_new = ev.items.first()
|
||||||
|
bl_new = ev.badge_layouts.first()
|
||||||
|
assert BadgeItem.objects.get(item=i_new, layout=bl_new)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from PyPDF2 import PdfFileReader
|
||||||
|
|
||||||
|
from pretix.base.models import (
|
||||||
|
Event, Item, ItemVariation, Order, OrderPosition, Organizer,
|
||||||
|
)
|
||||||
|
from pretix.plugins.badges.exporters import BadgeExporter
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def env():
|
||||||
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||||
|
event = Event.objects.create(
|
||||||
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
|
date_from=now(), live=True
|
||||||
|
)
|
||||||
|
o1 = Order.objects.create(
|
||||||
|
code='FOOBAR', event=event, email='dummy@dummy.test',
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
datetime=now(), expires=now() + timedelta(days=10),
|
||||||
|
total=Decimal('13.37'), payment_provider='banktransfer'
|
||||||
|
)
|
||||||
|
shirt = Item.objects.create(event=event, name='T-Shirt', default_price=12)
|
||||||
|
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
|
||||||
|
OrderPosition.objects.create(
|
||||||
|
order=o1, item=shirt, variation=shirt_red,
|
||||||
|
price=12, attendee_name=None, secret='1234'
|
||||||
|
)
|
||||||
|
OrderPosition.objects.create(
|
||||||
|
order=o1, item=shirt, variation=shirt_red,
|
||||||
|
price=12, attendee_name=None, secret='5678'
|
||||||
|
)
|
||||||
|
return event, o1, shirt
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_generate_pdf(env):
|
||||||
|
event, order, shirt = env
|
||||||
|
event.badge_layouts.create(name="Default", default=True)
|
||||||
|
e = BadgeExporter(event)
|
||||||
|
fname, ftype, buf = e.render({
|
||||||
|
'items': [shirt.pk],
|
||||||
|
'include_pending': False
|
||||||
|
})
|
||||||
|
assert ftype == 'application/pdf'
|
||||||
|
pdf = PdfFileReader(BytesIO(buf))
|
||||||
|
assert pdf.numPages == 0
|
||||||
|
|
||||||
|
fname, ftype, buf = e.render({
|
||||||
|
'items': [],
|
||||||
|
'include_pending': True
|
||||||
|
})
|
||||||
|
assert ftype == 'application/pdf'
|
||||||
|
pdf = PdfFileReader(BytesIO(buf))
|
||||||
|
assert pdf.numPages == 0
|
||||||
|
|
||||||
|
fname, ftype, buf = e.render({
|
||||||
|
'items': [shirt.pk],
|
||||||
|
'include_pending': True
|
||||||
|
})
|
||||||
|
assert ftype == 'application/pdf'
|
||||||
|
pdf = PdfFileReader(BytesIO(buf))
|
||||||
|
assert pdf.numPages == 2
|
||||||
Reference in New Issue
Block a user