Compare commits

..

13 Commits

Author SHA1 Message Date
Raphael Michel
a021ac6ecc Bump release 2017-08-21 14:53:46 +02:00
Raphael Michel
e6eb4945a7 [SECURITY] Support custom media URLs in CSP middleware 2017-08-21 14:53:37 +02:00
Raphael Michel
44b7b15b3e [SECURITY] Do not allow SVG files for logos 2017-08-21 14:53:21 +02:00
Raphael Michel
022ca8ad6c [SECURITY] Fix XSS injection vulnerabilities in question answers, event, quota and product names 2017-08-21 14:53:19 +02:00
Raphael Michel
5e37fbb4ef [SECURITY] Update to morris.js master to fix a XSS vulnerability 2017-08-21 14:52:41 +02:00
Raphael Michel
5049da95b5 [SECURITY] Use defusedcsv for exports 2017-08-21 14:52:31 +02:00
Raphael Michel
c78ca8bc00 [SECURITY] Rewrite all links in rich texts 2017-08-21 14:51:21 +02:00
Raphael Michel
c40e0bfdff [SECURITY] Fix XSS vulnerability in typeahead.js 2017-08-21 14:51:21 +02:00
Raphael Michel
ea79ebf105 [SECURITY] Fix XSS vulnerability in Lightbox caption 2017-08-21 14:51:00 +02:00
Raphael Michel
d4b9906638 Fix syntax error in setup.py 2017-07-02 18:57:10 +02:00
Raphael Michel
c1c4133ac6 Bump to 1.5.1 2017-07-02 18:45:00 +02:00
Raphael Michel
ff3e127648 Fix missing dependencies in setup.py 2017-07-02 18:44:39 +02:00
Raphael Michel
970b861947 Fix import in unit test 2017-07-02 17:36:38 +02:00
267 changed files with 5404 additions and 16888 deletions

View File

@@ -10,10 +10,9 @@ at the following locations. It will try to read the file from the specified path
the following order. The file that is found *last* will override the settings from
the files found before.
1. ``PREFIX_CONFIG_FILE`` environment variable
2. ``/etc/pretix/pretix.cfg``
3. ``~/.pretix.cfg``
4. ``pretix.cfg`` in the current working directory
1. ``/etc/pretix/pretix.cfg``
2. ``~/.pretix.cfg``
3. ``pretix.cfg`` in the current working directory
The file is expected to be in the INI format as specified in the `Python documentation`_.

View File

@@ -24,8 +24,6 @@ is_public boolean If ``true``, th
presale_start datetime The date at which the ticket shop opens (or ``null``)
presale_end datetime The date at which the ticket shop closes (or ``null``)
location multi-lingual string The event location (or ``null``)
has_subevents boolean ``True`` if the event series feature is active for this
event
===================================== ========================== =======================================================
@@ -69,7 +67,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"has_subevents": false
}
]
}
@@ -112,7 +109,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"has_subevents": false
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -6,7 +6,6 @@ Resources and endpoints
organizers
events
subevents
categories
items
questions

View File

@@ -11,7 +11,7 @@ The invoice resource contains the following public fields:
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
number string Invoice number (with prefix)
invoice_no string Invoice number (without prefix)
order string Order code of the order this invoice belongs to
is_cancellation boolean ``True``, if this invoice is the cancellation of a
different invoice.
@@ -35,13 +35,6 @@ lines list of objects The actual invo
===================================== ========================== =======================================================
.. versionchanged:: 1.6
The attribute ``invoice_no`` has been dropped in favor of ``number`` which includes the number including the prefix,
since the prefix can now vary. Also, invoices now need to be identified by their ``number`` instead of the raw
number.
Endpoints
---------
@@ -71,7 +64,7 @@ Endpoints
"previous": null,
"results": [
{
"number": "SAMPLECONF-00001",
"invoice_no": "00001",
"order": "ABC12",
"is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
@@ -102,14 +95,14 @@ Endpoints
:query string refers: If set, only invoices refering to the given invoice will be returned.
:query string locale: If set, only invoices with the given locale will be returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
``nr`` (equals to ``number``). Default: ``nr``
``invoice_no``. Default: ``invoice_no``
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/
Returns information on one invoice, identified by its invoice number.
@@ -117,7 +110,7 @@ Endpoints
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/SAMPLECONF-00001/ HTTP/1.1
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
@@ -130,7 +123,7 @@ Endpoints
Content-Type: text/javascript
{
"number": "SAMPLECONF-00001",
"invoice_no": "00001",
"order": "ABC12",
"is_cancellation": false,
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",

View File

@@ -50,13 +50,6 @@ downloads list of objects List of ticket
└ url string Download URL
===================================== ========================== =======================================================
.. versionchanged:: 1.6
The ``invoice_address.country`` attribute contains a two-letter country code for all new orders. For old orders,
a custom text might still be returned.
Order position resource
-----------------------
@@ -78,7 +71,6 @@ tax_rate decimal (string) VAT rate applie
tax_value money (string) VAT included in this position
secret string Secret code printed on the tickets for validation
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
checkins list of objects List of check-ins with this ticket
└ datetime datetime Time of check-in
downloads list of objects List of ticket download options
@@ -159,7 +151,6 @@ Order endpoints
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"
@@ -263,7 +254,6 @@ Order endpoints
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"
@@ -382,7 +372,6 @@ Order position endpoints
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"
@@ -418,7 +407,6 @@ Order position endpoints
:query string order__status: Only return positions with the given order status.
:query bollean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
checked in already.
:query integer subevent: Only return positions of the sub-event with the given ID
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
@@ -460,7 +448,6 @@ Order position endpoints
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
"checkins": [
{
"datetime": "2017-12-25T12:45:23Z"

View File

@@ -22,7 +22,6 @@ type string The expected ty
* ``B`` boolean
* ``C`` choice from a list
* ``M`` multiple choice from a list
* ``F`` file upload
required boolean If ``True``, the question needs to be filled out.
position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to.

View File

@@ -17,7 +17,6 @@ name string The internal na
size integer The size of the quota or ``null`` for unlimited
items list of integers List of item IDs this quota acts on.
variations list of integers List of item variation IDs this quota acts on.
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
===================================== ========================== =======================================================
@@ -54,8 +53,7 @@ Endpoints
"name": "Ticket Quota",
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
"variations": [1, 4, 5, 7]
}
]
}
@@ -63,7 +61,6 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
Default: ``position``
:query integer subevent: Only return quotas of the sub-event with the given ID
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
@@ -95,8 +92,7 @@ Endpoints
"name": "Ticket Quota",
"size": 200,
"items": [1, 2],
"variations": [1, 4, 5, 7],
"subevent": null
"variations": [1, 4, 5, 7]
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -1,137 +0,0 @@
Event series dates / Sub-events
===============================
Resource description
--------------------
Events can represent whole event series if the ``has_subevents`` property of the event is active.
In this case, many other resources are additionally connected to an event date (also called sub-event).
The sub-event resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the sub-event
name multi-lingual string The sub-event's full name
active boolean If ``true``, the sub-event ticket shop is publicly
available.
date_from datetime The sub-event's start date
date_to datetime The sub-event's end date (or ``null``)
date_admission datetime The sub-event's admission date (or ``null``)
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
location multi-lingual string The sub-event location (or ``null``)
item_price_overrides list of objects List of items for which this sub-event overrides the
default price
├ item integer The internal item ID
└ price money (string) The price or ``null`` for the default price
variation_price_overrides list of objects List of variations for which this sub-event overrides
the default price
├ variation integer The internal variation ID
└ price money (string) The price or ``null`` for the default price
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
Returns a list of all sub-events of an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": {"en": "First Sample Conference"},
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"item_price_overrides": [
{
"item": 2,
"price": "12.00"
}
],
"variation_price_overrides": []
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
Returns information on one sub-event, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"name": {"en": "First Sample Conference"},
"active": false,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
"presale_start": null,
"presale_end": null,
"location": null,
"item_price_overrides": [
{
"item": 2,
"price": "12.00"
}
],
"variation_price_overrides": []
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``slug`` field of the sub-event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.

View File

@@ -40,7 +40,6 @@ quota integer An ID of a quot
for all items without restriction.
tag string A string that is used for grouping vouchers
comment string An internal comment on the voucher
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
===================================== ========================== =======================================================
@@ -86,8 +85,7 @@ Endpoints
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"comment": ""
}
]
}
@@ -109,7 +107,6 @@ Endpoints
:query integer variation: If set, only vouchers attached to the variation with the given ID will be shown.
:query integer quota: If set, only vouchers attached to the quota with the given ID will be shown.
:query string tag: If set, only vouchers with the given tag will be shown.
:query integer subevent: Only return vouchers of the sub-event with the given ID
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``code``,
``max_usages``, ``valid_until``, and ``value``. Default: ``id``
:param organizer: The ``slug`` field of the organizer to fetch
@@ -152,8 +149,7 @@ Endpoints
"variation": null,
"quota": null,
"tag": "testvoucher",
"comment": "",
"subevent": null
"comment": ""
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -23,7 +23,6 @@ item integer An ID of an ite
variation integer An ID of a variation the user is waiting to be
available again (or ``null``)
locale string Locale of the waiting user
subevent integer ID of the date inside an event series this entry belongs to (or ``null``).
===================================== ========================== =======================================================
@@ -62,8 +61,7 @@ Endpoints
"voucher": null,
"item": 2,
"variation": null,
"locale": "en",
"subevent": null
"locale": "en"
}
]
}
@@ -75,7 +73,6 @@ Endpoints
have not been sent a voucher.
:query integer item: If set, only entries of users waiting for the item with the given ID will be shown.
:query integer variation: If set, only entries of users waiting for the variation with the given ID will be shown.
:query integer subevent: Only return entries of the sub-event with the given ID
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``created``,
``email``, ``item``. Default: ``created``
:param organizer: The ``slug`` field of the organizer to fetch
@@ -111,8 +108,7 @@ Endpoints
"voucher": null,
"item": 2,
"variation": null,
"locale": "en",
"subevent": null
"locale": "en"
}
:param organizer: The ``slug`` field of the organizer to fetch

View File

@@ -25,7 +25,7 @@ Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, question_form_fields, checkout_confirm_messages
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, checkout_confirm_messages
.. automodule:: pretix.presale.signals

View File

@@ -10,6 +10,5 @@ Contents:
exporter
ticketoutput
payment
invoice
customview
general

View File

@@ -1,95 +0,0 @@
.. highlight:: python
:linenothreshold: 5
Writing an invoice renderer plugin
==================================
An invoice renderer controls how invoice files are built.
The creation of such a plugin is very similar to creating an export output.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
Output registration
-------------------
The invoice renderer API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available ticket outputs. Your plugin
should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer``
that we'll provide in this plugin::
from django.dispatch import receiver
from pretix.base.signals import register_invoice_renderers
@receiver(register_invoice_renderers, dispatch_uid="output_custom")
def register_infoice_renderers(sender, **kwargs):
from .invoice import MyInvoiceRenderer
return MyInvoiceRenderer
The renderer class
------------------
.. class:: pretix.base.invoice.BaseInvoiceRenderer
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
.. py:attribute:: BaseInvoiceRenderer.event
The default constructor sets this property to the event we are currently
working for.
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: verbose_name
This is an abstract attribute, you **must** override this!
.. automethod:: generate
Helper class for reportlab-base renderers
-----------------------------------------
All PDF rendering that ships with pretix is based on reportlab. We recommend to read the
`reportlab User Guide`_ to understand all the concepts used here.
If you want to implement a renderer that also uses report lab, this helper class might be
convenient to you:
.. class:: pretix.base.invoice.BaseReportlabInvoiceRenderer
.. py:attribute:: BaseReportlabInvoiceRenderer.pagesize
.. py:attribute:: BaseReportlabInvoiceRenderer.left_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.right_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.top_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.bottom_margin
.. py:attribute:: BaseReportlabInvoiceRenderer.doc_template_class
.. py:attribute:: BaseReportlabInvoiceRenderer.invoice
.. automethod:: _init
.. automethod:: _get_stylesheet
.. automethod:: _register_fonts
.. automethod:: _on_first_page
.. automethod:: _on_other_page
.. automethod:: _get_first_page_frames
.. automethod:: _get_other_page_frames
.. automethod:: _build_doc
.. _reportlab User Guide: https://www.reportlab.com/docs/reportlab-userguide.pdf

View File

@@ -58,8 +58,6 @@ The output class
.. autoattribute:: is_enabled
.. autoattribute:: multi_download_enabled
.. autoattribute:: settings_form_fields
.. automethod:: settings_content_render

View File

@@ -2,9 +2,6 @@ Sending Email
=============
pretix allows event organizers to configure how they want to send emails to their users in multiple ways.
Therefore, all emails should be sent through the following function.
If the email you send is related to an order, you should also take a look at the
:py:meth:`~pretix.base.models.Order.send_mail` of the order model.
Therefore, all emails should be sent through the following function:
.. autofunction:: pretix.base.services.mail.mail

View File

@@ -21,10 +21,7 @@ Organizers and events
:members:
.. autoclass:: pretix.base.models.Event
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, get_cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
.. autoclass:: pretix.base.models.SubEvent
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running
:members:
.. autoclass:: pretix.base.models.Team
:members:
@@ -45,15 +42,6 @@ Items
.. autoclass:: pretix.base.models.ItemVariation
:members:
.. autoclass:: pretix.base.models.SubEventItem
:members:
.. autoclass:: pretix.base.models.SubEventItemVariation
:members:
.. autoclass:: pretix.base.models.ItemAddOn
:members:
.. autoclass:: pretix.base.models.Question
:members:

View File

@@ -19,12 +19,10 @@ same team. We update them regularly to make them compatible with the latest
pretix releases:
* `SEPA direct debit`_
* `Wirecard payment`_
* `Pages`_
* `Passbook/Wallet ticket output`_
* `Cartshare`_
* `Fontpack Free fonts`_
* `Mailing list subscription`_
The following closed-source plugins are available to customers of the hosted pretix.eu platform.
Please get in touch with the pretix team if you want to have them for your self-hosted
@@ -36,12 +34,10 @@ pretix installation:
* Integration with MailChimp
The following plugins are from independent third-party authors, so we can make
no statements about their functionality, security, stability or compatibility:
no statements about their stability or compatibility:
* `esPass ticket output`_
* `IcePay integration`_
* `Average price chart`_
* `Pay in cash upon arrival`_
.. _SEPA direct debit: https://github.com/pretix/pretix-sepadebit
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
@@ -50,7 +46,3 @@ no statements about their functionality, security, stability or compatibility:
.. _esPass ticket output: https://github.com/esPass/pretix-espass
.. _IcePay integration: https://github.com/chotee/pretix-icepay
.. _Fontpack Free fonts: https://github.com/pretix/pretix-fontpack-free
.. _Wirecard payment: https://github.com/pretix/pretix-wirecard
.. _Mailing list subscription: https://github.com/pretix/pretix-newsletter-ml
.. _Average price chart: https://github.com/rixx/pretix-avgchart
.. _Pay in cash upon arrival: https://github.com/pc-coholic/pretix-cashpayment

View File

@@ -1 +1 @@
__version__ = "1.6.0"
__version__ = "1.5.2"

View File

@@ -1,7 +1,5 @@
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Event
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
class EventSerializer(I18nAwareModelSerializer):
@@ -9,27 +7,4 @@ class EventSerializer(I18nAwareModelSerializer):
model = Event
fields = ('name', 'slug', 'live', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'has_subevents')
class SubEventItemSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItem
fields = ('item', 'price')
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItemVariation
fields = ('variation', 'price')
class SubEventSerializer(I18nAwareModelSerializer):
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location',
'item_price_overrides', 'variation_price_overrides')
'presale_end', 'location')

View File

@@ -61,4 +61,4 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')
fields = ('id', 'name', 'size', 'items', 'variations')

View File

@@ -9,17 +9,7 @@ from pretix.base.models import (
from pretix.base.signals import register_ticket_outputs
class CompatibleCountryField(serializers.Field):
def to_representation(self, instance: InvoiceAddress):
if instance.country:
return str(instance.country)
else:
return instance.country_old
class InvoiceAdddressSerializer(I18nAwareModelSerializer):
country = CompatibleCountryField(source='*')
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id')
@@ -96,8 +86,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
'answers')
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'checkins', 'downloads', 'answers')
class OrderSerializer(I18nAwareModelSerializer):
@@ -125,5 +114,5 @@ class InvoiceSerializer(I18nAwareModelSerializer):
class Meta:
model = Invoice
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
fields = ('order', 'invoice_no', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines')

View File

@@ -7,4 +7,4 @@ class VoucherSerializer(I18nAwareModelSerializer):
model = Voucher
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
'tag', 'comment', 'subevent')
'tag', 'comment')

View File

@@ -6,4 +6,4 @@ class WaitingListSerializer(I18nAwareModelSerializer):
class Meta:
model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent')
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale')

View File

@@ -13,7 +13,6 @@ orga_router = routers.DefaultRouter()
orga_router.register(r'events', event.EventViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)
event_router.register(r'items', item.ItemViewSet)
event_router.register(r'categories', item.ItemCategoryViewSet)
event_router.register(r'questions', item.QuestionViewSet)

View File

@@ -1,9 +1,7 @@
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import filters, viewsets
from rest_framework import viewsets
from pretix.api.serializers.event import EventSerializer, SubEventSerializer
from pretix.base.models import Event, ItemCategory
from pretix.base.models.event import SubEvent
from pretix.api.serializers.event import EventSerializer
from pretix.base.models import Event
class EventViewSet(viewsets.ReadOnlyModelViewSet):
@@ -14,21 +12,3 @@ class EventViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return self.request.organizer.events.all()
class SubEventFilter(FilterSet):
class Meta:
model = SubEvent
fields = ['active']
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SubEventSerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filter_class = SubEventFilter
def get_queryset(self):
return self.request.event.subevents.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set'
)

View File

@@ -58,17 +58,10 @@ class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
return self.request.event.questions.prefetch_related('options').all()
class QuotaFilter(FilterSet):
class Meta:
model = Quota
fields = ['subevent']
class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = QuotaSerializer
queryset = Quota.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)
filter_class = QuotaFilter
filter_backends = (OrderingFilter,)
ordering_fields = ('id', 'size')
ordering = ('id',)

View File

@@ -1,6 +1,5 @@
import django_filters
from django.db.models import Q
from django.db.models.functions import Concat
from django.http import FileResponse
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from rest_framework import viewsets
@@ -85,7 +84,7 @@ class OrderPositionFilter(FilterSet):
class Meta:
model = OrderPosition
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'order__status', 'has_checkin',
'addon_to', 'subevent']
'addon_to']
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
@@ -138,21 +137,12 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
refers = django_filters.CharFilter(name='refers', lookup_expr='invoice_no__iexact')
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
def refers_qs(self, queryset, name, value):
return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
class Meta:
model = Invoice
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
fields = ['order', 'invoice_no', 'is_cancellation', 'refers', 'locale']
class RetryException(APIException):
@@ -165,17 +155,15 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = InvoiceSerializer
queryset = Invoice.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('nr',)
ordering_fields = ('nr', 'date')
ordering = ('invoice_no',)
ordering_fields = ('invoice_no', 'date')
filter_class = InvoiceFilter
lookup_field = 'invoice_no'
lookup_url_kwarg = 'invoice_no'
permission = 'can_view_orders'
lookup_url_kwarg = 'number'
lookup_field = 'nr'
def get_queryset(self):
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
nr=Concat('prefix', 'invoice_no')
)
return self.request.event.invoices.prefetch_related('lines').select_related('order')
@detail_route()
def download(self, request, **kwargs):

View File

@@ -16,7 +16,7 @@ class VoucherFilter(FilterSet):
class Meta:
model = Voucher
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
'price_mode', 'value', 'item', 'variation', 'quota', 'tag']
def filter_active(self, queryset, name, value):
if value:

View File

@@ -15,7 +15,7 @@ class WaitingListFilter(FilterSet):
class Meta:
model = WaitingListEntry
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
fields = ['item', 'variation', 'email', 'locale', 'has_voucher']
class WaitingListViewSet(viewsets.ReadOnlyModelViewSet):

View File

@@ -1,5 +1,4 @@
from django.apps import AppConfig
from django.conf import settings
class PretixBaseConfig(AppConfig):
@@ -10,7 +9,6 @@ class PretixBaseConfig(AppConfig):
from . import exporter # NOQA
from . import payment # NOQA
from . import exporters # NOQA
from . import invoice # NOQA
from .services import export, mail, tickets, cart, orders, cleanup, update_check # NOQA
try:
@@ -18,10 +16,6 @@ class PretixBaseConfig(AppConfig):
except ImportError:
pass
if hasattr(settings, 'RAVEN_CONFIG'):
from ..sentry import initialize
initialize()
default_app_config = 'pretix.base.PretixBaseConfig'
try:

View File

@@ -1,4 +1,3 @@
from .answers import * # noqa
from .invoices import * # noqa
from .json import * # noqa
from .mail import * # noqa

View File

@@ -1,61 +0,0 @@
import os
import tempfile
from collections import OrderedDict
from zipfile import ZipFile
from django import forms
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import QuestionAnswer
from ..exporter import BaseExporter
from ..signals import register_data_exporters
class AnswerFilesExporter(BaseExporter):
identifier = 'answerfiles'
verbose_name = _('Answers to file upload questions')
@property
def export_form_fields(self):
return OrderedDict(
[
('questions',
forms.ModelMultipleChoiceField(
queryset=self.event.questions.filter(type='F'),
label=_('Questions'),
widget=forms.CheckboxSelectMultiple,
required=False
)),
]
)
def render(self, form_data: dict):
qs = QuestionAnswer.objects.filter(
orderposition__order__event=self.event,
).select_related('orderposition', 'orderposition__order', 'question')
if form_data.get('questions'):
qs = qs.filter(question__in=form_data['questions'])
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
if i.file:
i.file.open('r')
fname = '{}-{}-{}-q{}-{}'.format(
self.event.slug.upper(),
i.orderposition.order.code,
i.orderposition.positionid,
i.question.pk,
os.path.basename(i.file.name).split('.', 1)[1]
)
zipf.writestr(fname, i.file.read())
i.file.close()
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return 'answers.zip', 'application/zip', zipf.read()
@receiver(register_data_exporters, dispatch_uid="exporter_answers")
def register_anwers_export(sender, **kwargs):
return AnswerFilesExporter

View File

@@ -21,7 +21,7 @@ class InvoiceExporter(BaseExporter):
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
i.file.open('r')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
i.file.close()

View File

@@ -1,9 +1,9 @@
import csv
import io
from collections import OrderedDict
from decimal import Decimal
import pytz
from defusedcsv import csv
from django import forms
from django.db.models import Sum
from django.dispatch import receiver
@@ -100,7 +100,7 @@ class OrderListExporter(BaseExporter):
order.invoice_address.street,
order.invoice_address.zipcode,
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country_old,
order.invoice_address.country,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:

View File

@@ -7,7 +7,6 @@ from django.utils.crypto import get_random_string
from hierarkey.forms import HierarkeyForm
from pretix.base.models import Event
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from .validators import PlaceholderValidator # NOQA
@@ -57,9 +56,6 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
kwargs['locales'] = self.locales
kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
for f in self.fields.values():
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)
def get_new_filename(self, name: str) -> str:
nonce = get_random_string(length=8)

View File

@@ -1,411 +0,0 @@
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
from typing import Tuple
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext
from reportlab.lib import pagesizes
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import (
BaseDocTemplate, Frame, NextPageTemplate, PageTemplate, Paragraph, Spacer,
Table, TableStyle,
)
from pretix.base.models import Event, Invoice
from pretix.base.signals import register_invoice_renderers
class BaseInvoiceRenderer:
"""
This is the base class for all invoice renderers.
"""
def __init__(self, event: Event):
self.event = event
def __str__(self):
return self.identifier
def generate(self, invoice: Invoice) -> Tuple[str, str, str]:
"""
This method should generate the invoice file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
which is otherwise ignored.
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
A human-readable name for this renderer. This should be short but
self-explanatory. Good examples include 'German DIN 5008' or 'Italian invoice'.
"""
raise NotImplementedError() # NOQA
@property
def identifier(self) -> str:
"""
A short and unique identifier for this renderer.
This should only contain lowercase letters and in most
cases will be the same as your package name.
"""
raise NotImplementedError() # NOQA
class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
This is a convenience class to avoid duplicate code when implementing invoice renderers
that are based on reportlab.
"""
pagesize = pagesizes.A4
left_margin = 25 * mm
right_margin = 20 * mm
top_margin = 20 * mm
bottom_margin = 15 * mm
doc_template_class = BaseDocTemplate
def _init(self):
"""
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
"""
self.stylesheet = self._get_stylesheet()
self._register_fonts()
def _get_stylesheet(self):
"""
Get a stylesheet. By default, this contains the "Normal" and "Heading1" styles.
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName='OpenSans', fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Heading1', fontName='OpenSansBd', fontSize=15, leading=15 * 1.2))
return stylesheet
def _register_fonts(self):
"""
Register fonts with reportlab. By default, this registers the OpenSans font family
"""
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBI', finders.find('fonts/OpenSans-BoldItalic.ttf')))
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
def _on_other_page(self, canvas: Canvas, doc):
"""
Called when a new page is rendered that is *not* the first page.
"""
pass
def _on_first_page(self, canvas: Canvas, doc):
"""
Called when a new page is rendered that is the first page.
"""
pass
def _get_story(self, doc):
"""
Called to create the story to be inserted into the main frames.
"""
raise NotImplementedError()
def _get_first_page_frames(self, doc):
"""
Called to create a list of frames for the first page.
"""
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
id='normal')
]
def _get_other_page_frames(self, doc):
"""
Called to create a list of frames for the other pages.
"""
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
id='normal')
]
def _build_doc(self, fhandle):
"""
Build a PDF document in a given file handle
"""
self._init()
doc = self.doc_template_class(fhandle, pagesize=self.pagesize,
leftMargin=self.left_margin, rightMargin=self.right_margin,
topMargin=self.top_margin, bottomMargin=self.bottom_margin)
doc.addPageTemplates([
PageTemplate(
id='FirstPage',
frames=self._get_first_page_frames(doc),
onPage=self._on_first_page,
pagesize=self.pagesize
),
PageTemplate(
id='OtherPages',
frames=self._get_other_page_frames(doc),
onPage=self._on_other_page,
pagesize=self.pagesize
)
])
story = self._get_story(doc)
doc.build(story)
return doc
def generate(self, invoice: Invoice):
self.invoice = invoice
buffer = BytesIO()
self._build_doc(buffer)
buffer.seek(0)
return 'invoice.pdf', 'application/pdf', buffer.read()
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
def _on_other_page(self, canvas: Canvas, doc):
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc):
canvas.setCreator('pretix.eu')
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=self.invoice.number))
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
canvas.drawText(textobject)
p = Paragraph(self.invoice.invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
canvas.drawText(textobject)
p = Paragraph(self.invoice.invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 85 * mm, 50 * mm)
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Order code').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.order.full_code)
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.refers.number)
else:
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(self.invoice.number)
textobject.moveCursor(0, 5)
if self.invoice.is_cancellation:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
else:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
canvas.drawText(textobject)
if self.invoice.event.settings.invoice_logo_image:
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
canvas.drawImage(ImageReader(logo_file),
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n',
mask='auto')
if self.invoice.event.settings.show_date_to:
p_str = (
str(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format(
from_date=self.invoice.event.get_date_from_display(),
to_date=self.invoice.event.get_date_to_display())
)
else:
p_str = (
str(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p.wrapOn(canvas, 65 * mm, 50 * mm)
p_size = p.wrap(65 * mm, 50 * mm)
p.drawOn(canvas, 125 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Event').upper())
canvas.drawText(textobject)
canvas.restoreState()
def _get_first_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
def _get_other_page_frames(self, doc):
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
return [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
def _get_story(self, doc):
story = [
NextPageTemplate('FirstPage'),
Paragraph(pgettext('invoice', 'Invoice')
if not self.invoice.is_cancellation
else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']),
Spacer(1, 5 * mm),
NextPageTemplate('OtherPages'),
]
if self.invoice.introductory_text:
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
story.append(Spacer(1, 10 * mm))
taxvalue_map = defaultdict(Decimal)
grossvalue_map = defaultdict(Decimal)
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
]
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net'),
pgettext('invoice', 'Gross'),
)]
total = Decimal('0.00')
for line in self.invoice.lines.all():
tdata.append((
Paragraph(line.description, self.stylesheet['Normal']),
localize(line.tax_rate) + " %",
localize(line.net_value) + " " + self.invoice.event.currency,
localize(line.gross_value) + " " + self.invoice.event.currency,
))
taxvalue_map[line.tax_rate] += line.tax_value
grossvalue_map[line.tax_rate] += line.gross_value
total += line.gross_value
tdata.append(
[pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + self.invoice.event.currency])
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
story.append(table)
story.append(Spacer(1, 15 * mm))
if self.invoice.payment_provider_text:
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
if self.invoice.additional_text:
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
story.append(Spacer(1, 15 * mm))
tstyledata = [
('SPAN', (1, 0), (-1, 0)),
('ALIGN', (2, 1), (-1, -1), 'RIGHT'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
('FONTSIZE', (0, 0), (-1, -1), 8),
]
tdata = [('', pgettext('invoice', 'Included taxes'), '', '', ''),
('', pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net value'), pgettext('invoice', 'Gross value'), pgettext('invoice', 'Tax'))]
for rate, gross in grossvalue_map.items():
if rate == 0:
continue
tax = taxvalue_map[rate]
tdata.append((
'',
localize(rate) + " %",
localize((gross - tax)) + " " + self.invoice.event.currency,
localize(gross) + " " + self.invoice.event.currency,
localize(tax) + " " + self.invoice.event.currency,
))
if len(tdata) > 2:
colwidths = [a * doc.width for a in (.45, .10, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=2)
table.setStyle(TableStyle(tstyledata))
story.append(table)
return story
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
def recv_classic(sender, **kwargs):
return ClassicInvoiceRenderer

View File

@@ -7,6 +7,7 @@ from django.core.urlresolvers import get_script_prefix
from django.http import HttpRequest, HttpResponse
from django.utils import timezone, translation
from django.utils.cache import patch_vary_headers
from django.utils.crypto import get_random_string
from django.utils.deprecation import MiddlewareMixin
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.utils.translation.trans_real import (
@@ -132,8 +133,8 @@ def get_language_from_request(request: HttpRequest) -> str:
return (
get_language_from_user_settings(request)
or get_language_from_session_or_cookie(request)
or get_language_from_browser(request)
or get_language_from_event(request)
or get_language_from_browser(request)
or get_default_language()
)
@@ -165,6 +166,9 @@ class SecurityMiddleware(MiddlewareMixin):
'/api/v1/docs/',
)
def process_request(self, request):
request.csp_nonce = get_random_string(length=32)
def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
@@ -179,9 +183,9 @@ class SecurityMiddleware(MiddlewareMixin):
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'style-src': ["{static}"],
'style-src': ["{static}", "'nonce-{nonce}'"],
'connect-src': ["{dynamic}", "https://checkout.stripe.com"],
'img-src': ["{static}", "data:", "https://*.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
# single-sign-on this can be nearly anything so we cannot really restrict
@@ -193,6 +197,9 @@ class SecurityMiddleware(MiddlewareMixin):
staticdomain = "'self'"
dynamicdomain = "'self'"
mediadomain = "'self'"
if settings.MEDIA_URL.startswith('http'):
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
if settings.STATIC_URL.startswith('http'):
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
if settings.SITE_URL.startswith('http'):
@@ -212,5 +219,6 @@ class SecurityMiddleware(MiddlewareMixin):
dynamicdomain += " " + domain
if request.path not in self.CSP_EXEMPT:
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain)
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
media=mediadomain, nonce=request.csp_nonce)
return resp

View File

@@ -1,573 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-30 17:47
from __future__ import unicode_literals
import django.core.validators
import django.db.migrations.operations.special
import django.db.models.deletion
import django_countries.fields
import i18nfield.fields
from django.conf import settings
from django.core.cache import cache
from django.db import migrations, models
import pretix.base.models.base
import pretix.base.models.event
import pretix.base.models.invoices
import pretix.base.models.orders
import pretix.base.models.organizer
import pretix.base.validators
def create_teams(apps, schema_editor):
Organizer = apps.get_model('pretixbase', 'Organizer')
Team = apps.get_model('pretixbase', 'Team')
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)
def rename_placeholder(app, schema_editor):
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
for setting in EventSettingsStore.objects.all():
if setting.key == 'mail_text_order_placed':
new_value = setting.value.replace('{paymentinfo}', '{payment_info}')
setting.value = new_value
cache.delete('hierarkey_{}_{}'.format('event', setting.object_id))
setting.save()
def fwd69(app, schema_editor):
Event = app.get_model('pretixbase', 'Event')
for e in Event.objects.select_related('organizer').all():
e.invoices.all().update(prefix=e.slug.upper() + '-', organizer=e.organizer)
def fwd70(app, schema_editor):
InvoiceAddress = app.get_model('pretixbase', 'InvoiceAddress')
for ia in InvoiceAddress.objects.all():
if ia.company or ia.vat_id:
ia.is_business = True
ia.save()
class Migration(migrations.Migration):
replaces = [('pretixbase', '0052_team_teaminvite'), ('pretixbase', '0058_auto_20170429_1020'),
('pretixbase', '0059_checkin_nonce'), ('pretixbase', '0060_auto_20170510_1027'),
('pretixbase', '0061_auto_20170521_0942'), ('pretixbase', '0062_auto_20170602_0948'),
('pretixbase', '0063_auto_20170702_1711'), ('pretixbase', '0064_auto_20170703_0912'),
('pretixbase', '0065_auto_20170707_0920'), ('pretixbase', '0066_auto_20170708_2102'),
('pretixbase', '0067_auto_20170712_1610'), ('pretixbase', '0068_subevent_frontpage_text'),
('pretixbase', '0069_invoice_prefix'), ('pretixbase', '0070_auto_20170719_0910')]
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(
code=create_teams,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
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',
),
migrations.AddField(
model_name='checkin',
name='nonce',
field=models.CharField(blank=True, max_length=190, null=True),
),
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'),
),
migrations.RunPython(
code=rename_placeholder,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.CreateModel(
name='TeamAPIToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=190)),
('active', models.BooleanField(default=True)),
('token', models.CharField(default=pretix.base.models.organizer.generate_api_token, max_length=64)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens',
to='pretixbase.Team')),
],
),
migrations.AlterModelOptions(
name='voucher',
options={'ordering': ('code',), 'verbose_name': 'Voucher', 'verbose_name_plural': 'Vouchers'},
),
migrations.AddField(
model_name='cartposition',
name='meta_info',
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
),
migrations.AddField(
model_name='orderposition',
name='meta_info',
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
),
migrations.AlterField(
model_name='event',
name='currency',
field=models.CharField(choices=[('AED', 'AED - UAE Dirham'), ('AFN', 'AFN - Afghani'), ('ALL', 'ALL - Lek'),
('AMD', 'AMD - Armenian Dram'),
('ANG', 'ANG - Netherlands Antillean Guilder'),
('AOA', 'AOA - Kwanza'), ('ARS', 'ARS - Argentine Peso'),
('AUD', 'AUD - Australian Dollar'), ('AWG', 'AWG - Aruban Florin'),
('AZN', 'AZN - Azerbaijanian Manat'), ('BAM', 'BAM - Convertible Mark'),
('BBD', 'BBD - Barbados Dollar'), ('BDT', 'BDT - Taka'),
('BGN', 'BGN - Bulgarian Lev'), ('BHD', 'BHD - Bahraini Dinar'),
('BIF', 'BIF - Burundi Franc'), ('BMD', 'BMD - Bermudian Dollar'),
('BND', 'BND - Brunei Dollar'), ('BOB', 'BOB - Boliviano'),
('BRL', 'BRL - Brazilian Real'), ('BSD', 'BSD - Bahamian Dollar'),
('BTN', 'BTN - Ngultrum'), ('BWP', 'BWP - Pula'),
('BYN', 'BYN - Belarusian Ruble'), ('BZD', 'BZD - Belize Dollar'),
('CAD', 'CAD - Canadian Dollar'), ('CDF', 'CDF - Congolese Franc'),
('CHF', 'CHF - Swiss Franc'), ('CLP', 'CLP - Chilean Peso'),
('CNY', 'CNY - Yuan Renminbi'), ('COP', 'COP - Colombian Peso'),
('CRC', 'CRC - Costa Rican Colon'), ('CUC', 'CUC - Peso Convertible'),
('CUP', 'CUP - Cuban Peso'), ('CVE', 'CVE - Cabo Verde Escudo'),
('CZK', 'CZK - Czech Koruna'), ('DJF', 'DJF - Djibouti Franc'),
('DKK', 'DKK - Danish Krone'), ('DOP', 'DOP - Dominican Peso'),
('DZD', 'DZD - Algerian Dinar'), ('EGP', 'EGP - Egyptian Pound'),
('ERN', 'ERN - Nakfa'), ('ETB', 'ETB - Ethiopian Birr'),
('EUR', 'EUR - Euro'),
('FJD', 'FJD - Fiji Dollar'), ('FKP', 'FKP - Falkland Islands Pound'),
('GBP', 'GBP - Pound Sterling'), ('GEL', 'GEL - Lari'),
('GHS', 'GHS - Ghana Cedi'), ('GIP', 'GIP - Gibraltar Pound'),
('GMD', 'GMD - Dalasi'), ('GNF', 'GNF - Guinea Franc'),
('GTQ', 'GTQ - Quetzal'), ('GYD', 'GYD - Guyana Dollar'),
('HKD', 'HKD - Hong Kong Dollar'), ('HNL', 'HNL - Lempira'),
('HRK', 'HRK - Kuna'), ('HTG', 'HTG - Gourde'), ('HUF', 'HUF - Forint'),
('IDR', 'IDR - Rupiah'), ('ILS', 'ILS - New Israeli Sheqel'),
('INR', 'INR - Indian Rupee'), ('IQD', 'IQD - Iraqi Dinar'),
('IRR', 'IRR - Iranian Rial'), ('ISK', 'ISK - Iceland Krona'),
('JMD', 'JMD - Jamaican Dollar'), ('JOD', 'JOD - Jordanian Dinar'),
('JPY', 'JPY - Yen'), ('KES', 'KES - Kenyan Shilling'),
('KGS', 'KGS - Som'),
('KHR', 'KHR - Riel'), ('KMF', 'KMF - Comoro Franc'),
('KPW', 'KPW - North Korean Won'), ('KRW', 'KRW - Won'),
('KWD', 'KWD - Kuwaiti Dinar'), ('KYD', 'KYD - Cayman Islands Dollar'),
('KZT', 'KZT - Tenge'), ('LAK', 'LAK - Kip'),
('LBP', 'LBP - Lebanese Pound'),
('LKR', 'LKR - Sri Lanka Rupee'), ('LRD', 'LRD - Liberian Dollar'),
('LSL', 'LSL - Loti'), ('LYD', 'LYD - Libyan Dinar'),
('MAD', 'MAD - Moroccan Dirham'), ('MDL', 'MDL - Moldovan Leu'),
('MGA', 'MGA - Malagasy Ariary'), ('MKD', 'MKD - Denar'),
('MMK', 'MMK - Kyat'),
('MNT', 'MNT - Tugrik'), ('MOP', 'MOP - Pataca'), ('MRO', 'MRO - Ouguiya'),
('MUR', 'MUR - Mauritius Rupee'), ('MVR', 'MVR - Rufiyaa'),
('MWK', 'MWK - Malawi Kwacha'), ('MXN', 'MXN - Mexican Peso'),
('MYR', 'MYR - Malaysian Ringgit'), ('MZN', 'MZN - Mozambique Metical'),
('NAD', 'NAD - Namibia Dollar'), ('NGN', 'NGN - Naira'),
('NIO', 'NIO - Cordoba Oro'), ('NOK', 'NOK - Norwegian Krone'),
('NPR', 'NPR - Nepalese Rupee'), ('NZD', 'NZD - New Zealand Dollar'),
('OMR', 'OMR - Rial Omani'), ('PAB', 'PAB - Balboa'), ('PEN', 'PEN - Sol'),
('PGK', 'PGK - Kina'), ('PHP', 'PHP - Philippine Peso'),
('PKR', 'PKR - Pakistan Rupee'), ('PLN', 'PLN - Zloty'),
('PYG', 'PYG - Guarani'), ('QAR', 'QAR - Qatari Rial'),
('RON', 'RON - Romanian Leu'), ('RSD', 'RSD - Serbian Dinar'),
('RUB', 'RUB - Russian Ruble'), ('RWF', 'RWF - Rwanda Franc'),
('SAR', 'SAR - Saudi Riyal'), ('SBD', 'SBD - Solomon Islands Dollar'),
('SCR', 'SCR - Seychelles Rupee'), ('SDG', 'SDG - Sudanese Pound'),
('SEK', 'SEK - Swedish Krona'), ('SGD', 'SGD - Singapore Dollar'),
('SHP', 'SHP - Saint Helena Pound'), ('SLL', 'SLL - Leone'),
('SOS', 'SOS - Somali Shilling'), ('SRD', 'SRD - Surinam Dollar'),
('SSP', 'SSP - South Sudanese Pound'), ('STD', 'STD - Dobra'),
('SVC', 'SVC - El Salvador Colon'), ('SYP', 'SYP - Syrian Pound'),
('SZL', 'SZL - Lilangeni'), ('THB', 'THB - Baht'), ('TJS', 'TJS - Somoni'),
('TMT', 'TMT - Turkmenistan New Manat'), ('TND', 'TND - Tunisian Dinar'),
('TOP', 'TOP - Paanga'), ('TRY', 'TRY - Turkish Lira'),
('TTD', 'TTD - Trinidad and Tobago Dollar'),
('TWD', 'TWD - New Taiwan Dollar'),
('TZS', 'TZS - Tanzanian Shilling'), ('UAH', 'UAH - Hryvnia'),
('UGX', 'UGX - Uganda Shilling'), ('USD', 'USD - US Dollar'),
('UYU', 'UYU - Peso Uruguayo'), ('UZS', 'UZS - Uzbekistan Sum'),
('VEF', 'VEF - Bolívar'), ('VND', 'VND - Dong'), ('VUV', 'VUV - Vatu'),
('WST', 'WST - Tala'), ('XAF', 'XAF - CFA Franc BEAC'),
('XAG', 'XAG - Silver'),
('XAU', 'XAU - Gold'),
('XBA', 'XBA - Bond Markets Unit European Composite Unit (EURCO)'),
('XBB', 'XBB - Bond Markets Unit European Monetary Unit (E.M.U.-6)'),
('XBC', 'XBC - Bond Markets Unit European Unit of Account 9 (E.U.A.-9)'),
('XBD', 'XBD - Bond Markets Unit European Unit of Account 17 (E.U.A.-17)'),
('XCD', 'XCD - East Caribbean Dollar'),
('XDR', 'XDR - SDR (Special Drawing Right)'),
('XOF', 'XOF - CFA Franc BCEAO'),
('XPD', 'XPD - Palladium'), ('XPF', 'XPF - CFP Franc'),
('XPT', 'XPT - Platinum'), ('XSU', 'XSU - Sucre'),
('XTS', 'XTS - Codes specifically reserved for testing purposes'),
('XUA', 'XUA - ADB Unit of Account'), ('XXX',
'XXX - The codes assigned for transactions where no currency is involved'),
('YER', 'YER - Yemeni Rial'), ('ZAR', 'ZAR - Rand'),
('ZMW', 'ZMW - Zambian Kwacha'), ('ZWL', 'ZWL - Zimbabwe Dollar')],
default='EUR', max_length=10, verbose_name='Default currency'),
),
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(
help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',
name='date',
field=models.DateField(default=pretix.base.models.invoices.today),
),
migrations.AlterField(
model_name='item',
name='allow_cancel',
field=models.BooleanField(default=True,
help_text='If this is active and the general event settings allow it, orders containing this product can be canceled by the user until the order is paid for. Users cannot cancel paid orders on their own and you can cancel orders at all times, regardless of this setting',
verbose_name='Allow product to be canceled'),
),
migrations.AddField(
model_name='questionanswer',
name='file',
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.answerfile_name),
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(
choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'),
('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload')],
max_length=5, verbose_name='Question type'),
),
migrations.AddField(
model_name='event',
name='comment',
field=models.TextField(blank=True, null=True, verbose_name='Internal comment'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.',
null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='event',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.',
null=True, verbose_name='Start of presale'),
),
migrations.CreateModel(
name='SubEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=False,
help_text='Only with this checkbox enabled, this sub-event is visible in the frontend to users.',
verbose_name='Active')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
('date_admission', models.DateTimeField(blank=True, null=True, verbose_name='Admission time')),
('presale_end',
models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True,
verbose_name='End of presale')),
('presale_start',
models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True,
verbose_name='Start of presale')),
(
'location',
i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location')),
],
options={
'verbose_name': 'Sub-Event',
'verbose_name_plural': 'Sub-Events',
'ordering': ('date_from', 'name'),
},
bases=(pretix.base.models.event.EventMixin, models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='SubEventItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item')),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
],
),
migrations.CreateModel(
name='SubEventItemVariation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
(
'variation',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation')),
],
),
migrations.AddField(
model_name='event',
name='has_subevents',
field=models.BooleanField(default=False, verbose_name='Event series'),
),
migrations.AddField(
model_name='subevent',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subevents',
to='pretixbase.Event'),
),
migrations.AddField(
model_name='subevent',
name='items',
field=models.ManyToManyField(through='pretixbase.SubEventItem', to='pretixbase.Item'),
),
migrations.AddField(
model_name='subevent',
name='variations',
field=models.ManyToManyField(through='pretixbase.SubEventItemVariation', to='pretixbase.ItemVariation'),
),
migrations.AddField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='quota',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='quotas', to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='voucher',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AddField(
model_name='waitinglistentry',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterModelOptions(
name='subevent',
options={'ordering': ('date_from', 'name'), 'verbose_name': 'Date in event series',
'verbose_name_plural': 'Dates in event series'},
),
migrations.AddField(
model_name='itemaddon',
name='price_included',
field=models.BooleanField(default=False,
help_text='If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost money individually.',
verbose_name='Add-Ons are included in the price'),
),
migrations.AlterField(
model_name='subevent',
name='active',
field=models.BooleanField(default=False,
help_text='Only with this checkbox enabled, this date is visible in the frontend to users.',
verbose_name='Active'),
),
migrations.AlterField(
model_name='subevent',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.',
null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='subevent',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.',
null=True, verbose_name='Start of presale'),
),
migrations.AddField(
model_name='subevent',
name='frontpage_text',
field=i18nfield.fields.I18nTextField(blank=True, null=True, verbose_name='Frontpage text'),
),
migrations.AddField(
model_name='invoice',
name='prefix',
field=models.CharField(db_index=True, default='', max_length=160),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='invoices',
to='pretixbase.Organizer'),
preserve_default=False,
),
migrations.RunPython(
code=fwd69, reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterUniqueTogether(
name='invoice',
unique_together=set([('organizer', 'prefix', 'invoice_no')]),
),
migrations.AlterField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='invoices',
to='pretixbase.Organizer'),
),
migrations.RenameField(
model_name='invoiceaddress',
old_name='country',
new_name='country_old',
),
migrations.AddField(
model_name='invoiceaddress',
name='country',
field=django_countries.fields.CountryField(default='', max_length=2, verbose_name='Country'),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceaddress',
name='is_business',
field=models.BooleanField(default=False, verbose_name='Business customer'),
),
migrations.RunPython(
code=fwd70, reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
]

File diff suppressed because one or more lines are too long

View File

@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-03 09:12
from __future__ import unicode_literals
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0063_auto_20170702_1711'),
]
operations = [
migrations.AddField(
model_name='questionanswer',
name='file',
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.answerfile_name),
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'), ('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload')], max_length=5, verbose_name='Question type'),
),
]

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-07 09:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0064_auto_20170703_0912'),
]
operations = [
migrations.AddField(
model_name='event',
name='comment',
field=models.TextField(blank=True, null=True, verbose_name='Internal comment'),
),
migrations.AlterField(
model_name='event',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.', null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='event',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.', null=True, verbose_name='Start of presale'),
),
]

View File

@@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-08 21:02
from __future__ import unicode_literals
import django.db.models.deletion
import i18nfield.fields
from django.db import migrations, models
import pretix.base.models.base
import pretix.base.models.event
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0065_auto_20170707_0920'),
]
operations = [
migrations.CreateModel(
name='SubEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=False, help_text='Only with this checkbox enabled, this sub-event is visible in the frontend to users.', verbose_name='Active')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
('date_admission', models.DateTimeField(blank=True, null=True, verbose_name='Admission time')),
('presale_end', models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True, verbose_name='End of presale')),
('presale_start', models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True, verbose_name='Start of presale')),
('location', i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location')),
],
options={
'verbose_name': 'Sub-Event',
'verbose_name_plural': 'Sub-Events',
'ordering': ('date_from', 'name'),
},
bases=(pretix.base.models.event.EventMixin, models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.CreateModel(
name='SubEventItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item')),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
],
),
migrations.CreateModel(
name='SubEventItemVariation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
('variation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation')),
],
),
migrations.AddField(
model_name='event',
name='has_subevents',
field=models.BooleanField(default=False, help_text='Only recommended for advanced users. If this feature is enabled, this will not only be a single event but a series of very similar events that are handled within a single shop. The single events inside the series can only differ in prices and quotas, not in other settings, and buying tickets across multiple of these events at the same time is possible.', verbose_name='Event series'),
),
migrations.AddField(
model_name='subevent',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subevents', to='pretixbase.Event'),
),
migrations.AddField(
model_name='subevent',
name='items',
field=models.ManyToManyField(through='pretixbase.SubEventItem', to='pretixbase.Item'),
),
migrations.AddField(
model_name='subevent',
name='variations',
field=models.ManyToManyField(through='pretixbase.SubEventItemVariation', to='pretixbase.ItemVariation'),
),
migrations.AddField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='quota',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='voucher',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
migrations.AddField(
model_name='waitinglistentry',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
),
]

View File

@@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-12 16:10
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0066_auto_20170708_2102'),
]
operations = [
migrations.AlterModelOptions(
name='subevent',
options={'ordering': ('date_from', 'name'), 'verbose_name': 'Date in event series', 'verbose_name_plural': 'Dates in event series'},
),
migrations.AddField(
model_name='itemaddon',
name='price_included',
field=models.BooleanField(default=False, help_text='If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost money individually.', verbose_name='Add-Ons are included in the price'),
),
migrations.AlterField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='event',
name='has_subevents',
field=models.BooleanField(default=False, verbose_name='Event series'),
),
migrations.AlterField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='quota',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='subevent',
name='active',
field=models.BooleanField(default=False, help_text='Only with this checkbox enabled, this date is visible in the frontend to users.', verbose_name='Active'),
),
migrations.AlterField(
model_name='subevent',
name='presale_end',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.', null=True, verbose_name='End of presale'),
),
migrations.AlterField(
model_name='subevent',
name='presale_start',
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.', null=True, verbose_name='Start of presale'),
),
migrations.AlterField(
model_name='voucher',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
migrations.AlterField(
model_name='waitinglistentry',
name='subevent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
),
]

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-14 16:11
from __future__ import unicode_literals
import i18nfield.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0067_auto_20170712_1610'),
]
operations = [
migrations.AddField(
model_name='subevent',
name='frontpage_text',
field=i18nfield.fields.I18nTextField(blank=True, null=True, verbose_name='Frontpage text'),
),
]

View File

@@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-17 19:29
from __future__ import unicode_literals
from django.db import migrations, models
from django.db.models import deletion
def fwd(app, schema_editor):
Event = app.get_model('pretixbase', 'Event')
for e in Event.objects.select_related('organizer').all():
e.invoices.all().update(prefix=e.slug.upper() + '-', organizer=e.organizer)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0068_subevent_frontpage_text'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='prefix',
field=models.CharField(db_index=True, default='', max_length=160),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(null=True,
on_delete=deletion.PROTECT,
related_name='invoices', to='pretixbase.Organizer'),
preserve_default=False,
),
migrations.RunPython(
fwd, migrations.RunPython.noop
),
migrations.AlterUniqueTogether(
name='invoice',
unique_together=set([('organizer', 'prefix', 'invoice_no')]),
),
migrations.AlterField(
model_name='invoice',
name='organizer',
field=models.ForeignKey(on_delete=deletion.PROTECT, related_name='invoices', to='pretixbase.Organizer'),
),
]

View File

@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-19 09:10
from __future__ import unicode_literals
import django_countries
from django.db import migrations, models
def fwd(app, schema_editor):
InvoiceAddress = app.get_model('pretixbase', 'InvoiceAddress')
for ia in InvoiceAddress.objects.all():
if ia.company or ia.vat_id:
ia.is_business = True
ia.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0069_invoice_prefix'),
]
operations = [
migrations.RenameField(
model_name='invoiceaddress',
old_name='country',
new_name='country_old',
),
migrations.AddField(
model_name='invoiceaddress',
name='country',
field=django_countries.fields.CountryField(default='', max_length=2, verbose_name='Country'),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceaddress',
name='is_business',
field=models.BooleanField(default=False, verbose_name='Business customer'),
),
migrations.RunPython(
fwd, migrations.RunPython.noop
),
]

View File

@@ -3,13 +3,13 @@ from .auth import U2FDevice, User
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin
from .event import (
Event, Event_SettingsStore, EventLock, RequiredAction, SubEvent,
Event, Event_SettingsStore, EventLock, RequiredAction,
generate_invite_token,
)
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota, SubEventItem, SubEventItemVariation, itempicture_upload_to,
Quota, itempicture_upload_to,
)
from .log import LogEntry
from .orders import (

View File

@@ -6,8 +6,7 @@ from django.db import models
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.utils.crypto import get_random_string
from pretix.helpers.json import CustomJSONEncoder
from i18nfield.utils import I18nJSONEncoder
def cachedfile_name(instance, filename: str) -> str:
@@ -17,7 +16,7 @@ def cachedfile_name(instance, filename: str) -> str:
class CachedFile(models.Model):
"""
An uploaded file, with an optional expiry date.
A cached file (e.g. pre-generated ticket PDF)
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
expires = models.DateTimeField(null=True, blank=True)
@@ -55,7 +54,7 @@ class LoggingMixin:
event = self.event
l = LogEntry(content_object=self, user=user, action_type=action, event=event)
if data:
l.data = json.dumps(data, cls=CustomJSONEncoder)
l.data = json.dumps(data, cls=I18nJSONEncoder)
l.save()

View File

@@ -1,7 +1,6 @@
import string
import uuid
from collections import OrderedDict
from datetime import datetime, time
from datetime import date, datetime, time
import pytz
from django.conf import settings
@@ -10,128 +9,23 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Q
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import CustomSMTPBackend
from pretix.base.models.base import LoggedModel
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBlacklistValidator
from pretix.helpers.daterange import daterange
from pretix.helpers.json import safe_string
from ..settings import settings_hierarkey
from .organizer import Organizer
class EventMixin:
def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
if self.date_from and self.date_to and self.date_from > self.date_to:
raise ValidationError({'date_to': _('The end of the event has to be later than its start.')})
super().clean()
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.
"""
tz = tz or pytz.timezone(self.settings.timezone)
return _date(
self.date_from.astimezone(tz),
"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:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
if ``show_date_to`` is ``False``.
"""
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return ""
return _date(
self.date_to.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_range_display(self, tz=None) -> str:
"""
Returns a formatted string containing the start date and the event date
of the event with respect to the current locale and to the ``show_times`` and
``show_date_to`` settings.
"""
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
@property
def presale_has_ended(self):
"""
Is true, when ``presale_end`` is set and in the past.
"""
if self.presale_end and now() > self.presale_end:
return True
return False
@property
def presale_is_running(self):
"""
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
set or in the past.
"""
if self.presale_start and now() < self.presale_start:
return False
if self.presale_end and now() > self.presale_end:
return False
return True
@property
def event_microdata(self):
import json
eventdict = {
"@context": "http://schema.org",
"@type": "Event", "location": {
"@type": "Place",
"address": str(self.location)
},
"name": str(self.name)
}
if self.settings.show_times:
eventdict["startDate"] = self.date_from.isoformat()
if self.settings.show_date_to and self.date_to is not None:
eventdict["endDate"] = self.date_to.isoformat()
else:
eventdict["startDate"] = self.date_from.date().isoformat()
if self.settings.show_date_to and self.date_to is not None:
eventdict["endDate"] = self.date_to.date().isoformat()
return safe_string(json.dumps(eventdict))
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
class Event(EventMixin, LoggedModel):
class Event(LoggedModel):
"""
This model represents an event. An event is anything you can buy
tickets for.
@@ -160,8 +54,6 @@ class Event(EventMixin, LoggedModel):
:param plugins: A comma-separated list of plugin names that are active for this
event.
:type plugins: str
:param has_subevents: Enable event series functionality
:type has_subevents: bool
"""
settings_namespace = 'event'
@@ -204,12 +96,12 @@ class Event(EventMixin, LoggedModel):
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),
help_text=_("Optional. No products will be sold after this date."),
help_text=_("No products will be sold after this date."),
)
presale_start = models.DateTimeField(
null=True, blank=True,
verbose_name=_("Start of presale"),
help_text=_("Optional. No products will be sold before this date."),
help_text=_("No products will be sold before this date."),
)
location = I18nTextField(
null=True, blank=True,
@@ -220,14 +112,6 @@ class Event(EventMixin, LoggedModel):
null=True, blank=True,
verbose_name=_("Plugins"),
)
comment = models.TextField(
verbose_name=_("Internal comment"),
null=True, blank=True
)
has_subevents = models.BooleanField(
verbose_name=_('Event series'),
default=False
)
class Meta:
verbose_name = _("Event")
@@ -242,6 +126,13 @@ class Event(EventMixin, LoggedModel):
self.get_cache().clear()
return obj
def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
if self.date_from and self.date_to and self.date_from > self.date_to:
raise ValidationError({'date_to': _('The end of the event has to be later than its start.')})
super().clean()
def get_plugins(self) -> "list[str]":
"""
Returns the names of the plugins activated for this event as a list.
@@ -250,6 +141,47 @@ class Event(EventMixin, LoggedModel):
return []
return self.plugins.split(",")
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.
"""
tz = tz or pytz.timezone(self.settings.timezone)
return _date(
self.date_from.astimezone(tz),
"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:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
if ``show_date_to`` is ``False``.
"""
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return ""
return _date(
self.date_to.astimezone(tz),
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_range_display(self, tz=None) -> str:
tz = tz or pytz.timezone(self.settings.timezone)
if not self.settings.show_date_to or not self.date_to:
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
"""
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
@@ -261,6 +193,20 @@ class Event(EventMixin, LoggedModel):
return ObjectRelatedCache(self)
@property
def presale_has_ended(self):
if self.presale_end and now() > self.presale_end:
return True
return False
@property
def presale_is_running(self):
if self.presale_start and now() < self.presale_start:
return False
if self.presale_end and now() > self.presale_end:
return False
return True
def lock(self):
"""
Returns a contextmanager that can be used to lock an event for bookings.
@@ -270,10 +216,6 @@ class Event(EventMixin, LoggedModel):
return locking.LockManager(self)
def get_mail_backend(self, force_custom=False):
"""
Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the event's settings.
"""
if self.settings.smtp_use_custom or force_custom:
return CustomSMTPBackend(host=self.settings.smtp_host,
port=self.settings.smtp_port,
@@ -287,12 +229,9 @@ class Event(EventMixin, LoggedModel):
@property
def payment_term_last(self):
"""
The last datetime of payments for this event.
"""
tz = pytz.timezone(self.settings.timezone)
return make_aware(datetime.combine(
self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(),
self.settings.get('payment_term_last', as_type=date),
time(hour=23, minute=59, second=59)
), tz)
@@ -301,7 +240,6 @@ class Event(EventMixin, LoggedModel):
from ..signals import event_copy_data
self.plugins = other.plugins
self.is_public = other.is_public
self.save()
category_map = {}
@@ -335,7 +273,7 @@ class Event(EventMixin, LoggedModel):
ia.addon_category = category_map[ia.addon_category.pk]
ia.save()
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
for q in Quota.objects.filter(event=other).prefetch_related('items', 'variations'):
items = list(q.items.all())
vars = list(q.variations.all())
q.pk = None
@@ -376,9 +314,6 @@ class Event(EventMixin, LoggedModel):
event_copy_data.send(sender=self, other=other)
def get_payment_providers(self) -> dict:
"""
Returns a dictionary of initialized payment providers mapped by their identifiers.
"""
from ..signals import register_payment_providers
responses = register_payment_providers.send(self)
@@ -389,137 +324,7 @@ class Event(EventMixin, LoggedModel):
for p in response:
pp = p(self)
providers[pp.identifier] = pp
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
def get_invoice_renderers(self) -> dict:
"""
Returns a dictionary of initialized invoice renderers mapped by their identifiers.
"""
from ..signals import register_invoice_renderers
responses = register_invoice_renderers.send(self)
renderers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
renderers[pp.identifier] = pp
return renderers
@property
def invoice_renderer(self):
"""
Returns the currently configured invoice renderer.
"""
irs = self.get_invoice_renderers()
return irs[self.settings.invoice_renderer]
@property
def active_subevents(self):
"""
Returns a queryset of active subevents.
"""
return self.subevents.filter(active=True).order_by('-date_from', 'name')
@property
def active_future_subevents(self):
return self.subevents.filter(
Q(active=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(date_to__gte=now())
)
).order_by('date_from', 'name')
class SubEvent(EventMixin, LoggedModel):
"""
This model represents a date within an event series.
:param event: The event this belongs to
:type event: Event
:param active: Whether to show the subevent
:type active: bool
:param name: This event's full title
:type name: str
:param date_from: The datetime this event starts
:type date_from: datetime
:param date_to: The datetime this event ends
:type date_to: datetime
:param presale_start: No tickets will be sold before this date.
:type presale_start: datetime
:param presale_end: No tickets will be sold after this date.
:type presale_end: datetime
:param location: venue
:type location: str
"""
event = models.ForeignKey(Event, related_name="subevents", on_delete=models.PROTECT)
active = models.BooleanField(default=False, verbose_name=_("Active"),
help_text=_("Only with this checkbox enabled, this date is visible in the "
"frontend to users."))
name = I18nCharField(
max_length=200,
verbose_name=_("Name"),
)
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"))
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),
help_text=_("Optional. No products will be sold after this date."),
)
presale_start = models.DateTimeField(
null=True, blank=True,
verbose_name=_("Start of presale"),
help_text=_("Optional. No products will be sold before this date."),
)
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text")
)
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
class Meta:
verbose_name = _("Date in event series")
verbose_name_plural = _("Dates in event series")
ordering = ("date_from", "name")
def __str__(self):
return '{} - {}'.format(self.name, self.get_date_range_display())
@cached_property
def settings(self):
return self.event.settings
@cached_property
def item_price_overrides(self):
from .items import SubEventItem
return {
si.item_id: si.price
for si in SubEventItem.objects.filter(subevent=self, price__isnull=False)
}
@cached_property
def var_price_overrides(self):
from .items import SubEventItemVariation
return {
si.variation_id: si.price
for si in SubEventItemVariation.objects.filter(subevent=self, price__isnull=False)
}
return providers
def generate_invite_token():

View File

@@ -9,10 +9,9 @@ from django.utils.functional import cached_property
def invoice_filename(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
return 'invoices/{org}/{ev}/{no}-{code}-{secret}.{ext}'.format(
return 'invoices/{org}/{ev}/{no}-{code}-{secret}.pdf'.format(
org=instance.event.organizer.slug, ev=instance.event.slug,
no=instance.number, code=instance.order.code, secret=secret,
ext=filename.split('.')[-1]
no=instance.number, code=instance.order.code, secret=secret
)
@@ -29,8 +28,6 @@ class Invoice(models.Model):
:type order: Order
:param event: The event this belongs to (for convenience)
:type event: Event
:param organizer: The organizer this belongs to (redundant, for enforcing uniqueness)
:type organizer: Organizer
:param invoice_no: The human-readable, event-unique invoice number
:type invoice_no: int
:param is_cancellation: Whether or not this is a cancellation instead of an invoice
@@ -57,9 +54,7 @@ class Invoice(models.Model):
:type file: File
"""
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
organizer = models.ForeignKey('Organizer', related_name='invoices', db_index=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
prefix = models.CharField(max_length=160, db_index=True)
invoice_no = models.CharField(max_length=19, db_index=True)
is_cancellation = models.BooleanField(default=False)
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
@@ -78,10 +73,7 @@ class Invoice(models.Model):
return '{:05d}'.format(int(number))
def _get_numeric_invoice_number(self):
numeric_invoices = Invoice.objects.filter(
event__organizer=self.event.organizer,
prefix=self.prefix,
).exclude(invoice_no__contains='-')
numeric_invoices = Invoice.objects.filter(event=self.event).exclude(invoice_no__contains='-')
return self._to_numeric_invoice_number(numeric_invoices.count() + 1)
def _get_invoice_number_from_order(self):
@@ -95,10 +87,6 @@ class Invoice(models.Model):
raise ValueError('Every invoice needs to be connected to an order')
if not self.event:
self.event = self.order.event
if not self.organizer:
self.organizer = self.order.event.organizer
if not self.prefix:
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
if not self.invoice_no:
for i in range(10):
if self.event.settings.get('invoice_numbers_consecutive'):
@@ -127,8 +115,8 @@ class Invoice(models.Model):
"""
Returns the invoice number in a human-readable string with the event slug prepended.
"""
return '{prefix}{code}'.format(
prefix=self.prefix,
return '{event}-{code}'.format(
event=self.event.slug.upper(),
code=self.invoice_no
)
@@ -137,7 +125,7 @@ class Invoice(models.Model):
return self.refered.filter(is_cancellation=True).exists()
class Meta:
unique_together = ('organizer', 'prefix', 'invoice_no')
unique_together = ('event', 'invoice_no')
ordering = ('invoice_no',)

View File

@@ -10,13 +10,13 @@ from django.db import models
from django.db.models import F, Func, Q, Sum
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
from .event import Event, SubEvent
from .event import Event
class ItemCategory(LoggedModel):
@@ -88,40 +88,6 @@ def itempicture_upload_to(instance, filename: str) -> str:
)
class SubEventItem(models.Model):
"""
This model can be used to change the price of a product for a single subevent (i.e. a
date in an event series).
:param subevent: The date this belongs to
:type subevent: SubEvent
:param item: The item to modify the price for
:type item: Item
:param price: The modified price (or ``None`` for the original price)
:type price: Decimal
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
item = models.ForeignKey('Item', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
class SubEventItemVariation(models.Model):
"""
This model can be used to change the price of a product variation for a single
subevent (i.e. a date in an event series).
:param subevent: The date this belongs to
:type subevent: SubEvent
:param variation: The variation to modify the price for
:type variation: ItemVariation
:param price: The modified price (or ``None`` for the original price)
:type price: Decimal
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
class Item(LoggedModel):
"""
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
@@ -305,7 +271,7 @@ class Item(LoggedModel):
return False
return True
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None):
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None):
"""
This method is used to determine whether this Item is currently available
for sale.
@@ -319,18 +285,12 @@ class Item(LoggedModel):
:raises ValueError: if you call this on an item which has variations associated with it.
Please use the method on the ItemVariation object you are interested in.
"""
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
self.quotas.select_related('subevent').filter(subevent=subevent)
if subevent else self.quotas.all()
))
if not subevent and self.event.has_subevents:
raise TypeError('You need to supply a subevent.')
check_quotas = set(self.quotas.all())
if ignored_quotas:
check_quotas -= set(ignored_quotas)
if not check_quotas:
return Quota.AVAILABILITY_OK, sys.maxsize
if self.has_variations: # NOQA
if self.variations.count() > 0: # NOQA
raise ValueError('Do not call this directly on items which have variations '
'but call this on their ItemVariation objects')
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
@@ -411,7 +371,7 @@ class ItemVariation(models.Model):
if self.item:
self.item.event.get_cache().clear()
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None) -> Tuple[int, int]:
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
"""
This method is used to determine whether this ItemVariation is currently
available for sale in terms of quotas.
@@ -423,15 +383,9 @@ class ItemVariation(models.Model):
:param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
:returns: any of the return codes of :py:meth:`Quota.availability()`.
"""
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
self.quotas.filter(subevent=subevent).select_related('subevent')
if subevent else self.quotas.all()
))
check_quotas = set(self.quotas.all())
if ignored_quotas:
check_quotas -= set(ignored_quotas)
if not subevent and self.item.event.has_subevents: # NOQA
raise TypeError('You need to supply a subevent.')
if not check_quotas:
return Quota.AVAILABILITY_OK, sys.maxsize
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
@@ -448,17 +402,6 @@ class ItemAddOn(models.Model):
An instance of this model indicates that buying a ticket of the time ``base_item``
allows you to add up to ``max_count`` items from the category ``addon_category``
to your order that will be associated with the base item.
:param base_item: The base item the add-ons are attached to
:type base_item: Item
:param addon_category: The category the add-on can be chosen from
:type addon_category: ItemCategory
:param min_count: The minimal number of add-ons to be chosen
:type min_count: int
:param max_count: The maximal number of add-ons to be chosen
:type max_count: int
:param position: An integer used for sorting
:type position: int
"""
base_item = models.ForeignKey(
Item,
@@ -477,12 +420,6 @@ class ItemAddOn(models.Model):
default=1,
verbose_name=_('Maximum number')
)
price_included = models.BooleanField(
default=False,
verbose_name=_('Add-Ons are included in the price'),
help_text=_('If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost '
'money individually.')
)
position = models.PositiveIntegerField(
default=0,
verbose_name=_("Position")
@@ -509,7 +446,6 @@ class Question(LoggedModel):
* a multi-line string (``TYPE_TEXT``)
* a boolean (``TYPE_BOOLEAN``)
* a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``)
* a file upload (``TYPE_FILE``))
:param event: The event this question belongs to
:type event: Event
@@ -527,15 +463,13 @@ class Question(LoggedModel):
TYPE_BOOLEAN = "B"
TYPE_CHOICE = "C"
TYPE_CHOICE_MULTIPLE = "M"
TYPE_FILE = "F"
TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")),
(TYPE_TEXT, _("Multiline text")),
(TYPE_BOOLEAN, _("Yes/No")),
(TYPE_CHOICE, _("Choose one from a list")),
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
(TYPE_FILE, _("File upload")),
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list"))
)
event = models.ForeignKey(
@@ -637,8 +571,6 @@ class Quota(LoggedModel):
:param event: The event this belongs to
:type event: Event
:param subevent: The event series date this belongs to, if event series are enabled
:type subevent: SubEvent
:param name: This quota's name
:type name: str
:param size: The number of items in this quota
@@ -658,13 +590,6 @@ class Quota(LoggedModel):
related_name="quotas",
verbose_name=_("Event"),
)
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.CASCADE,
related_name="quotas",
verbose_name=pgettext_lazy('subevent', "Date"),
)
name = models.CharField(
max_length=200,
verbose_name=_("Name")
@@ -759,11 +684,11 @@ class Quota(LoggedModel):
now_dt = now_dt or now()
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
func = 'MAX'
else: # NOQA
else:
func = 'GREATEST'
return Voucher.objects.filter(
Q(event=self.event) & Q(subevent=self.subevent) &
Q(event=self.event) &
Q(block_quota=True) &
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
Q(Q(self._position_lookup) | Q(quota=self))
@@ -774,7 +699,7 @@ class Quota(LoggedModel):
def count_waiting_list_pending(self) -> int:
from pretix.base.models import WaitingListEntry
return WaitingListEntry.objects.filter(
Q(voucher__isnull=True) & Q(subevent=self.subevent) &
Q(voucher__isnull=True) &
self._position_lookup
).distinct().count()
@@ -783,7 +708,7 @@ class Quota(LoggedModel):
now_dt = now_dt or now()
return CartPosition.objects.filter(
Q(event=self.event) & Q(subevent=self.subevent) &
Q(event=self.event) &
Q(expires__gte=now_dt) &
~Q(
Q(voucher__isnull=False) & Q(voucher__block_quota=True)
@@ -797,14 +722,14 @@ class Quota(LoggedModel):
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event
).values('id').distinct().count()
def count_paid_orders(self):
from pretix.base.models import Order, OrderPosition
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event
).values('id').distinct().count()
@cached_property

View File

@@ -5,9 +5,8 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.models.event import SubEvent
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
class LogEntry(models.Model):
@@ -68,7 +67,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'code': co.code
}),
'val': co.code,
'val': escape(co.code),
}
elif isinstance(co, Voucher):
a_text = _('Voucher {val}')
@@ -78,7 +77,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'voucher': co.id
}),
'val': co.code[:6],
'val': escape(co.code[:6]),
}
elif isinstance(co, Item):
a_text = _('Product {val}')
@@ -88,17 +87,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'item': co.id
}),
'val': co.name,
}
elif isinstance(co, SubEvent):
a_text = pgettext_lazy('subevent', 'Date {val}')
a_map = {
'href': reverse('control:event.subevent', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'subevent': co.id
}),
'val': str(co)
'val': escape(co.name),
}
elif isinstance(co, Quota):
a_text = _('Quota {val}')
@@ -108,7 +97,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'quota': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, ItemCategory):
a_text = _('Category {val}')
@@ -118,7 +107,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'category': co.id
}),
'val': co.name,
'val': escape(co.name),
}
elif isinstance(co, Question):
a_text = _('Question {val}')
@@ -128,7 +117,7 @@ class LogEntry(models.Model):
'organizer': self.event.organizer.slug,
'question': co.id
}),
'val': co.question,
'val': escape(co.question),
}
if a_text and a_map:

View File

@@ -2,33 +2,23 @@ import copy
import json
import os
import string
from datetime import datetime, time
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Union
from typing import List, Union
import pytz
from django.conf import settings
from django.db import models
from django.db.models import F, Sum
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.utils.crypto import get_random_string
from django.utils.encoding import escape_uri_path
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import CountryField
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from ..decimal import round_decimal
from .base import LoggedModel
from .event import Event, SubEvent
from .event import Event
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -274,16 +264,7 @@ class Order(LoggedModel):
"""
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
return False
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
if self.event.has_subevents and modify_deadline:
modify_deadline = min([
modify_deadline.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
])
elif modify_deadline:
modify_deadline = modify_deadline.datetime(self.event)
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=datetime)
if modify_deadline is not None and now() > modify_deadline:
return False
if self.event.settings.get('invoice_address_asked', as_type=bool):
@@ -297,9 +278,6 @@ class Order(LoggedModel):
@property
def can_user_cancel(self) -> bool:
"""
Returns whether or not this order can be canceled by the user.
"""
positions = self.positions.all().select_related('item')
cancelable = all([op.item.allow_cancel for op in positions])
return self.event.settings.cancel_allow_user and cancelable
@@ -311,41 +289,6 @@ class Order(LoggedModel):
and not self.event.settings.get('payment_term_expire_automatically')
)
@property
def ticket_download_date(self):
"""
Returns the first date the tickets for this order can be downloaded or ``None`` if there is no
restriction.
"""
dl_date = self.event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
if dl_date:
if self.event.has_subevents:
dl_date = min([
dl_date.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
])
else:
dl_date = dl_date.datetime(self.event)
return dl_date
@property
def payment_term_last(self):
tz = pytz.timezone(self.event.settings.timezone)
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents:
term_last = min([
term_last.datetime(se).date()
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
])
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
return term_last
def _can_be_paid(self) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
@@ -353,9 +296,9 @@ class Order(LoggedModel):
'late': _("The payment can not be accepted as it the order is expired and you configured that no late "
"payments should be accepted in the payment settings."),
}
term_last = self.payment_term_last
if term_last:
if now() > term_last:
if self.event.settings.get('payment_term_last'):
if now() > self.event.payment_term_last:
return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING:
@@ -374,11 +317,9 @@ class Order(LoggedModel):
quota_cache = {}
try:
for i, op in enumerate(positions):
quotas = list(op.quotas)
quotas = list(op.item.quotas.all()) if op.variation is None else list(op.variation.quotas.all())
if len(quotas) == 0:
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
item=str(op.item) + (' - ' + str(op.variation) if op.variation else '')
))
raise Quota.QuotaExceededException(error_messages['unavailable'])
for quota in quotas:
if quota.id not in quota_cache:
@@ -398,59 +339,6 @@ class Order(LoggedModel):
return str(e)
return True
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, ``recipient`` and
``order`` parameters.
* Create a ``LogEntry`` with the email contents.
:param subject: Subject of the email
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
:param context: Dictionary to use for rendering the template
:param log_entry_type: Key to be used for the log entry
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
recipient = self.email
email_content = render_mail(template, context)[0]
try:
with language(self.locale):
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
data={
'subject': subject,
'message': email_content,
'recipient': recipient
}
)
def answerfile_name(instance, filename: str) -> str:
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
event = (instance.cartposition if instance.cartposition else instance.orderposition.order).event
return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format(
org=event.organizer.slug,
ev=event.slug,
secret=secret,
filename=escape_uri_path(filename),
)
class QuestionAnswer(models.Model):
"""
@@ -482,39 +370,12 @@ class QuestionAnswer(models.Model):
QuestionOption, related_name='answers', blank=True
)
answer = models.TextField()
file = models.FileField(
null=True, blank=True, upload_to=answerfile_name
)
@property
def file_link(self):
from pretix.multidomain.urlreverse import eventreverse
if self.file:
if self.orderposition:
url = eventreverse(self.orderposition.order.event, 'presale:event.order.download.answer', kwargs={
'order': self.orderposition.order.code,
'secret': self.orderposition.order.secret,
'answer': self.pk,
})
else:
url = eventreverse(self.cartposition.event, 'presale:event.cart.download.answer', kwargs={
'answer': self.pk,
})
return mark_safe("<a href='{}'>{}</a>".format(
url,
escape(self.file.name.split('.', 1)[-1])
))
return ""
def __str__(self):
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
return str(_("Yes"))
elif self.question.type == Question.TYPE_BOOLEAN and self.answer == "False":
return str(_("No"))
elif self.question.type == Question.TYPE_FILE:
return str(_("<file>"))
else:
return self.answer
@@ -528,8 +389,6 @@ class AbstractPosition(models.Model):
"""
A position can either be one line of an order or an item placed in a cart.
:param subevent: The date in the event series, if event series are enabled
:type subevent: SubEvent
:param item: The selected item
:type item: Item
:param variation: The selected ItemVariation or null, if the item has no variations
@@ -546,15 +405,7 @@ class AbstractPosition(models.Model):
:type attendee_email: str
:param voucher: A voucher that has been applied to this sale
:type voucher: Voucher
:param meta_info: Additional meta information on the position, JSON-encoded.
:type meta_info: str
"""
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.CASCADE,
verbose_name=pgettext_lazy("subevent", "Date"),
)
item = models.ForeignKey(
Item,
verbose_name=_("Item"),
@@ -587,21 +438,10 @@ class AbstractPosition(models.Model):
addon_to = models.ForeignKey(
'self', null=True, blank=True, on_delete=models.CASCADE, related_name='addons'
)
meta_info = models.TextField(
verbose_name=_("Meta information"),
null=True, blank=True
)
class Meta:
abstract = True
@property
def meta_info_data(self):
if self.meta_info:
return json.loads(self.meta_info)
else:
return {}
def cache_answers(self):
"""
Creates two properties on the object.
@@ -626,12 +466,6 @@ class AbstractPosition(models.Model):
def net_price(self):
return self.price - self.tax_value
@property
def quotas(self):
return (self.item.quotas.filter(subevent=self.subevent)
if self.variation is None
else self.variation.quotas.filter(subevent=self.subevent))
class OrderPosition(AbstractPosition):
"""
@@ -780,17 +614,14 @@ class CartPosition(AbstractPosition):
class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True)
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address')
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
name = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
street = models.TextField(verbose_name=_('Address'), blank=False)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
country = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'))
def cachedticket_name(instance, filename: str) -> str:
@@ -840,17 +671,3 @@ def cachedticket_delete(sender, instance, **kwargs):
if instance.file:
# Pass false so FileField doesn't save the model.
instance.file.delete(False)
@receiver(post_delete, sender=CachedCombinedTicket)
def cachedcombinedticket_delete(sender, instance, **kwargs):
if instance.file:
# Pass false so FileField doesn't save the model.
instance.file.delete(False)
@receiver(post_delete, sender=QuestionAnswer)
def answer_delete(sender, instance, **kwargs):
if instance.file:
# Pass false so FileField doesn't save the model.
instance.file.delete(False)

View File

@@ -5,11 +5,11 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from ..decimal import round_decimal
from .base import LoggedModel
from .event import Event, SubEvent
from .event import Event
from .items import Item, ItemVariation, Quota
@@ -33,8 +33,6 @@ class Voucher(LoggedModel):
:param event: The event this voucher is valid for
:type event: Event
:param subevent: The date in the event series, if event series are enabled
:type subevent: SubEvent
:param code: The secret voucher code
:type code: str
:param max_usages: The number of times this voucher can be redeemed
@@ -82,12 +80,6 @@ class Voucher(LoggedModel):
related_name="vouchers",
verbose_name=_("Event"),
)
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.CASCADE,
verbose_name=pgettext_lazy("subevent", "Date"),
)
code = models.CharField(
verbose_name=_("Voucher code"),
max_length=255, default=generate_code,
@@ -194,8 +186,6 @@ class Voucher(LoggedModel):
'Otherwise it might be unclear which quotas to block.'))
else:
raise ValidationError(_('You need to specify either a quota or a product.'))
if self.event.has_subevents and self.block_quota and not self.subevent:
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
def save(self, *args, **kwargs):
self.code = self.code.upper()

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from pretix.base.i18n import language
from pretix.base.models import Voucher
@@ -11,7 +11,7 @@ from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import build_absolute_uri
from .base import LoggedModel
from .event import Event, SubEvent
from .event import Event
from .items import Item, ItemVariation
@@ -26,12 +26,6 @@ class WaitingListEntry(LoggedModel):
related_name="waitinglistentries",
verbose_name=_("Event"),
)
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.CASCADE,
verbose_name=pgettext_lazy("subevent", "Date"),
)
created = models.DateTimeField(
verbose_name=_("On waiting list since"),
auto_now_add=True
@@ -83,9 +77,9 @@ class WaitingListEntry(LoggedModel):
def send_voucher(self, quota_cache=None, user=None):
availability = (
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
self.variation.check_quotas(count_waitinglist=False, _cache=quota_cache)
if self.variation
else self.item.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
else self.item.check_quotas(count_waitinglist=False, _cache=quota_cache)
)
if availability[1] < 1:
raise WaitingListException(_('This product is currently not available.'))
@@ -104,7 +98,6 @@ class WaitingListEntry(LoggedModel):
email=self.email
),
block_quota=True,
subevent=self.subevent,
)
v.log_action('pretix.voucher.added.waitinglist', {
'item': self.item.pk,
@@ -114,8 +107,7 @@ class WaitingListEntry(LoggedModel):
'valid_until': v.valid_until.isoformat(),
'max_usages': 1,
'email': self.email,
'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None,
'waitinglistentry': self.pk
}, user=user)
self.log_action('pretix.waitinglist.voucher', user=user)
self.voucher = v

View File

@@ -1,5 +1,5 @@
import logging
from collections import OrderedDict
from datetime import date
from decimal import Decimal
from typing import Any, Dict, Union
@@ -16,14 +16,11 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from i18nfield.strings import LazyI18nString
from pretix.base.decimal import round_decimal
from pretix.base.models import CartPosition, Event, Order, Quota
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.models import Event, Order, Quota
from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.presale.views import get_cart_total
logger = logging.getLogger(__name__)
class PaymentProviderForm(Form):
def clean(self):
@@ -98,7 +95,7 @@ class BasePaymentProvider:
"""
A short and unique identifier for this payment provider.
This should only contain lowercase letters and in most
cases will be the same as your package name.
cases will be the same as your packagename.
"""
raise NotImplementedError() # NOQA
@@ -153,17 +150,18 @@ class BasePaymentProvider:
required=False
)),
('_availability_date',
RelativeDateField(
forms.DateField(
label=_('Available until'),
help_text=_('Users will not be able to choose this payment provider after the given date.'),
required=False,
widget=forms.DateInput(attrs={'class': 'datepickerfield'})
)),
('_fee_reverse_calc',
forms.BooleanField(
label=_('Calculate the fee from the total value including the fee.'),
help_text=_('We recommend to enable this if you want your users to pay the payment fees of your '
help_text=_('We recommend you to enable this if you want your users to pay the payment fees of your '
'payment provider. <a href="{docs_url}" target="_blank">Click here '
'for detailed information on what this does.</a> Don\'t forget to set the correct fees '
'for detailled information on what this does.</a> Don\'t forget to set the correct fees '
'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
required=False
)),
@@ -232,36 +230,12 @@ class BasePaymentProvider:
return form
def _is_still_available(self, now_dt=None, cart_id=None, order=None):
def _is_still_available(self, now_dt=None):
now_dt = now_dt or now()
tz = pytz.timezone(self.event.settings.timezone)
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
availability_date = self.settings.get('_availability_date', as_type=date)
if availability_date:
if self.event.has_subevents and cart_id:
availability_date = min([
availability_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=CartPosition.objects.filter(
cart_id=cart_id, event=self.event
).values_list('subevent', flat=True)
)
])
elif self.event.has_subevents and order:
availability_date = min([
availability_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=order.positions.values_list('subevent', flat=True)
)
])
elif self.event.has_subevents:
logger.error('Payment provider is not subevent-ready.')
return False
else:
availability_date = availability_date.datetime(self.event).date()
return availability_date >= now_dt.astimezone(tz).date()
return True
def is_allowed(self, request: HttpRequest) -> bool:
@@ -273,11 +247,11 @@ class BasePaymentProvider:
The default implementation checks for the _availability_date setting to be either unset or in the future.
"""
return self._is_still_available(cart_id=request.session.session_key)
return self._is_still_available()
def payment_form_render(self, request: HttpRequest) -> str:
"""
When the user selects this provider as his preferred payment method,
When the user selects this provider as his prefered payment method,
they will be shown the HTML you return from this method.
The default implementation will call :py:meth:`checkout_form`
@@ -411,7 +385,7 @@ class BasePaymentProvider:
:param order: The order object
"""
return self._is_still_available(order=order)
return self._is_still_available()
def order_can_retry(self, order: Order) -> bool:
"""
@@ -484,7 +458,7 @@ class BasePaymentProvider:
"""
return _('Payment provider: %s' % self.verbose_name)
def order_control_refund_render(self, order: Order, request: HttpRequest=None) -> str:
def order_control_refund_render(self, order: Order) -> str:
"""
Will be called if the event administrator clicks an order's 'refund' button.
This can be used to display information *before* the order is being refunded.
@@ -494,11 +468,6 @@ class BasePaymentProvider:
automatically.
:param order: The order object
:param request: The HTTP request
.. versionchanged:: 1.6
The parameter ``request`` has been added.
"""
return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, '
'please transfer the money back manually.')

View File

@@ -1,279 +0,0 @@
import datetime
from collections import namedtuple
from typing import Union
import pytz
from dateutil import parser
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
BASE_CHOICES = (
('date_from', _('Event start')),
('date_to', _('Event end')),
('date_admission', _('Event admission')),
('presale_start', _('Presale start')),
('presale_end', _('Presale end')),
)
RelativeDate = namedtuple('RelativeDate', ['days_before', 'time', 'base_date_name'])
class RelativeDateWrapper:
"""
This contains information on a date that might be relative to an event. This means
that the underlying data is either a fixed date or a number of days and a wall clock
time to calculate the date based on a base point.
The base point can be the date_from, date_to, date_admission, presale_start or presale_end
attribute of an event or subevent. If the respective attribute is not set, ``date_from``
will be used.
"""
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
self.data = data
def date(self, event) -> datetime.datetime:
from .models import SubEvent
if isinstance(self.data, datetime.date):
return self.data
elif isinstance(self.data, datetime.datetime):
return self.data.date()
else:
tz = pytz.timezone(event.settings.timezone)
if isinstance(event, SubEvent):
base_date = (
getattr(event, self.data.base_date_name)
or getattr(event.event, self.data.base_date_name)
or event.date_from
)
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
return new_date.date()
def datetime(self, event) -> datetime.datetime:
from .models import SubEvent
if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data
else:
tz = pytz.timezone(event.settings.timezone)
if isinstance(event, SubEvent):
base_date = (
getattr(event, self.data.base_date_name)
or getattr(event.event, self.data.base_date_name)
or event.date_from
)
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
if self.data.time:
new_date = new_date.replace(
hour=self.data.time.hour,
minute=self.data.time.minute,
second=self.data.time.second
)
return new_date
def to_string(self) -> str:
if isinstance(self.data, (datetime.datetime, datetime.date)):
return self.data.isoformat()
else:
return 'RELDATE/{}/{}/{}/'.format( #
self.data.days_before,
self.data.time.strftime('%H:%M:%S') if self.data.time else '-',
self.data.base_date_name
)
@classmethod
def from_string(cls, input: str):
if input.startswith('RELDATE/'):
parts = input.split('/')
if parts[2] == '-':
time = None
else:
timeparts = parts[2].split(':')
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
data = RelativeDate(
days_before=int(parts[1]),
base_date_name=parts[3],
time=time
)
else:
data = parser.parse(input)
return RelativeDateWrapper(data)
class RelativeDateTimeWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/reldatetime.html'
def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices')
widgets = (
forms.RadioSelect(choices=self.status_choices),
forms.DateTimeInput(
attrs={'class': 'datetimepicker'}
),
forms.NumberInput(),
forms.Select(choices=kwargs.pop('base_choices')),
forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'})
)
super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value):
if isinstance(value, str):
value = RelativeDateWrapper.from_string(value)
if not value:
return ['unset', None, 1, 'date_from', None]
elif isinstance(value.data, (datetime.datetime, datetime.date)):
return ['absolute', value.data, 1, 'date_from', None]
return ['relative', None, value.data.days_before, value.data.base_date_name, value.data.time]
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['required'] = self.status_choices[0][0] == 'unset'
return ctx
class RelativeDateTimeField(forms.MultiValueField):
def __init__(self, *args, **kwargs):
status_choices = [
('absolute', _('Fixed date:')),
('relative', _('Relative date:')),
]
if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set')))
fields = (
forms.ChoiceField(
choices=status_choices,
required=True
),
forms.DateTimeField(
required=False
),
forms.IntegerField(
required=False
),
forms.ChoiceField(
choices=BASE_CHOICES,
required=False
),
forms.TimeField(
required=False,
),
)
if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
def set_event(self, event):
self.widget.widgets[3].choices = [
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
]
def compress(self, data_list):
if not data_list:
return None
if data_list[0] == 'absolute':
return RelativeDateWrapper(data_list[1])
elif data_list[0] == 'unset':
return None
else:
return RelativeDateWrapper(RelativeDate(
days_before=data_list[2],
base_date_name=data_list[3],
time=data_list[4]
))
def clean(self, value):
if value[0] == 'absolute' and not value[1]:
raise ValidationError(self.error_messages['incomplete'])
elif value[0] == 'relative' and (value[2] is None or not value[3]):
raise ValidationError(self.error_messages['incomplete'])
return super().clean(value)
class RelativeDateWidget(RelativeDateTimeWidget):
template_name = 'pretixbase/forms/widgets/reldate.html'
def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices')
widgets = (
forms.RadioSelect(choices=self.status_choices),
forms.DateInput(
attrs={'class': 'datepickerfield'}
),
forms.NumberInput(),
forms.Select(choices=kwargs.pop('base_choices')),
)
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
def decompress(self, value):
if isinstance(value, str):
value = RelativeDateWrapper.from_string(value)
if not value:
return ['unset', None, 1, 'date_from']
elif isinstance(value.data, (datetime.datetime, datetime.date)):
return ['absolute', value.data, 1, 'date_from']
return ['relative', None, value.data.days_before, value.data.base_date_name]
class RelativeDateField(RelativeDateTimeField):
def __init__(self, *args, **kwargs):
status_choices = [
('absolute', _('Fixed date:')),
('relative', _('Relative date:')),
]
if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set')))
fields = (
forms.ChoiceField(
choices=status_choices,
required=True
),
forms.DateField(
required=False
),
forms.IntegerField(
required=False
),
forms.ChoiceField(
choices=BASE_CHOICES,
required=False
),
)
if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
forms.MultiValueField.__init__(
self, fields=fields, require_all_fields=False, *args, **kwargs
)
def compress(self, data_list):
if not data_list:
return None
if data_list[0] == 'absolute':
return RelativeDateWrapper(data_list[1])
elif data_list[0] == 'unset':
return None
else:
return RelativeDateWrapper(RelativeDate(
days_before=data_list[2],
base_date_name=data_list[3],
time=None
))
def clean(self, value):
if value[0] == 'absolute' and not value[1]:
raise ValidationError(self.error_messages['incomplete'])
elif value[0] == 'relative' and (value[2] is None or not value[3]):
raise ValidationError(self.error_messages['incomplete'])
return super().clean(value)

View File

@@ -7,16 +7,15 @@ from celery.exceptions import MaxRetriesExceededError
from django.db import transaction
from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext as _
from django.utils.translation import ugettext as _
from pretix.base.decimal import round_decimal
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
CartPosition, Event, Item, ItemVariation, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.async import ProfiledTask
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.pricing import get_price
from pretix.celery_app import app
@@ -29,7 +28,6 @@ error_messages = {
'server was too busy. Please try again.'),
'empty': _('You did not select any products.'),
'unknown_position': _('Unknown cart position.'),
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
'not_for_sale': _('You selected a product which is not available for sale.'),
'unavailable': _('Some of the products you selected are no longer available. '
'Please see below for details.'),
@@ -41,11 +39,7 @@ error_messages = {
'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than "
"%(min)s items of it."),
'not_started': _('The presale period for this event has not yet started.'),
'ended': _('The presale period for this event has ended.'),
'some_subevent_not_started': _('The presale period for this event has not yet started. The affected positions '
'have been removed from your cart.'),
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'ended': _('The presale period has ended.'),
'price_too_high': _('The entered price is to high.'),
'voucher_invalid': _('This voucher code is not known in our database.'),
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
@@ -54,9 +48,7 @@ error_messages = {
'cart if you want to use it for a different product.'),
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
'voucher_required': _('You need a valid voucher code to order this product.'),
'inactive_subevent': pgettext_lazy('subevent', 'The selected event date is not active.'),
'addon_invalid_base': _('You can not select an add-on for the selected product.'),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'),
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
@@ -68,10 +60,10 @@ error_messages = {
class CartManager:
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
'addon_to', 'subevent'))
'addon_to'))
RemoveOperation = namedtuple('RemoveOperation', ('position',))
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
'quotas', 'subevent'))
'quotas'))
order = {
RemoveOperation: 10,
ExtendOperation: 20,
@@ -86,7 +78,6 @@ class CartManager:
self._quota_diff = Counter()
self._voucher_use_diff = Counter()
self._items_cache = {}
self._subevents_cache = {}
self._variations_cache = {}
self._expiry = None
@@ -94,7 +85,7 @@ class CartManager:
def positions(self):
return CartPosition.objects.filter(
Q(cart_id=self.cart_id) & Q(event=self.event)
).select_related('item', 'subevent')
).select_related('item')
def _calculate_expiry(self):
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
@@ -110,41 +101,31 @@ class CartManager:
# We can extend the reservation of items which are not yet expired without risk
self.positions.filter(expires__gt=self.now_dt).update(expires=self._expiry)
def _delete_out_of_timeframe(self):
err = None
for cp in self.positions:
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start:
err = error_messages['some_subevent_not_started']
def _delete_expired(self, expired: List[CartPosition]):
for cp in expired:
if cp.expires <= self.now_dt:
cp.delete()
if cp.subevent and cp.subevent.presale_end and self.now_dt > cp.subevent.presale_end:
err = error_messages['some_subevent_ended']
cp.delete()
return err
def _update_subevents_cache(self, se_ids: List[int]):
self._subevents_cache.update({
i.pk: i
for i in self.event.subevents.filter(id__in=[i for i in se_ids if i and i not in self._items_cache])
})
def _update_items_cache(self, item_ids: List[int], variation_ids: List[int]):
self._items_cache.update({
i.pk: i
for i in self.event.items.select_related('category').prefetch_related(
'addons', 'addons__addon_category', 'quotas'
).filter(
id__in=[i for i in item_ids if i and i not in self._items_cache]
)
})
self._variations_cache.update({
v.pk: v
for v in ItemVariation.objects.filter(item__event=self.event).prefetch_related(
'quotas'
).select_related('item', 'item__event').filter(
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
)
})
self._items_cache.update(
{
i.pk: i
for i
in self.event.items.select_related('category').prefetch_related(
'addons', 'addons__addon_category', 'quotas'
).filter(
id__in=[i for i in item_ids if i and i not in self._items_cache]
)
}
)
self._variations_cache.update(
{v.pk: v for v in
ItemVariation.objects.filter(item__event=self.event).prefetch_related(
'quotas'
).select_related('item', 'item__event').filter(
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
)}
)
def _check_max_cart_size(self):
cartsize = self.positions.filter(addon_to__isnull=True).count()
@@ -169,18 +150,6 @@ class CartManager:
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
raise CartError(error_messages['voucher_invalid_item'])
if op.voucher and op.voucher.subevent_id and op.voucher.subevent_id != op.subevent.pk:
raise CartError(error_messages['voucher_invalid_subevent'])
if op.subevent and not op.subevent.active:
raise CartError(error_messages['inactive_subevent'])
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start:
raise CartError(error_messages['not_started'])
if op.subevent and op.subevent.presale_end and self.now_dt > op.subevent.presale_end:
raise CartError(error_messages['ended'])
if isinstance(op, self.AddOperation):
if op.item.category and op.item.category.is_addon and not op.addon_to:
raise CartError(error_messages['addon_only'])
@@ -212,24 +181,34 @@ class CartManager:
)
def _get_price(self, item: Item, variation: Optional[ItemVariation],
voucher: Optional[Voucher], custom_price: Optional[Decimal],
subevent: Optional[SubEvent]):
return get_price(item, variation, voucher, custom_price, subevent, self.event.settings.display_net_prices)
voucher: Optional[Voucher], custom_price: Optional[Decimal]):
price = item.default_price if variation is None else (
variation.default_price if variation.default_price is not None else item.default_price
)
if voucher:
price = voucher.calculate_price(price)
if item.free_price and custom_price is not None and custom_price != "":
if not isinstance(custom_price, Decimal):
custom_price = Decimal(custom_price.replace(",", "."))
if custom_price > 100000000:
raise CartError(error_messages['price_too_high'])
if self.event.settings.display_net_prices:
custom_price = round_decimal(custom_price * (100 + item.tax_rate) / 100)
price = max(custom_price, price)
return price
def extend_expired_positions(self):
expired = self.positions.filter(expires__lte=self.now_dt).select_related(
'item', 'variation', 'voucher'
).prefetch_related('item__quotas', 'variation__quotas')
err = None
for cp in expired:
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent)
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price)
quotas = list(cp.quotas)
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
if not quotas:
self._operations.append(self.RemoveOperation(position=cp))
continue
err = error_messages['unavailable']
raise CartError(error_messages['unavailable'])
if not cp.voucher or (not cp.voucher.allow_ignore_quota and not cp.voucher.block_quota):
for quota in quotas:
self._quota_diff[quota] += 1
@@ -238,7 +217,7 @@ class CartManager:
op = self.ExtendOperation(
position=cp, item=cp.item, variation=cp.variation, voucher=cp.voucher, count=1,
price=price, quotas=quotas, subevent=cp.subevent
price=price, quotas=quotas
)
self._check_item_constraints(op)
@@ -246,12 +225,10 @@ class CartManager:
self._voucher_use_diff[cp.voucher] += 1
self._operations.append(op)
return err
def add_new_items(self, items: List[dict]):
# Fetch items from the database
self._update_items_cache([i['item'] for i in items], [i['variation'] for i in items])
self._update_subevents_cache([i['subevent'] for i in items if i.get('subevent')])
quota_diff = Counter()
voucher_use_diff = Counter()
operations = []
@@ -263,13 +240,6 @@ class CartManager:
if i['item'] not in self._items_cache or (i['variation'] and i['variation'] not in self._variations_cache):
raise CartError(error_messages['not_for_sale'])
if self.event.has_subevents:
if not i.get('subevent'):
raise CartError(error_messages['subevent_required'])
subevent = self._subevents_cache[int(i.get('subevent'))]
else:
subevent = None
item = self._items_cache[i['item']]
variation = self._variations_cache[i['variation']] if i['variation'] is not None else None
voucher = None
@@ -283,8 +253,8 @@ class CartManager:
voucher_use_diff[voucher] += i['count']
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
quotas = list(item.quotas.filter(subevent=subevent)
if variation is None else variation.quotas.filter(subevent=subevent))
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
if not quotas:
raise CartError(error_messages['unavailable'])
if not voucher or (not voucher.allow_ignore_quota and not voucher.block_quota):
@@ -293,10 +263,10 @@ class CartManager:
else:
quotas = []
price = self._get_price(item, variation, voucher, i.get('price'), subevent)
price = self._get_price(item, variation, voucher, i.get('price'))
op = self.AddOperation(
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
addon_to=False, subevent=subevent
addon_to=False
)
self._check_item_constraints(op)
operations.append(op)
@@ -340,7 +310,6 @@ class CartManager:
quota_diff = Counter() # Quota -> Number of usages
operations = []
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
price_included = defaultdict(dict) # CartPos -> CategoryID -> bool(price is included)
toplevel_cp = self.positions.filter(
addon_to__isnull=True
).prefetch_related(
@@ -350,7 +319,6 @@ class CartManager:
# Prefill some of the cache containers
for cp in toplevel_cp:
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
price_included[cp.pk] = {iao.addon_category_id: iao.price_included for iao in cp.item.addons.all()}
cpcache[cp.pk] = cp
current_addons[cp] = {
(a.item_id, a.variation_id): a
@@ -377,8 +345,7 @@ class CartManager:
raise CartError(error_messages['addon_invalid_base'])
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
quotas = list(item.quotas.filter(subevent=cp.subevent)
if variation is None else variation.quotas.filter(subevent=cp.subevent))
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
if not quotas:
raise CartError(error_messages['unavailable'])
@@ -394,14 +361,11 @@ class CartManager:
for quota in quotas:
quota_diff[quota] += 1
if price_included[cp.pk].get(item.category_id):
price = Decimal('0.00')
else:
price = self._get_price(item, variation, None, None, cp.subevent)
price = self._get_price(item, variation, None, None)
op = self.AddOperation(
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
addon_to=cp, subevent=cp.subevent
addon_to=cp
)
self._check_item_constraints(op)
operations.append(op)
@@ -439,7 +403,7 @@ class CartManager:
for k, v in al.items():
if k not in input_addons[cp.id]:
if v.expires > self.now_dt:
quotas = list(cp.quotas)
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
for quota in quotas:
quota_diff[quota] -= 1
@@ -559,8 +523,7 @@ class CartManager:
event=self.event, item=op.item, variation=op.variation,
price=op.price, expires=self._expiry,
cart_id=self.cart_id, voucher=op.voucher,
addon_to=op.addon_to if op.addon_to else None,
subevent=op.subevent
addon_to=op.addon_to if op.addon_to else None
))
elif isinstance(op, self.ExtendOperation):
if available_count == 1:
@@ -584,9 +547,8 @@ class CartManager:
with transaction.atomic():
self.now_dt = now_dt
self._extend_expiry_of_valid_existing_positions()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = self._perform_operations() or err
self.extend_expired_positions()
err = self._perform_operations()
if err:
raise CartError(err)
@@ -597,7 +559,8 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
Adds a list of items to a user's cart.
:param event: The event ID in question
:param items: A list of dicts with the keys item, variation, number, custom_price, voucher
:param cart_id: Session ID of a guest
:param session: Session ID of a guest
:param coupon: A coupon that should also be reeemed
:raises CartError: On any error that occured
"""
with language(locale):

View File

@@ -1,12 +1,25 @@
import copy
import tempfile
from collections import defaultdict
from decimal import Decimal
from django.contrib.staticfiles import finders
from django.core.files.base import ContentFile
from django.db import transaction
from django.db.models import Count
from django.utils import timezone
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext, ugettext as _
from i18nfield.strings import LazyI18nString
from reportlab.lib import pagesizes
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
BaseDocTemplate, Frame, NextPageTemplate, PageTemplate, Paragraph, Spacer,
Table, TableStyle,
)
from pretix.base.i18n import language
from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
@@ -37,11 +50,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
{i.name}
{i.street}
{i.zipcode} {i.city}
{country}""")
invoice.invoice_to = addr_template.format(
i=invoice.order.invoice_address,
country=invoice.order.invoice_address.country.name if invoice.order.invoice_address.country else invoice.order.invoice_address.country_old
).strip()
{i.country}""")
invoice.invoice_to = addr_template.format(i=invoice.order.invoice_address).strip()
if invoice.order.invoice_address.vat_id:
invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % invoice.order.invoice_address.vat_id
except InvoiceAddress.DoesNotExist:
@@ -51,16 +61,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.save()
invoice.lines.all().delete()
positions = list(
invoice.order.positions.select_related('addon_to', 'item', 'variation').annotate(
addon_c=Count('addons')
)
)
positions = list(invoice.order.positions.select_related('addon_to', 'item', 'variation'))
positions.sort(key=lambda p: p.sort_key)
for p in positions:
if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c:
continue
desc = str(p.item.name)
if p.variation:
desc += " - " + str(p.variation.value)
@@ -74,8 +77,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
if invoice.order.payment_fee:
InvoiceLine.objects.create(
invoice=invoice,
description=_('Payment via {method}').format(method=str(payment_provider.verbose_name)),
invoice=invoice, description=_('Payment via {method}').format(method=str(payment_provider.verbose_name)),
gross_value=invoice.order.payment_fee, tax_value=invoice.order.payment_fee_tax_value,
tax_rate=invoice.order.payment_fee_tax_rate
)
@@ -99,7 +101,6 @@ def generate_cancellation(invoice: Invoice):
cancellation = copy.copy(invoice)
cancellation.pk = None
cancellation.invoice_no = None
cancellation.prefix = None
cancellation.refers = invoice
cancellation.is_cancellation = True
cancellation.date = timezone.now().date()
@@ -129,7 +130,6 @@ def generate_invoice(order: Order):
invoice = Invoice(
order=order,
event=order.event,
organizer=order.event.organizer,
date=timezone.now().date(),
locale=locale
)
@@ -138,12 +138,265 @@ def generate_invoice(order: Order):
return invoice
def _invoice_get_stylesheet():
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName='OpenSans', fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Heading1', fontName='OpenSansBd', fontSize=15, leading=15 * 1.2))
return stylesheet
def _invoice_register_fonts():
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
def _invoice_generate_german(invoice, f):
_invoice_register_fonts()
styles = _invoice_get_stylesheet()
pagesize = pagesizes.A4
def on_page(canvas, doc):
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(pagesize[0] - 20 * mm, 10 * mm, _("Page %d") % (doc.page,))
for i, line in enumerate(invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.restoreState()
def on_first_page(canvas, doc):
canvas.setCreator('pretix.eu')
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=invoice.number))
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(pagesize[0] - 20 * mm, 10 * mm, _("Page %d") % (doc.page,))
for i, line in enumerate(invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
canvas.drawText(textobject)
p = Paragraph(invoice.invoice_from.strip().replace('\n', '<br />\n'), style=styles['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
canvas.drawText(textobject)
p = Paragraph(invoice.invoice_to.strip().replace('\n', '<br />\n'), style=styles['Normal'])
p.wrapOn(canvas, 85 * mm, 50 * mm)
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Order code').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.order.full_code)
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
if invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.number)
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.refers.number)
else:
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.number)
textobject.moveCursor(0, 5)
if invoice.is_cancellation:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(invoice.refers.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
else:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
canvas.drawText(textobject)
if invoice.event.settings.invoice_logo_image:
logo_file = invoice.event.settings.get('invoice_logo_image', binary_file=True)
canvas.drawImage(ImageReader(logo_file),
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n',
mask='auto')
if invoice.event.settings.show_date_to:
p_str = (
str(invoice.event.name) + '\n' + _('{from_date}\nuntil {to_date}').format(
from_date=invoice.event.get_date_from_display(),
to_date=invoice.event.get_date_to_display())
)
else:
p_str = (
str(invoice.event.name) + '\n' + invoice.event.get_date_from_display()
)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=styles['Normal'])
p.wrapOn(canvas, 65 * mm, 50 * mm)
p_size = p.wrap(65 * mm, 50 * mm)
p.drawOn(canvas, 125 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Event').upper())
canvas.drawText(textobject)
canvas.restoreState()
doc = BaseDocTemplate(f.name, pagesize=pagesizes.A4,
leftMargin=25 * mm, rightMargin=20 * mm,
topMargin=20 * mm, bottomMargin=15 * mm)
footer_length = 3.5 * len(invoice.footer_text.split('\n')) * mm
frames_p1 = [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
frames = [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
doc.addPageTemplates([
PageTemplate(id='FirstPage', frames=frames_p1, onPage=on_first_page, pagesize=pagesize),
PageTemplate(id='OtherPages', frames=frames, onPage=on_page, pagesize=pagesize)
])
story = [
NextPageTemplate('FirstPage'),
Paragraph(pgettext('invoice', 'Invoice')
if not invoice.is_cancellation
else pgettext('invoice', 'Cancellation'),
styles['Heading1']),
Spacer(1, 5 * mm),
NextPageTemplate('OtherPages'),
]
if invoice.introductory_text:
story.append(Paragraph(invoice.introductory_text, styles['Normal']))
story.append(Spacer(1, 10 * mm))
taxvalue_map = defaultdict(Decimal)
grossvalue_map = defaultdict(Decimal)
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
]
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net'),
pgettext('invoice', 'Gross'),
)]
total = Decimal('0.00')
for line in invoice.lines.all():
tdata.append((
Paragraph(line.description, styles['Normal']),
localize(line.tax_rate) + " %",
localize(line.net_value) + " " + invoice.event.currency,
localize(line.gross_value) + " " + invoice.event.currency,
))
taxvalue_map[line.tax_rate] += line.tax_value
grossvalue_map[line.tax_rate] += line.gross_value
total += line.gross_value
tdata.append([pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + invoice.event.currency])
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
story.append(table)
story.append(Spacer(1, 15 * mm))
if invoice.payment_provider_text:
story.append(Paragraph(invoice.payment_provider_text, styles['Normal']))
if invoice.additional_text:
story.append(Paragraph(invoice.additional_text, styles['Normal']))
story.append(Spacer(1, 15 * mm))
tstyledata = [
('SPAN', (1, 0), (-1, 0)),
('ALIGN', (2, 1), (-1, -1), 'RIGHT'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
('FONTSIZE', (0, 0), (-1, -1), 8),
]
tdata = [('', pgettext('invoice', 'Included taxes'), '', '', ''),
('', pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net value'), pgettext('invoice', 'Gross value'), pgettext('invoice', 'Tax'))]
for rate, gross in grossvalue_map.items():
if line.tax_rate == 0:
continue
tax = taxvalue_map[rate]
tdata.append((
'',
localize(rate) + " %",
localize((gross - tax)) + " " + invoice.event.currency,
localize(gross) + " " + invoice.event.currency,
localize(tax) + " " + invoice.event.currency,
))
if len(tdata) > 2:
colwidths = [a * doc.width for a in (.45, .10, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=2)
table.setStyle(TableStyle(tstyledata))
story.append(table)
doc.build(story)
return doc
@app.task(base=TransactionAwareTask)
def invoice_pdf_task(invoice: int):
i = Invoice.objects.get(pk=invoice)
with language(i.locale):
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
i.file.save(fname, ContentFile(fcontent))
with tempfile.NamedTemporaryFile(suffix=".pdf") as f:
_invoice_generate_german(i, f)
f.seek(0)
i.file.save('invoice.pdf', ContentFile(f.read()))
i.save()
return i.file.name
@@ -176,7 +429,7 @@ def build_preview_invoice_pdf(event):
expires=timezone.now(), code="PREVIEW", total=119)
invoice = Invoice(
order=order, event=event, invoice_no="PREVIEW",
date=timezone.now().date(), locale=locale, organizer=event.organizer
date=timezone.now().date(), locale=locale
)
invoice.invoice_from = event.settings.get('invoice_address_from')
@@ -199,4 +452,7 @@ def build_preview_invoice_pdf(event):
gross_value=119, tax_value=19,
tax_rate=19
)
return event.invoice_renderer.generate(invoice)
with tempfile.NamedTemporaryFile(suffix=".pdf") as f:
_invoice_generate_german(invoice, f)
f.seek(0)
return f.read()

View File

@@ -33,7 +33,7 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, headers: dict=None, sender: str=None):
order: Order=None, headers: dict=None):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -58,9 +58,6 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param locale: The locale to be used while evaluating the subject and the template
:param sender: Set the sender email address. If not set and ``event`` is set, the event's default will be used,
otherwise the system default.
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
@@ -81,8 +78,19 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
'invoice_name': '',
'invoice_company': ''
})
body, body_md = render_mail(template, context)
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = body.format_map(TolerantDict(context))
body_md = bleach.linkify(bleach.clean(markdown.markdown(body), tags=bleach.ALLOWED_TAGS + [
'p',
]))
else:
tpl = get_template(template)
body = tpl.render(context)
body_md = bleach.linkify(markdown.markdown(body))
sender = event.settings.get('mail_from') if event else settings.MAIL_FROM
subject = str(subject)
body_plain = body
@@ -159,18 +167,3 @@ def mail_send_task(to: List[str], subject: str, body: str, html: str, sender: st
def mail_send(*args, **kwargs):
mail_send_task.apply_async(args=args, kwargs=kwargs)
def render_mail(template, context):
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = body.format_map(TolerantDict(context))
body_md = bleach.linkify(bleach.clean(markdown.markdown(body), tags=bleach.ALLOWED_TAGS + [
'p',
]))
else:
tpl = get_template(template)
body = tpl.render(context)
body_md = bleach.linkify(markdown.markdown(body))
return body, body_md

View File

@@ -22,17 +22,14 @@ from pretix.base.models import (
CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota,
User, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import CachedTicket, InvoiceAddress
from pretix.base.payment import BasePaymentProvider
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.async import ProfiledTask
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.services.mail import SendMailException, mail
from pretix.base.signals import order_paid, order_placed, periodic_task
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -61,10 +58,6 @@ error_messages = {
'removed this item from your cart.'),
'voucher_required': _('You need a valid voucher code to order one of the products in your cart. We removed this '
'item from your cart.'),
'some_subevent_not_started': _('The presale period for one of the events in your cart has not yet started. The '
'affected positions have been removed from your cart.'),
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
}
logger = logging.getLogger(__name__)
@@ -129,27 +122,22 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = order.event.settings.mail_text_order_paid
email_context = {
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'downloads': order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user
)
except SendMailException:
logger.exception('Order paid email could not be sent')
mail(
order.email, _('Payment received for your order: %(code)s') % {'code': order.code},
order.event.settings.mail_text_order_paid,
{
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'downloads': order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
},
order.event, locale=order.locale
)
return order
@@ -177,7 +165,7 @@ def mark_order_refunded(order, user=None):
@transaction.atomic
def _cancel_order(order, user=None, send_mail: bool=True):
def _cancel_order(order, user=None):
"""
Mark this order as canceled
:param order: The order to change
@@ -202,26 +190,6 @@ def _cancel_order(order, user=None, send_mail: bool=True):
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=F('redeemed') - 1)
if send_mail:
email_template = order.event.settings.mail_text_order_canceled
email_context = {
'event': order.event.name,
'code': order.code,
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
})
}
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
with language(order.locale):
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_canceled', user
)
except SendMailException:
logger.exception('Order canceled email could not be sent')
return order.pk
@@ -247,7 +215,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
err = err or error_messages['unavailable']
cp.delete()
continue
quotas = list(cp.quotas)
quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all())
products_seen[cp.item] += 1
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
@@ -267,19 +235,9 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
cp.delete() # Sorry!
continue
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
err = err or error_messages['some_subevent_not_started']
cp.delete()
break
if cp.subevent and cp.subevent.presale_end and now_dt > cp.subevent.presale_end:
err = err or error_messages['some_subevent_ended']
cp.delete()
break
if cp.item.require_voucher and cp.voucher is None:
cp.delete()
err = err or error_messages['voucher_required']
err = error_messages['voucher_required']
break
if cp.item.hide_without_voucher and (cp.voucher is None or cp.voucher.item is None
@@ -292,8 +250,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
# Other checks are not necessary
continue
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
addon_to=cp.addon_to)
price = cp.item.default_price if cp.variation is None else (
cp.variation.default_price if cp.variation.default_price is not None else cp.item.default_price)
if price is False or len(quotas) == 0:
err = err or error_messages['unavailable']
@@ -305,6 +263,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
err = err or error_messages['voucher_expired']
cp.delete()
continue
price = cp.voucher.calculate_price(price)
if price != cp.price and not (cp.item.free_price and cp.price > price):
positions[i] = cp
@@ -343,7 +302,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
meta_info: dict=None):
from datetime import time
from datetime import date, time
total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total)
@@ -360,21 +319,13 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
expires = exp_by_date
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if event.has_subevents:
term_last = min([
term_last.datetime(se).date()
for se in event.subevents.filter(id__in=[p.subevent_id for p in positions])
])
else:
term_last = term_last.datetime(event).date()
term_last = make_aware(datetime.combine(
term_last,
if event.settings.get('payment_term_last'):
last_date = make_aware(datetime.combine(
event.settings.get('payment_term_last', as_type=date),
time(hour=23, minute=59, second=59)
), tz)
if term_last < expires:
expires = term_last
if last_date < expires:
expires = last_date
with transaction.atomic():
order = Order.objects.create(
@@ -419,7 +370,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
with event.lock() as now_dt:
positions = list(CartPosition.objects.filter(
id__in=position_ids).select_related('item', 'variation', 'subevent'))
id__in=position_ids).select_related('item', 'variation'))
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
@@ -433,11 +384,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
generate_invoice(order)
if order.total == Decimal('0.00'):
email_template = event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
mailtext = event.settings.mail_text_order_free
else:
email_template = event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
mailtext = event.settings.mail_text_order_placed
try:
invoice_name = order.invoice_address.name
@@ -445,27 +394,25 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'payment_info': str(pprov.order_pending_mail_render(order)),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry
)
except SendMailException:
logger.exception('Order received email could not be sent')
mail(
order.email, _('Your order: %(code)s') % {'code': order.code},
mailtext,
{
'total': LazyNumber(order.total),
'currency': event.currency,
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'payment_info': str(pprov.order_pending_mail_render(order)),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
event, locale=order.locale
)
return order.id
@@ -507,25 +454,27 @@ def send_expiry_warnings(sender, **kwargs):
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = eventsettings.mail_text_order_expire_warning
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order is about to expire: %(code)s') % {'code': o.code}
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.expire_warning_sent'
)
with language(o.locale):
mail(
o.email, _('Your order is about to expire: %(code)s') % {'code': o.code},
eventsettings.mail_text_order_expire_warning,
{
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
o.event, locale=o.locale
)
except SendMailException:
logger.exception('Reminder email could not be sent')
else:
o.log_action('pretix.event.order.expire_warning_sent')
class OrderChangeManager:
@@ -533,7 +482,6 @@ class OrderChangeManager:
'free_to_paid': _('You cannot change a free order to a paid order.'),
'product_without_variation': _('You need to select a variation of the product.'),
'quota': _('The quota {name} does not have enough capacity left to perform the operation.'),
'quota_missing': _('There is no quota defined that allows this operation.'),
'product_invalid': _('The selected product is not active or has no price set.'),
'complete_cancel': _('This operation would leave the order empty. Please cancel the order itself instead.'),
'not_pending_or_paid': _('Only pending or paid orders can be changed.'),
@@ -543,13 +491,11 @@ class OrderChangeManager:
'price of the order as partial payments or refunds are not yet supported.'),
'addon_to_required': _('This is an addon product, please select the base position it should be added to.'),
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
'subevent_required': _('You need to choose a subevent for the new position.'),
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent', 'price'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
CancelOperation = namedtuple('CancelOperation', ('position',))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to'))
def __init__(self, order: Order, user):
self.order = order
@@ -557,69 +503,30 @@ class OrderChangeManager:
self._totaldiff = 0
self._quotadiff = Counter()
self._operations = []
self._invoice_dirty = False
def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]):
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
raise OrderError(self.error_messages['product_without_variation'])
price = get_price(item, variation, voucher=position.voucher, subevent=position.subevent)
if price is None: # NOQA
price = item.default_price if variation is None else variation.price
if price is None:
raise OrderError(self.error_messages['product_invalid'])
new_quotas = (variation.quotas.filter(subevent=position.subevent)
if variation else item.quotas.filter(subevent=position.subevent))
if not new_quotas:
raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff = price - position.price
self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas)
self._quotadiff.update(variation.quotas.all() if variation else item.quotas.all())
self._quotadiff.subtract(position.variation.quotas.all() if position.variation else position.item.quotas.all())
self._operations.append(self.ItemOperation(position, item, variation, price))
def change_subevent(self, position: OrderPosition, subevent: SubEvent):
price = get_price(position.item, position.variation, voucher=position.voucher, subevent=subevent)
if price is None: # NOQA
raise OrderError(self.error_messages['product_invalid'])
new_quotas = (position.variation.quotas.filter(subevent=subevent)
if position.variation else position.item.quotas.filter(subevent=subevent))
if not new_quotas:
raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff = price - position.price
self._quotadiff.update(new_quotas)
self._quotadiff.subtract(position.quotas)
self._operations.append(self.SubeventOperation(position, subevent, price))
def change_price(self, position: OrderPosition, price: Decimal):
self._totaldiff = price - position.price
if self.order.event.settings.invoice_include_free or price != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._operations.append(self.PriceOperation(position, price))
def cancel(self, position: OrderPosition):
self._totaldiff = -position.price
self._quotadiff.subtract(position.quotas)
self._quotadiff.subtract(position.variation.quotas.all() if position.variation else position.item.quotas.all())
self._operations.append(self.CancelOperation(position))
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
self._invoice_dirty = True
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order = None,
subevent: SubEvent = None):
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order):
if price is None:
price = get_price(item, variation, subevent=subevent)
price = item.default_price if variation is None else variation.price
if price is None:
raise OrderError(self.error_messages['product_invalid'])
if not addon_to and item.category and item.category.is_addon:
@@ -627,20 +534,10 @@ class OrderChangeManager:
if addon_to:
if not item.category or item.category_id not in addon_to.item.addons.values_list('addon_category', flat=True):
raise OrderError(self.error_messages['addon_invalid'])
if self.order.event.has_subevents and not subevent:
raise OrderError(self.error_messages['subevent_required'])
new_quotas = (variation.quotas.filter(subevent=subevent)
if variation else item.quotas.filter(subevent=subevent))
if not new_quotas:
raise OrderError(self.error_messages['quota_missing'])
if self.order.event.settings.invoice_include_free or price != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff = price
self._quotadiff.update(new_quotas)
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent))
self._quotadiff.update(variation.quotas.all() if variation else item.quotas.all())
self._operations.append(self.AddOperation(item, variation, price, addon_to))
def _check_quotas(self):
for quota, diff in self._quotadiff.items():
@@ -685,19 +582,6 @@ class OrderChangeManager:
op.position.price = op.price
op.position._calculate_tax()
op.position.save()
elif isinstance(op, self.SubeventOperation):
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, data={
'position': op.position.pk,
'positionid': op.position.positionid,
'old_subevent': op.position.subevent.pk,
'new_subevent': op.subevent.pk,
'old_price': op.position.price,
'new_price': op.price
})
op.position.subevent = op.subevent
op.position.price = op.price
op.position._calculate_tax()
op.position.save()
elif isinstance(op, self.PriceOperation):
self.order.log_action('pretix.event.order.changed.price', user=self.user, data={
'position': op.position.pk,
@@ -732,7 +616,7 @@ class OrderChangeManager:
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
price=op.price, order=self.order,
positionid=nextposid, subevent=op.subevent
positionid=nextposid
)
nextposid += 1
self.order.log_action('pretix.event.order.changed.add', user=self.user, data={
@@ -741,8 +625,7 @@ class OrderChangeManager:
'variation': op.variation.pk if op.variation else None,
'addon_to': op.addon_to.pk if op.addon_to else None,
'price': op.price,
'positionid': pos.positionid,
'subevent': op.subevent.pk if op.subevent else None,
'positionid': pos.positionid
})
def _recalculate_total_and_payment_fee(self):
@@ -759,7 +642,7 @@ class OrderChangeManager:
def _reissue_invoice(self):
i = self.order.invoices.filter(is_cancellation=False).last()
if i and self._invoice_dirty:
if i:
generate_cancellation(i)
generate_invoice(self.order)
@@ -776,24 +659,20 @@ class OrderChangeManager:
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_changed
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order has been changed: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_changed', self.user
)
except SendMailException:
logger.exception('Order changed email could not be sent')
mail(
self.order.email, _('Your order has been changed: %(code)s') % {'code': self.order.code},
self.order.event.settings.mail_text_order_changed,
{
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
},
self.order.event, locale=self.order.locale
)
def commit(self):
if not self._operations:
@@ -838,10 +717,10 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_order(self, order: int, user: int=None, send_mail: bool=True):
def cancel_order(self, order: int, user: int=None):
try:
try:
return _cancel_order(order, user, send_mail)
return _cancel_order(order, user)
except LockTimeoutException:
self.retry(exc=OrderError(error_messages['busy']))
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -1,44 +0,0 @@
from decimal import Decimal
from pretix.base.decimal import round_decimal
from pretix.base.models import (
AbstractPosition, Item, ItemAddOn, ItemVariation, Voucher,
)
from pretix.base.models.event import SubEvent
def get_price(item: Item, variation: ItemVariation = None,
voucher: Voucher = None, custom_price: Decimal = None,
subevent: SubEvent = None, custom_price_is_net: bool = False,
addon_to: AbstractPosition = None):
if addon_to:
try:
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
if iao.price_included:
return Decimal('0.00')
except ItemAddOn.DoesNotExist:
pass
price = item.default_price
if subevent and item.pk in subevent.item_price_overrides:
price = subevent.item_price_overrides[item.pk]
if variation is not None:
if variation.default_price is not None:
price = variation.default_price
if subevent and variation.pk in subevent.var_price_overrides:
price = subevent.var_price_overrides[variation.pk]
if voucher:
price = voucher.calculate_price(price)
if item.free_price and custom_price is not None and custom_price != "":
if not isinstance(custom_price, Decimal):
custom_price = Decimal(str(custom_price).replace(",", "."))
if custom_price > 100000000:
raise ValueError('price_too_high')
if custom_price_is_net:
custom_price = round_decimal(custom_price * (100 + item.tax_rate) / 100)
price = max(custom_price, price)
return price

View File

@@ -5,7 +5,6 @@ from django.db.models import Count, Sum
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
from pretix.base.models.event import SubEvent
class DummyObject:
@@ -68,18 +67,14 @@ def dictsum(*dicts) -> dict:
return res
def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[ItemCategory, List[Item]]],
Dict[str, Tuple[Decimal, Decimal]]]:
def order_overview(event: Event) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
).prefetch_related(
'variations'
).order_by('category__position', 'category_id', 'position', 'name')
).order_by('category__position', 'category_id', 'name')
qs = OrderPosition.objects
if subevent:
qs = qs.filter(subevent=subevent)
counters = qs.filter(
counters = OrderPosition.objects.filter(
order__event=event
).values(
'item', 'variation', 'order__status'
@@ -160,72 +155,71 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It
payment_cat_obj.name = _('Payment method fees')
payment_items = []
if not subevent:
counters = event.orders.values('payment_provider', 'status').annotate(
cnt=Count('id'), payment_fee=Sum('payment_fee'), tax_value=Sum('payment_fee_tax_value')
).order_by()
counters = event.orders.values('payment_provider', 'status').annotate(
cnt=Count('id'), payment_fee=Sum('payment_fee'), tax_value=Sum('payment_fee_tax_value')
).order_by()
num_canceled = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_CANCELED
}
num_refunded = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_REFUNDED
}
num_pending = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_PENDING
}
num_expired = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_EXPIRED
}
num_paid = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_PAID
}
num_total = dictsum(num_pending, num_paid)
num_canceled = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_CANCELED
}
num_refunded = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_REFUNDED
}
num_pending = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_PENDING
}
num_expired = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_EXPIRED
}
num_paid = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_PAID
}
num_total = dictsum(num_pending, num_paid)
provider_names = {
k: v.verbose_name
for k, v in event.get_payment_providers().items()
}
provider_names = {
k: v.verbose_name
for k, v in event.get_payment_providers().items()
}
for pprov, total in num_total.items():
ppobj = DummyObject()
ppobj.name = provider_names.get(pprov, pprov)
ppobj.provider = pprov
ppobj.has_variations = False
ppobj.num_total = total
ppobj.num_canceled = num_canceled.get(pprov, (0, 0, 0))
ppobj.num_refunded = num_refunded.get(pprov, (0, 0, 0))
ppobj.num_expired = num_expired.get(pprov, (0, 0, 0))
ppobj.num_pending = num_pending.get(pprov, (0, 0, 0))
ppobj.num_paid = num_paid.get(pprov, (0, 0, 0))
payment_items.append(ppobj)
for pprov, total in num_total.items():
ppobj = DummyObject()
ppobj.name = provider_names.get(pprov, pprov)
ppobj.provider = pprov
ppobj.has_variations = False
ppobj.num_total = total
ppobj.num_canceled = num_canceled.get(pprov, (0, 0, 0))
ppobj.num_refunded = num_refunded.get(pprov, (0, 0, 0))
ppobj.num_expired = num_expired.get(pprov, (0, 0, 0))
ppobj.num_pending = num_pending.get(pprov, (0, 0, 0))
ppobj.num_paid = num_paid.get(pprov, (0, 0, 0))
payment_items.append(ppobj)
payment_cat_obj.num_total = (
Dontsum(''), sum(i.num_total[1] for i in payment_items), sum(i.num_total[2] for i in payment_items)
)
payment_cat_obj.num_canceled = (
Dontsum(''), sum(i.num_canceled[1] for i in payment_items), sum(i.num_canceled[2] for i in payment_items)
)
payment_cat_obj.num_refunded = (
Dontsum(''), sum(i.num_refunded[1] for i in payment_items), sum(i.num_refunded[2] for i in payment_items)
)
payment_cat_obj.num_expired = (
Dontsum(''), sum(i.num_expired[1] for i in payment_items), sum(i.num_expired[2] for i in payment_items)
)
payment_cat_obj.num_pending = (
Dontsum(''), sum(i.num_pending[1] for i in payment_items), sum(i.num_pending[2] for i in payment_items)
)
payment_cat_obj.num_paid = (
Dontsum(''), sum(i.num_paid[1] for i in payment_items), sum(i.num_paid[2] for i in payment_items)
)
payment_cat = (payment_cat_obj, payment_items)
payment_cat_obj.num_total = (
Dontsum(''), sum(i.num_total[1] for i in payment_items), sum(i.num_total[2] for i in payment_items)
)
payment_cat_obj.num_canceled = (
Dontsum(''), sum(i.num_canceled[1] for i in payment_items), sum(i.num_canceled[2] for i in payment_items)
)
payment_cat_obj.num_refunded = (
Dontsum(''), sum(i.num_refunded[1] for i in payment_items), sum(i.num_refunded[2] for i in payment_items)
)
payment_cat_obj.num_expired = (
Dontsum(''), sum(i.num_expired[1] for i in payment_items), sum(i.num_expired[2] for i in payment_items)
)
payment_cat_obj.num_pending = (
Dontsum(''), sum(i.num_pending[1] for i in payment_items), sum(i.num_pending[2] for i in payment_items)
)
payment_cat_obj.num_paid = (
Dontsum(''), sum(i.num_paid[1] for i in payment_items), sum(i.num_paid[2] for i in payment_items)
)
payment_cat = (payment_cat_obj, payment_items)
items_by_category.append(payment_cat)
items_by_category.append(payment_cat)
total = {
'num_total': tuplesum(c.num_total for c, i in items_by_category),

View File

@@ -8,7 +8,7 @@ from pretix.celery_app import app
@app.task(base=ProfiledTask)
def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None):
def assign_automatically(event_id: int, user_id: int=None):
event = Event.objects.get(id=event_id)
if user_id:
user = User.objects.get(id=user_id)
@@ -21,24 +21,17 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
qs = WaitingListEntry.objects.filter(
event=event, voucher__isnull=True
).select_related('item', 'variation').prefetch_related('item__quotas', 'variation__quotas').order_by('created')
if subevent_id and event.has_subevents:
subevent = event.subevents.get(id=subevent_id)
qs = qs.filter(subevent=subevent)
sent = 0
for wle in qs:
if (wle.item, wle.variation) in gone:
continue
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
if wle.variation
else wle.item.quotas.filter(subevent=wle.subevent))
quotas = wle.variation.quotas.all() if wle.variation else wle.item.quotas.all()
availability = (
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache)
if wle.variation
else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache, subevent=wle.subevent)
else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache)
)
if availability[1] > 0:
try:

View File

@@ -10,8 +10,6 @@ from hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.strings import LazyI18nString
from typing import Any
from pretix.base.reldate import RelativeDateWrapper
DEFAULTS = {
'max_items_per_order': {
'default': '10',
@@ -45,10 +43,6 @@ DEFAULTS = {
'default': 'True',
'type': bool,
},
'invoice_name_required': {
'default': 'False',
'type': bool,
},
'invoice_address_required': {
'default': 'False',
'type': bool,
@@ -57,22 +51,10 @@ DEFAULTS = {
'default': 'False',
'type': bool,
},
'invoice_include_free': {
'default': 'True',
'type': bool,
},
'invoice_numbers_consecutive': {
'default': 'True',
'type': bool,
},
'invoice_numbers_prefix': {
'default': '',
'type': str,
},
'invoice_renderer': {
'default': 'classic',
'type': str,
},
'reservation_time': {
'default': '30',
'type': int
@@ -83,7 +65,7 @@ DEFAULTS = {
},
'payment_term_last': {
'default': None,
'type': RelativeDateWrapper,
'type': datetime,
},
'payment_term_weekdays': {
'default': 'True',
@@ -179,7 +161,7 @@ DEFAULTS = {
},
'ticket_download_date': {
'default': None,
'type': RelativeDateWrapper
'type': datetime
},
'ticket_download_addons': {
'default': 'False',
@@ -189,13 +171,9 @@ DEFAULTS = {
'default': 'True',
'type': bool
},
'event_list_type': {
'default': 'list',
'type': str
},
'last_order_modification_date': {
'default': None,
'type': RelativeDateWrapper
'type': datetime
},
'cancel_allow_user': {
'default': 'True',
@@ -338,28 +316,6 @@ Please note that this link is only valid within the next {hours} hours!
We will reassign the ticket to the next person on the list if you do not
redeem the voucher within that timeframe.
Best regards,
Your {event} team"""))
},
'mail_text_order_canceled': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
your order {code} for {event} has been canceled.
You can view the details of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_text_order_custom_mail': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
@@ -415,10 +371,6 @@ Your {event} team"""))
'default': '',
'type': LazyI18nString
},
'organizer_info_text': {
'default': '',
'type': LazyI18nString
},
'update_check_ack': {
'default': 'False',
'type': bool
@@ -465,9 +417,6 @@ def i18n_uns(v):
settings_hierarkey.add_type(LazyI18nString,
serialize=lambda s: json.dumps(s.data),
unserialize=i18n_uns)
settings_hierarkey.add_type(RelativeDateWrapper,
serialize=lambda rdw: rdw.to_string(),
unserialize=lambda s: RelativeDateWrapper.from_string(s))
@settings_hierarkey.set_global(cache_namespace='global')
@@ -489,9 +438,6 @@ class SettingsSandbox:
self._type = typestr
self._key = key
def get_prefix(self):
return '%s_%s_' % (self._type, self._key)
def _convert_key(self, key: str) -> str:
return '%s_%s_%s' % (self._type, self._key, key)

View File

@@ -8,15 +8,6 @@ from django.dispatch.dispatcher import NO_RECEIVERS
from .models import Event
app_cache = {}
def _populate_app_cache():
global app_cache
apps.check_apps_ready()
for ac in apps.app_configs.values():
app_cache[ac.name] = ac
class EventPluginSignal(django.dispatch.Signal):
"""
@@ -39,23 +30,23 @@ class EventPluginSignal(django.dispatch.Signal):
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return responses
if not app_cache:
_populate_app_cache()
for receiver in self._live_receivers(sender):
# Find the Django application this belongs to
searchpath = receiver.__module__
core_module = any([searchpath.startswith(cm) for cm in settings.CORE_MODULES])
app = None
if not core_module:
while True:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
searchpath, _ = searchpath.rsplit(".", 1)
mod = None
while True:
try:
if apps.is_installed(searchpath):
app = apps.get_app_config(searchpath.split(".")[-1])
except LookupError:
pass
if "." not in searchpath:
break
searchpath, mod = searchpath.rsplit(".", 1)
# Only fire receivers from active plugins and core modules
if core_module or (sender and app and app.name in sender.get_plugins()):
if (searchpath, mod) in settings.CORE_MODULES or (sender and app and app.name in sender.get_plugins()):
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
@@ -91,16 +82,6 @@ subclass of pretix.base.payment.BasePaymentProvider
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_invoice_renderers = EventPluginSignal(
providing_args=[]
)
"""
This signal is sent out to get all known invoice renderers. Receivers should return a
subclass of pretix.base.invoice.BaseInvoiceRenderer
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_ticket_outputs = EventPluginSignal(
providing_args=[]
)

View File

@@ -6,15 +6,6 @@
<h1>{% trans "Internal Server Error" %}</h1>
<p>{% trans "We had trouble processing your request." %}</p>
<p>{% trans "If this problem persists, please contact us." %}</p>
{% if request.sentry.id %}
<p>
{% blocktrans trimmed %}
If you contact us, please send us the following code:
{% endblocktrans %}
<br>
{{ request.sentry.id }}
</p>
{% endif %}
<p>{{ exception }}</p>
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>

View File

@@ -1,21 +0,0 @@
{% load i18n %}
<div class="reldatetime">
{% for group_name, group_choices, group-index in widget.subwidgets.0.optgroups %}
{% for selopt in group_choices %}
<div class="radio">
<label>
<input type="radio" name="{{ widget.subwidgets.0.name }}" value="{{ selopt.value }}"
{% include "django/forms/widgets/attrs.html" with widget=selopt %} />
{{ selopt.label }}
</label>
{% if selopt.value == "absolute" %}
{% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %}
{% elif selopt.value == "relative" %}
{% include widget.subwidgets.2.template_name with widget=widget.subwidgets.2 %}
{% trans "days before" %}
{% include widget.subwidgets.3.template_name with widget=widget.subwidgets.3 %}
{% endif %}
</div>
{% endfor %}
{% endfor %}
</div>

View File

@@ -1,23 +0,0 @@
{% load i18n %}
<div class="reldatetime">
{% for group_name, group_choices, group-index in widget.subwidgets.0.optgroups %}
{% for selopt in group_choices %}
<div class="radio">
<label>
<input type="radio" name="{{ widget.subwidgets.0.name }}" value="{{ selopt.value }}"
{% include "django/forms/widgets/attrs.html" with widget=selopt %} />
{{ selopt.label }}
</label>
{% if selopt.value == "absolute" %}
{% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %}
{% elif selopt.value == "relative" %}
{% include widget.subwidgets.2.template_name with widget=widget.subwidgets.2 %}
{% trans "days before" %}
{% include widget.subwidgets.3.template_name with widget=widget.subwidgets.3 %}
{% trans "at" %}
{% include widget.subwidgets.4.template_name with widget=widget.subwidgets.4 %}
{% endif %}
</div>
{% endfor %}
{% endfor %}
</div>

View File

@@ -0,0 +1,13 @@
from django import template
from django.template.defaultfilters import stringfilter
from pretix.helpers.escapejson import escapejson
register = template.Library()
@register.filter("escapejson")
@stringfilter
def escapejs_filter(value):
"""Hex encodes characters for use in a application/json type script."""
return escapejson(value)

View File

@@ -1,6 +1,12 @@
import urllib.parse
import bleach
import markdown
from bleach import DEFAULT_CALLBACKS
from django import template
from django.core import signing
from django.urls import reverse
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
register = template.Library()
@@ -48,6 +54,15 @@ ALLOWED_ATTRIBUTES = {
}
def safelink_callback(attrs, new=False):
url = attrs.get((None, 'href'), '/')
if not is_safe_url(url):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank'
return attrs
@register.filter
def rich_text(text: str, **kwargs):
"""
@@ -58,5 +73,5 @@ def rich_text(text: str, **kwargs):
markdown.markdown(text),
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
))
), callbacks=DEFAULT_CALLBACKS + [safelink_callback])
return mark_safe(body_md)

View File

@@ -32,26 +32,11 @@ class BaseTicketOutput:
"""
return self.settings.get('_enabled', as_type=bool)
@property
def multi_download_enabled(self) -> bool:
"""
Returns whether or not the ``generate_order`` method may be called. Returns
``True`` by default.
"""
return True
def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
"""
This method should generate the download file and return a tuple consisting of a
filename, a file type and file content. The extension will be taken from the filename
which is otherwise ignored.
.. note:: If the event uses the event series feature (internally called subevents)
and your generated ticket contains information like the event name or date,
you probably want to display the properties of the subevent. A common pattern
to do this would be a declaration ``ev = position.subevent or position.order.event``
and then access properties that are present on both classes like ``ev.name`` or
``ev.date_from``.
"""
raise NotImplementedError()

View File

@@ -31,7 +31,6 @@ class EventSlugBlacklistValidator(BlacklistValidator):
'_global',
'__debug__',
'api',
'events',
]

View File

@@ -1,8 +1,5 @@
from django.http import (
HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError,
)
from django.http import HttpResponseForbidden, HttpResponseNotFound
from django.middleware.csrf import REASON_NO_CSRF_COOKIE, REASON_NO_REFERER
from django.template import TemplateDoesNotExist, loader
from django.template.loader import get_template
from django.utils.functional import Promise
from django.utils.translation import ugettext as _
@@ -56,14 +53,3 @@ def page_not_found(request, exception):
template = get_template('404.html')
body = template.render(context, request)
return HttpResponseNotFound(body)
@requires_csrf_token
def server_error(request):
try:
template = loader.get_template('500.html')
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
return HttpResponseServerError(template.render({
'request': request
}))

View File

@@ -9,3 +9,10 @@ from django.conf import settings
app = Celery('pretix')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
if hasattr(settings, 'RAVEN_CONFIG'):
# Celery signal registration
from raven.contrib.celery import register_signal
from raven.contrib.django.models import client
register_signal(client, ignore_expected=True)

View File

@@ -74,7 +74,6 @@ def contextprocessor(request):
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
ctx['js_locale'] = get_moment_locale()
if settings.DEBUG and 'runserver' not in sys.argv:

View File

@@ -88,13 +88,3 @@ class ExtFileField(forms.FileField):
if ext not in self.ext_whitelist:
raise forms.ValidationError(_("Filetype not allowed!"))
return data
class SlugWidget(forms.TextInput):
template_name = 'pretixcontrol/slug_widget.html'
prefix = ''
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['pre'] = self.prefix
return ctx

View File

@@ -10,9 +10,7 @@ from pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.control.forms import ExtFileField, SlugWidget
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.control.forms import ExtFileField
class EventWizardFoundationForm(forms.Form):
@@ -22,15 +20,6 @@ class EventWizardFoundationForm(forms.Form):
widget=forms.CheckboxSelectMultiple,
help_text=_('Choose all languages that your event should be available in.')
)
has_subevents = forms.BooleanField(
label=_("This is an event series"),
help_text=_('Only recommended for advanced users. If this feature is enabled, this will not only be a '
'single event but a series of very similar events that are handled within a single shop. '
'The single events inside the series can only differ in date, time, location, prices and '
'quotas, but not in other settings, and buying tickets across multiple of these events at '
'the same time is possible. You cannot change this setting for this event later.'),
required=False,
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
@@ -78,22 +67,15 @@ class EventWizardBasicsForm(I18nModelForm):
'presale_start': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
'presale_end': forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-date-after': '#id_basics-presale_start'}),
'slug': SlugWidget
}
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
self.locales = kwargs.get('locales')
self.has_subevents = kwargs.pop('has_subevents')
kwargs.pop('user')
super().__init__(*args, **kwargs)
self.initial['timezone'] = get_current_timezone_name()
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
self.fields['location'].widget.attrs['rows'] = '3'
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index')
if self.has_subevents:
del self.fields['presale_start']
del self.fields['presale_end']
def clean(self):
data = super().clean()
@@ -143,7 +125,6 @@ class EventWizardCopyForm(forms.Form):
def __init__(self, *args, **kwargs):
kwargs.pop('organizer')
kwargs.pop('locales')
kwargs.pop('has_subevents')
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['copy_from_event'] = forms.ModelChoiceField(
@@ -182,8 +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',
'data-date-default': '#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'}),
@@ -215,15 +195,15 @@ class EventSettingsForm(SettingsForm):
presale_start_show_date = forms.BooleanField(
label=_("Show start date"),
help_text=_("Show the presale start date before presale has started."),
widget=forms.CheckboxInput,
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_presale_start'}),
required=False
)
last_order_modification_date = RelativeDateTimeField(
last_order_modification_date = forms.DateTimeField(
label=_('Last date of modifications'),
help_text=_("The last date users can modify details of their orders, such as attendee names or "
"answers to questions. If you use the event series feature and an order contains tickest for "
"multiple event dates, the earliest date will be used."),
"answers to questions."),
required=False,
widget=forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
)
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
@@ -308,7 +288,7 @@ class EventSettingsForm(SettingsForm):
reservation_time = forms.IntegerField(
min_value=0,
label=_("Reservation period"),
help_text=_("The number of minutes the items in a user's cart are reserved for this user."),
help_text=_("The number of minutes the items in a user's card are reserved for this user."),
)
imprint_url = forms.URLField(
label=_("Imprint URL"),
@@ -347,12 +327,12 @@ class PaymentSettingsForm(SettingsForm):
label=_('Payment term in days'),
help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."),
)
payment_term_last = RelativeDateField(
payment_term_last = forms.DateField(
label=_('Last date of payments'),
help_text=_("The last date any payments are accepted. This has precedence over the number of "
"days configured above. If you use the event series feature and an order contains tickets for "
"multiple dates, the earliest date will be used."),
"days configured above."),
required=False,
widget=forms.DateInput(attrs={'class': 'datepickerfield'})
)
payment_term_weekdays = forms.BooleanField(
label=_('Only end payment terms on weekdays'),
@@ -385,7 +365,7 @@ class PaymentSettingsForm(SettingsForm):
cleaned_data = super().clean()
payment_term_last = cleaned_data.get('payment_term_last')
if payment_term_last and self.obj.presale_end:
if payment_term_last.date(self.obj) < self.obj.presale_end.date():
if payment_term_last < self.obj.presale_end.date():
self.add_error(
'payment_term_last',
_('The last payment date cannot be before the end of presale.'),
@@ -412,8 +392,6 @@ class ProviderForm(SettingsForm):
v._required = v.one_required
v.one_required = False
v.widget.enabled_locales = self.locales
elif isinstance(v, (RelativeDateTimeField, RelativeDateField)):
v.set_event(self.obj)
def clean(self):
cleaned_data = super().clean()
@@ -436,39 +414,17 @@ class InvoiceSettingsForm(SettingsForm):
required=False,
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
)
invoice_name_required = forms.BooleanField(
label=_("Require customer name"),
required=False,
widget=forms.CheckboxInput(
attrs={'data-checkbox-dependency': '#id_invoice_address_asked',
'data-inverse-dependency': '#id_invoice_address_required'}
),
)
invoice_address_vatid = forms.BooleanField(
label=_("Ask for VAT ID"),
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
required=False
)
invoice_include_free = forms.BooleanField(
label=_("Show free products on invoices"),
help_text=_("Note that invoices will never be generated for orders that contain only free "
"products."),
required=False
)
invoice_numbers_consecutive = forms.BooleanField(
label=_("Generate invoices with consecutive numbers"),
help_text=_("If deactivated, the order code will be used in the invoice number."),
required=False
)
invoice_numbers_prefix = forms.CharField(
label=_("Invoice number prefix"),
help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will "
"be used followed by a dash. Attention: If multiple events within the same organization use the "
"same value in this field, they will share their number range, i.e. every full number will be "
"used at most once over all of your events. This setting only affects future invoices."),
required=False,
)
invoice_generate = forms.ChoiceField(
label=_("Generate invoices"),
required=False,
@@ -480,11 +436,6 @@ class InvoiceSettingsForm(SettingsForm):
('paid', _('Automatically on payment')),
)
)
invoice_renderer = forms.ChoiceField(
label=_("Invoice style"),
required=True,
choices=[]
)
invoice_address_from = forms.CharField(
widget=forms.Textarea(attrs={'rows': 5}), required=False,
label=_("Your address"),
@@ -521,20 +472,12 @@ class InvoiceSettingsForm(SettingsForm):
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
)
def __init__(self, *args, **kwargs):
event = kwargs.get('obj')
super().__init__(*args, **kwargs)
self.fields['invoice_renderer'].choices = [
(r.identifier, r.verbose_name) for r in event.get_invoice_renderers().values()
]
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
class MailSettingsForm(SettingsForm):
mail_prefix = forms.CharField(
label=_("Subject prefix"),
help_text=_("This will be prepended to the subject of all outgoing emails, formatted as [prefix]. "
"Choose, for example, a short form of your event name."),
help_text=_("This will be prepended to the subject of all outgoing emails. This could be a short form of "
"your event name."),
required=False
)
mail_from = forms.EmailField(
@@ -615,22 +558,6 @@ class MailSettingsForm(SettingsForm):
help_text=_("Available placeholders: {event}, {url}, {product}, {hours}, {code}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{product}', '{hours}', '{code}'])]
)
mail_text_order_canceled = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {code}, {url}"),
validators=[PlaceholderValidator(['{event}', '{code}', '{url}'])]
)
mail_text_order_custom_mail = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}',
'{invoice_name}', '{invoice_company}'])]
)
smtp_use_custom = forms.BooleanField(
label=_("Use custom SMTP server"),
help_text=_("All mail related to your event will be sent over the smtp server specified by you."),
@@ -690,7 +617,7 @@ class DisplaySettingsForm(SettingsForm):
)
logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
help_text=_('If you provide a logo image, we will by default not show your events name and date '
'in the page header. We will show your logo with a maximal height of 120 pixels.')
@@ -712,12 +639,12 @@ class TicketSettingsForm(SettingsForm):
help_text=_("Use pretix to generate tickets for the user to download and print out."),
required=False
)
ticket_download_date = RelativeDateTimeField(
ticket_download_date = forms.DateTimeField(
label=_("Download date"),
help_text=_("Ticket download will be offered after this date. If you use the event series feature and an order "
"contains tickets for multiple event dates, download of all tickets will be available if at least "
"one of the event dates allows it."),
required=False,
help_text=_("Ticket download will be offered after this date."),
required=True,
widget=forms.DateTimeInput(attrs={'class': 'datetimepicker',
'data-display-dependency': '#id_ticket_download'}),
)
ticket_download_addons = forms.BooleanField(
label=_("Offer to download tickets separately for add-on products"),
@@ -751,15 +678,3 @@ class TicketSettingsForm(SettingsForm):
val = cleaned_data.get(k)
if v._required and (val is None or val == ""):
self.add_error(k, _('This field is required.'))
class CommentForm(I18nModelForm):
class Meta:
model = Event
fields = ['comment']
widgets = {
'comment': forms.Textarea(attrs={
'rows': 3,
'class': 'helper-width-100',
}),
}

View File

@@ -1,10 +1,9 @@
from django import forms
from django.db.models import Q
from django.db.models.functions import Concat
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Item, Order, Organizer, SubEvent
from pretix.base.models import Item, Order, Organizer
from pretix.base.signals import register_payment_providers
from pretix.control.utils.i18n import i18ncomp
@@ -47,13 +46,11 @@ class OrderFilterForm(FilterForm):
if fdata.get('query'):
u = fdata.get('query')
if "-" in u:
code = (Q(event__slug__icontains=u.split("-")[0])
& Q(code__icontains=Order.normalize_code(u.split("-")[1])))
else:
code = Q(code__icontains=Order.normalize_code(u))
qs = qs.annotate(inr=Concat('invoices__prefix', 'invoices__invoice_no'))
qs = qs.filter(
code
| Q(email__icontains=u)
@@ -61,9 +58,6 @@ class OrderFilterForm(FilterForm):
| Q(positions__attendee_email__icontains=u)
| Q(invoice_address__name__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(invoices__invoice_no=u)
| Q(invoices__invoice_no=u.zfill(5))
| Q(inr=u)
)
if fdata.get('status'):
@@ -92,12 +86,6 @@ class EventOrderFilterForm(OrderFilterForm):
],
required=False,
)
subevent = forms.ModelChoiceField(
label=pgettext_lazy('subevent', 'Date'),
queryset=SubEvent.objects.none(),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates')
)
def get_payment_providers(self):
providers = []
@@ -117,20 +105,12 @@ class EventOrderFilterForm(OrderFilterForm):
self.fields['provider'].choices += [(k, v.verbose_name) for k, v
in self.event.get_payment_providers().items()]
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
elif 'subevent':
del self.fields['subevent']
def filter_qs(self, qs):
fdata = self.cleaned_data
qs = super().filter_qs(qs)
if fdata.get('item'):
qs = qs.filter(positions__item=fdata.get('item'))
if fdata.get('subevent'):
qs = qs.filter(positions__subevent=fdata.get('subevent'))
qs = qs.filter(positions__item_id__in=(fdata.get('item'),))
if fdata.get('provider'):
qs = qs.filter(payment_provider=fdata.get('provider'))
@@ -166,57 +146,6 @@ class OrderSearchFilterForm(OrderFilterForm):
return qs
class SubEventFilterForm(FilterForm):
status = forms.ChoiceField(
label=_('Status'),
choices=(
('', _('All')),
('active', _('Active')),
('running', _('Shop live and presale running')),
('inactive', _('Inactive')),
('future', _('Presale not started')),
('past', _('Presale over')),
),
required=False
)
query = forms.CharField(
label=_('Event name'),
widget=forms.TextInput(attrs={
'placeholder': _('Event name'),
'autofocus': 'autofocus'
}),
required=False
)
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('status') == 'active':
qs = qs.filter(active=True)
elif fdata.get('status') == 'running':
qs = qs.filter(
active=True
).filter(
Q(presale_start__isnull=True) | Q(presale_start__lte=now())
).filter(
Q(presale_end__isnull=True) | Q(presale_end__gte=now())
)
elif fdata.get('status') == 'inactive':
qs = qs.filter(active=False)
elif fdata.get('status') == 'future':
qs = qs.filter(presale_start__gte=now())
elif fdata.get('status') == 'past':
qs = qs.filter(presale_end__lte=now())
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
)
return qs
class EventFilterForm(FilterForm):
status = forms.ChoiceField(
label=_('Status'),

View File

@@ -3,6 +3,7 @@ import copy
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.forms import BooleanField, ModelMultipleChoiceField
from django.forms.formsets import DELETION_FIELD_NAME
from django.utils.translation import ugettext as __, ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea
@@ -51,6 +52,7 @@ class QuestionForm(I18nModelForm):
class QuestionOptionForm(I18nModelForm):
class Meta:
model = QuestionOption
localized_fields = '__all__'
@@ -60,38 +62,36 @@ class QuestionOptionForm(I18nModelForm):
class QuotaForm(I18nModelForm):
def __init__(self, **kwargs):
self.instance = kwargs.get('instance', None)
self.event = kwargs.get('event')
items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations')
self.original_instance = copy.copy(self.instance) if self.instance else None
initial = kwargs.get('initial', {})
if self.instance and self.instance.pk:
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all()] + [
'{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all()
]
kwargs['initial'] = initial
items = kwargs['items']
del kwargs['items']
instance = kwargs.get('instance', None)
self.original_instance = copy.copy(instance) if instance else None
super().__init__(**kwargs)
choices = []
if hasattr(self, 'instance') and self.instance.pk:
active_items = set(self.instance.items.all())
active_variations = set(self.instance.variations.all())
else:
active_items = set()
active_variations = set()
for item in items:
if len(item.variations.all()) > 0:
for v in item.variations.all():
choices.append(('{}-{}'.format(item.pk, v.pk), '{} {}'.format(item.name, v.value)))
self.fields['item_%s' % item.id] = ModelMultipleChoiceField(
label=_("Activate for"),
required=False,
initial=active_variations,
queryset=item.variations.all(),
widget=forms.CheckboxSelectMultiple
)
else:
choices.append(('{}'.format(item.pk), item.name))
self.fields['itemvars'] = forms.MultipleChoiceField(
label=_('Products'),
required=False,
choices=choices,
widget=forms.CheckboxSelectMultiple
)
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
else:
del self.fields['subevent']
self.fields['item_%s' % item.id] = BooleanField(
label=_("Activate"),
required=False,
initial=(item in active_items)
)
class Meta:
model = Quota
@@ -99,34 +99,10 @@ class QuotaForm(I18nModelForm):
fields = [
'name',
'size',
'subevent'
]
def save(self, *args, **kwargs):
creating = not self.instance.pk
inst = super().save(*args, **kwargs)
selected_items = set(list(self.event.items.filter(id__in=[
i.split('-')[0] for i in self.cleaned_data['itemvars']
])))
selected_variations = list(ItemVariation.objects.filter(item__event=self.event, id__in=[
i.split('-')[1] for i in self.cleaned_data['itemvars'] if '-' in i
]))
current_items = [] if creating else self.instance.items.all()
current_variations = [] if creating else self.instance.variations.all()
self.instance.items.remove(*[i for i in current_items if i not in selected_items])
self.instance.items.add(*[i for i in selected_items if i not in current_items])
self.instance.variations.remove(*[i for i in current_variations if i not in selected_variations])
self.instance.variations.add(*[i for i in selected_variations if i not in current_variations])
return inst
class ItemCreateForm(I18nModelForm):
NONE = 'none'
EXISTING = 'existing'
NEW = 'new'
has_variations = forms.BooleanField(label=_('The product should exist in multiple variations'),
help_text=_('Select this option e.g. for t-shirts that come in multiple sizes. '
'You can select the variations in the next step.'),
@@ -135,7 +111,6 @@ class ItemCreateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
self.fields['copy_from'] = forms.ModelChoiceField(
label=_("Copy product information"),
@@ -145,41 +120,6 @@ class ItemCreateForm(I18nModelForm):
required=False
)
if not self.event.has_subevents:
self.fields['quota_option'] = forms.ChoiceField(
label=_("Quota options"),
widget=forms.RadioSelect,
choices=(
(self.NONE, _("Do not add to a quota now")),
(self.EXISTING, _("Add product to an existing quota")),
(self.NEW, _("Create a new quota for this product"))
),
initial=self.NONE,
required=False
)
self.fields['quota_add_existing'] = forms.ModelChoiceField(
label=_("Add to existing quota"),
widget=forms.Select(),
queryset=self.instance.event.quotas.all(),
required=False
)
self.fields['quota_add_new_name'] = forms.CharField(
label=_("Name"),
max_length=200,
widget=forms.TextInput(attrs={'placeholder': _("New quota name")}),
required=False
)
self.fields['quota_add_new_size'] = forms.IntegerField(
min_value=0,
label=_("Size"),
widget=forms.TextInput(attrs={'placeholder': _("New quota size")}),
help_text=_("Leave empty for an unlimited number of tickets."),
required=False
)
def save(self, *args, **kwargs):
if self.cleaned_data.get('copy_from'):
self.instance.description = self.cleaned_data['copy_from'].description
@@ -195,19 +135,6 @@ class ItemCreateForm(I18nModelForm):
instance = super().save(*args, **kwargs)
if not self.event.has_subevents:
if self.cleaned_data.get('quota_option') == self.EXISTING and self.cleaned_data.get('quota_add_existing') is not None:
quota = self.cleaned_data.get('quota_add_existing')
quota.items.add(self.instance)
elif self.cleaned_data.get('quota_option') == self.NEW:
quota_name = self.cleaned_data.get('quota_add_new_name')
quota_size = self.cleaned_data.get('quota_add_new_size')
quota = Quota.objects.create(
event=self.event, name=quota_name, size=quota_size
)
quota.items.add(self.instance)
if self.cleaned_data.get('has_variations'):
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
for variation in self.cleaned_data['copy_from'].variations.all():
@@ -224,23 +151,6 @@ class ItemCreateForm(I18nModelForm):
return instance
def clean(self):
cleaned_data = super().clean()
if not self.event.has_subevents:
if cleaned_data.get('quota_option') == self.NEW:
if not self.cleaned_data.get('quota_add_new_name'):
raise forms.ValidationError(
{'quota_add_new_name': [_("Quota name is required.")]}
)
elif cleaned_data.get('quota_option') == self.EXISTING:
if not self.cleaned_data.get('quota_add_existing'):
raise forms.ValidationError(
{'quota_add_existing': [_("Please select a quota.")]}
)
return cleaned_data
class Meta:
model = Item
localized_fields = '__all__'
@@ -287,6 +197,7 @@ class ItemUpdateForm(I18nModelForm):
class ItemVariationsFormSet(I18nFormSet):
def clean(self):
super().clean()
for f in self.forms:
@@ -371,7 +282,6 @@ class ItemAddOnForm(I18nModelForm):
'addon_category',
'min_count',
'max_count',
'price_included'
]
help_texts = {
'min_count': _('Be aware that setting a minimal number makes it impossible to buy this product if all '

View File

@@ -4,12 +4,10 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, ItemAddOn, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
class ExtendForm(I18nModelForm):
@@ -57,15 +55,6 @@ class CommentForm(I18nModelForm):
}
class SubEventChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
p = get_price(self.instance.item, self.instance.variation,
voucher=self.instance.voucher,
subevent=obj)
return '{} {} ({} {})'.format(obj.name, obj.get_date_range_display(),
p, self.instance.order.event.currency)
class OrderPositionAddForm(forms.Form):
do = forms.BooleanField(
label=_('Add a new product to the order'),
@@ -85,12 +74,6 @@ class OrderPositionAddForm(forms.Form):
label=_('Gross price'),
help_text=_("Keep empty for the product's default price")
)
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'Date'),
required=True,
empty_label=None
)
def __init__(self, *args, **kwargs):
order = kwargs.pop('order')
@@ -117,20 +100,9 @@ class OrderPositionAddForm(forms.Form):
else:
del self.fields['addon_to']
if order.event.has_subevents:
self.fields['subevent'].queryset = order.event.subevents.all()
else:
del self.fields['subevent']
class OrderPositionChangeForm(forms.Form):
itemvar = forms.ChoiceField()
subevent = SubEventChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'New date'),
required=True,
empty_label=None
)
price = forms.DecimalField(
required=False,
max_digits=10, decimal_places=2,
@@ -142,7 +114,6 @@ class OrderPositionChangeForm(forms.Form):
choices=(
('product', 'Change product'),
('price', 'Change price'),
('subevent', 'Change event date'),
('cancel', 'Remove product')
)
)
@@ -160,15 +131,9 @@ class OrderPositionChangeForm(forms.Form):
pass
initial['price'] = instance.price
initial['subevent'] = instance.subevent
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if instance.order.event.has_subevents:
self.fields['subevent'].instance = instance
self.fields['subevent'].queryset = instance.order.event.subevents.all()
else:
del self.fields['subevent']
choices = []
for i in instance.order.event.items.prefetch_related('variations').all():
pname = str(i.name)
@@ -177,13 +142,11 @@ class OrderPositionChangeForm(forms.Form):
variations = list(i.variations.all())
if variations:
for v in variations:
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent)
choices.append(('%d-%d' % (i.pk, v.pk),
'%s %s (%s %s)' % (pname, v.value, localize(p),
'%s %s (%s %s)' % (pname, v.value, localize(v.price),
instance.order.event.currency)))
else:
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent)
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(p),
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(i.default_price),
instance.order.event.currency)))
self.fields['itemvar'].choices = choices
@@ -215,30 +178,3 @@ class OrderLocaleForm(forms.ModelForm):
super().__init__(*args, **kwargs)
locale_names = dict(settings.LANGUAGES)
self.fields['locale'].choices = [(a, locale_names[a]) for a in self.instance.event.settings.locales]
class OrderMailForm(forms.Form):
subject = forms.CharField(
label=_("Subject"),
required=True
)
def __init__(self, *args, **kwargs):
order = kwargs.pop('order')
super().__init__(*args, **kwargs)
self.fields['sendto'] = forms.EmailField(
label=_("Recipient"),
required=True,
initial=order.email
)
self.fields['sendto'].widget.attrs['readonly'] = 'readonly'
self.fields['message'] = forms.CharField(
label=_("Message"),
required=True,
widget=forms.Textarea,
initial=order.event.settings.mail_text_order_custom_mail.localize(order.locale),
help_text=_("Available placeholders: {expire_date}, {event}, {code}, {date}, {url}, "
"{invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{expire_date}', '{event}', '{code}', '{date}', '{url}',
'{invoice_name}', '{invoice_company}'])]
)

View File

@@ -119,25 +119,10 @@ class OrganizerSettingsForm(SettingsForm):
help_text=_('This will be displayed on the organizer homepage.')
)
organizer_info_text = I18nFormField(
label=_('Info text'),
required=False,
widget=I18nTextarea,
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
)
organizer_logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".svg", ".gif", ".jpeg"),
ext_whitelist=(".png", ".jpg", ".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.')
)
event_list_type = forms.ChoiceField(
label=_('Event overview stile'),
choices=(
('list', _('List')),
('calendar', _('Calendar'))
)
)

View File

@@ -1,101 +0,0 @@
from django import forms
from django.utils.functional import cached_property
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem
class SubEventForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
super().__init__(*args, **kwargs)
self.fields['location'].widget.attrs['rows'] = '3'
class Meta:
model = SubEvent
localized_fields = '__all__'
fields = [
'name',
'active',
'date_from',
'date_to',
'date_admission',
'presale_start',
'presale_end',
'location',
'frontpage_text'
]
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'}),
}
class SubEventItemOrVariationFormMixin:
def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item')
self.variation = kwargs.pop('variation', None)
super().__init__(*args, **kwargs)
class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format(
self.item.default_price, self.item.event.currency
)
self.fields['price'].label = str(self.item.name)
class Meta:
model = SubEventItem
fields = ['price']
class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format(
self.variation.price, self.item.event.currency
)
self.fields['price'].label = '{} {}'.format(str(self.item.name), self.variation.value)
class Meta:
model = SubEventItem
fields = ['price']
class QuotaFormSet(I18nInlineFormSet):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event', None)
self.locales = self.event.settings.get('locales')
super().__init__(*args, **kwargs)
@cached_property
def items(self):
return self.event.items.prefetch_related('variations').all()
def _construct_form(self, i, **kwargs):
kwargs['locales'] = self.locales
kwargs['event'] = self.event
kwargs['items'] = self.items
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
locales=self.locales,
event=self.event,
items=self.items
)
self.add_fields(form, None)
return form

View File

@@ -4,7 +4,7 @@ from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, ItemVariation, Quota, Voucher
@@ -24,7 +24,7 @@ class VoucherForm(I18nModelForm):
localized_fields = '__all__'
fields = [
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
'comment', 'max_usages', 'price_mode', 'subevent'
'comment', 'max_usages', 'price_mode'
]
widgets = {
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),
@@ -47,12 +47,6 @@ class VoucherForm(I18nModelForm):
else:
self.initial_instance_data = None
super().__init__(*args, **kwargs)
if instance.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all()
elif 'subevent':
del self.fields['subevent']
choices = []
for i in self.instance.event.items.prefetch_related('variations').all():
variations = list(i.variations.all())
@@ -109,12 +103,6 @@ class VoucherForm(I18nModelForm):
else:
cnt = data['max_usages']
if self.instance.event.has_subevents and data['block_quota'] and not data.get('subevent'):
raise ValidationError(pgettext_lazy(
'subevent',
'If you want this voucher to block quota, you need to select a specific date.'
))
if self._clean_quota_needs_checking(data):
self._clean_quota_check(data, cnt)
@@ -148,10 +136,6 @@ class VoucherForm(I18nModelForm):
# The voucher has been reassigned to a different item, variation or quota
return True
if data.get('subevent') != self.initial.get('subevent'):
# The voucher has been reassigned to a different subevent
return True
return False
def _clean_was_valid(self):
@@ -163,11 +147,9 @@ class VoucherForm(I18nModelForm):
if self.initial_instance_data.quota:
quotas.add(self.initial_instance_data.quota)
elif self.initial_instance_data.variation:
quotas |= set(self.initial_instance_data.variation.quotas.filter(
subevent=self.initial_instance_data.subevent))
quotas |= set(self.initial_instance_data.variation.quotas.all())
elif self.initial_instance_data.item:
quotas |= set(self.initial_instance_data.item.quotas.filter(
subevent=self.initial_instance_data.subevent))
quotas |= set(self.initial_instance_data.item.quotas.all())
return quotas
def _clean_quota_check(self, data, cnt):
@@ -182,9 +164,9 @@ class VoucherForm(I18nModelForm):
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
'Otherwise it might be unclear which quotas to block.'))
elif self.instance.item and self.instance.variation:
avail = self.instance.variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
avail = self.instance.variation.check_quotas(ignored_quotas=old_quotas)
elif self.instance.item and not self.instance.item.has_variations:
avail = self.instance.item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
avail = self.instance.item.check_quotas(ignored_quotas=old_quotas)
else:
raise ValidationError(_('You need to specify either a quota or a product.'))
@@ -213,7 +195,7 @@ class VoucherBulkForm(VoucherForm):
localized_fields = '__all__'
fields = [
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
'max_usages', 'price_mode', 'subevent'
'max_usages', 'price_mode'
]
widgets = {
'valid_until': forms.DateTimeInput(attrs={'class': 'datetimepicker'}),

View File

@@ -3,7 +3,7 @@ from decimal import Decimal
from django.dispatch import receiver
from django.utils import formats
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from i18nfield.strings import LazyI18nString
from pretix.base.models import Event, ItemVariation, LogEntry, OrderPosition
@@ -33,17 +33,6 @@ def _display_order_changed(event: Event, logentry: LogEntry):
new_price=formats.localize(Decimal(data['new_price'])),
currency=event.currency
)
elif logentry.action_type == 'pretix.event.order.changed.subevent':
old_se = str(event.subevents.get(pk=data['old_subevent']))
new_se = str(event.subevents.get(pk=data['new_subevent']))
return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price} {currency}) changed '
'to "{new_event}" ({new_price} {currency}).').format(
posid=data.get('positionid', '?'),
old_event=old_se, new_event=new_se,
old_price=formats.localize(Decimal(data['old_price'])),
new_price=formats.localize(Decimal(data['new_price'])),
currency=event.currency
)
elif logentry.action_type == 'pretix.event.order.changed.price':
return text + ' ' + _('Price of position #{posid} changed from {old_price} {currency} '
'to {new_price} {currency}.').format(
@@ -87,9 +76,9 @@ def _display_order_changed(event: Event, logentry: LogEntry):
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
plains = {
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.order.modified': _('The order details have been modified.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.resend': _('The link to the order detail page has been resent to the user.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
'pretix.event.order.expirychanged': _('The order\'s expiry date has been changed.'),
'pretix.event.order.expired': _('The order has been marked as expired.'),
@@ -104,16 +93,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.comment': _('The order\'s internal comment has been updated.'),
'pretix.event.order.payment.changed': _('The payment method has been changed.'),
'pretix.event.order.email.sent': _('An unindentified type email has been sent.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.email.expire_warning_sent': _('An email has been sent with a warning that the order is about '
'to expire.'),
'pretix.event.order.email.order_canceled': _('An email has been sent to notify user order has been canceled.'),
'pretix.event.order.email.order_changed': _('An email has been sent to notify user order has been changed.'),
'pretix.event.order.email.order_free': _('An email has been sent to notify user order has been received.'),
'pretix.event.order.email.order_paid': _('An email has been sent to notify user payment has been received.'),
'pretix.event.order.email.order_placed': _('An email has been sent to notify user order has been received and require payment.'),
'pretix.event.order.email.resend': _('An email has been sent with link to the order detail page has been resent to the user.'),
'pretix.event.order.expire_warning_sent': _('An email has been sent with a warning that the order is about '
'to expire.'),
'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
@@ -164,12 +145,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'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.'),
'pretix.subevent.deleted': pgettext_lazy('subevent', 'The event date has been deleted.'),
'pretix.subevent.changed': pgettext_lazy('subevent', 'The event date has been modified.'),
'pretix.subevent.added': pgettext_lazy('subevent', 'The event date has been created.'),
'pretix.subevent.quota.added': pgettext_lazy('subevent', 'A quota has been added to the event date.'),
'pretix.subevent.quota.changed': pgettext_lazy('subevent', 'A quota has been modified on the event date.'),
'pretix.subevent.quota.deleted': pgettext_lazy('subevent', 'A quota has been removed from the event date.'),
}
data = json.loads(logentry.data)

View File

@@ -44,9 +44,8 @@
{{ html_head|safe }}
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
</head>
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}">
<body data-datetimeformat="{{ js_datetime_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}">
<div id="wrapper">
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="navbar-header">
@@ -61,22 +60,6 @@
data-toggle="collapse" data-target=".navbar-events-collapse">
<i class="fa fa-calendar"></i><span class="caret"></span>
</button>
{% if request.event %}
{% if has_domain and not request.event.live %}
<form action="{% eventurl request.event "presale:event.auth" %}" method="post"
target="_blank" class="mobile-navbar-view-form visible-xs-block">
<input type="hidden" value="{{ new_session }}" name="session">
<button type="submit" class="btn btn-link navbar-toggle">
<i class="fa fa-eye"></i>
</button>
</form>
{% else %}
<a href="{% eventurl request.event "presale:event.index" %}" title="{% trans "Go to Shop" %}"
target="_blank" class="navbar-toggle mobile-navbar-view-link">
<i class="fa fa-eye"></i>
</a>
{% endif %}
{% endif %}
<a class="navbar-brand" href="{% url "control:index" %}">
<img src="{% static "pretixbase/img/pretix-icon.svg" %}" />
{{ settings.PRETIX_INSTANCE_NAME }}

View File

@@ -21,17 +21,6 @@
</option>
{% endfor %}
</select>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<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>
@@ -56,10 +45,6 @@
<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>
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %} <a href="?{% url_replace request 'ordering' '-subevent'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'subevent'%}"><i class="fa fa-caret-up"></i></a></th>
{% endif %}
<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>
@@ -75,12 +60,10 @@
{% 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>
<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 }}{% if e.variation %} {{ e.variation }}{% endif %}</td>
{% if request.event.has_subevents %}
<td>{{ e.subevent.name }} {{ e.subevent.get_date_range_display }}</td>
{% endif %}
<td>{{ e.item.name }}</td>
<td>{{ e.order.email }}</td>
<td>
{% if e.addon_to %}

View File

@@ -11,23 +11,13 @@
{% trans "Dashboard" %}
</a>
</li>
{% if 'can_change_event_settings' in request.eventpermset %}
{% 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 %}"
{% if "event.settings" == url_name or "event.settings." in url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-wrench fa-fw"></i>
{% trans "Settings" %}
</a>
</li>
{% if request.event.has_subevents %}
<li>
<a href="{% url 'control:event.subevents' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if "event.subevent" in url_name %}class="active"{% endif %}>
<i class="fa fa-calendar fa-fw"></i>
{% trans "Dates" context "subevent" %}
</a>
</li>
{% endif %}
{% endif %}
{% if 'can_change_items' in request.eventpermset %}
<li>

View File

@@ -1,13 +1,12 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load eventurl %}
{% load bootstrap3 %}
{% load staticfiles %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<h1>
{{ request.event.name }}
</h1>
<h1>
{{ request.event.name }}
</h1>
{% if actions|length > 0 %}
<div class="panel panel-danger">
<div class="panel-heading">
@@ -20,12 +19,10 @@
<li class="list-group-item logentry">
<p>
<a href="{% url "control:event.requiredaction.discard" event=request.event.slug organizer=request.event.organizer.slug id=action.id %}"
class="btn btn-default btn-xs pull-right">
class="btn btn-default btn-xs pull-right">
{% trans "Hide message" %}
</a>
<small><span
class="fa fa-clock-o"></span> {{ action.datetime|date:"SHORT_DATETIME_FORMAT" }}
</small>
<small><span class="fa fa-clock-o"></span> {{ action.datetime|date:"SHORT_DATETIME_FORMAT" }}</small>
</p>
{{ action.display|safe }}
</li>
@@ -60,28 +57,6 @@
</div>
<p>&nbsp;</p>
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Internal comment" %}
</h3>
</div>
<div class="panel-body">
<form class="form" method="post"
action="{% url "control:event.comment" event=request.event.slug organizer=request.event.organizer.slug %}">
{% csrf_token %}
<div class="row">
{% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
</div>
<p class="text-right">
<br>
<button class="btn btn-default">
{% trans "Update comment" %}
</button>
</p>
</form>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
@@ -99,8 +74,8 @@
{% if log.user %}
{% if log.user.is_superuser %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
</span>
{% else %}
<span class="fa fa-user fa-fw"></span>

View File

@@ -9,14 +9,10 @@
<legend>{% trans "Invoicing" %}</legend>
{% bootstrap_field form.invoice_address_asked layout="horizontal" %}
{% bootstrap_field form.invoice_address_required layout="horizontal" %}
{% bootstrap_field form.invoice_name_required layout="horizontal" %}
{% bootstrap_field form.invoice_address_vatid layout="horizontal" %}
{% bootstrap_field form.invoice_numbers_consecutive layout="horizontal" %}
{% bootstrap_field form.invoice_numbers_prefix layout="horizontal" %}
{% bootstrap_field form.invoice_generate layout="horizontal" %}
{% bootstrap_field form.invoice_renderer layout="horizontal" %}
{% bootstrap_field form.invoice_language layout="horizontal" %}
{% bootstrap_field form.invoice_include_free layout="horizontal" %}
{% bootstrap_field form.invoice_address_from layout="horizontal" %}
{% bootstrap_field form.invoice_introductory_text layout="horizontal" %}
{% bootstrap_field form.invoice_additional_text layout="horizontal" %}

View File

@@ -35,13 +35,6 @@
{% blocktrans asvar title_waiting_list_notification %}Waiting list notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="waiting_list" title=title_waiting_list_notification items="mail_text_waiting_list" %}
{% blocktrans asvar title_order_canceled %}Order canceled{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_canceled" title=title_order_canceled items="mail_text_order_canceled" %}
{% blocktrans asvar title_order_custom_mail %}Order custom mail{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
</div>
</fieldset>
<fieldset>

View File

@@ -4,9 +4,8 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#{{ pid }}">
<a data-toggle="collapse" href="#{{ pid }}">
<strong>{% trans title %}</strong>
<i class="fa fa-angle-down collapse-indicator"></i>
</a>
</h4>
</div>
@@ -24,14 +23,6 @@
{% with form|getattr:item as field %}
<label class="col-md-3 control-label">{{ field.label }}</label>
<div class="col-md-9">
<ul class="nav nav-tabs">
<li role="presentation" class="active">
<a data-toggle="tab" type="edit" href="#{{ item }}_edit"><i class="fa fa-pencil-square-o fa-fw"></i> {% trans "Edit" %}</a>
</li>
<li role="presentation">
<a data-toggle="tab" type="preview" href="#{{ item }}_preview"><i class="fa fa-tv fa-fw"></i> {% trans "Preview" %}</a>
</li>
</ul>
<div class="tab-content">
<div id="{{ item }}_edit" class="tab-pane fade in active">
{% bootstrap_field field show_label=False form_group_class="" %}
@@ -42,6 +33,14 @@
{% endfor %}
</div>
</div>
<ul class="nav nav-pills pull-right">
<li role="presentation" class="active">
<a data-toggle="pill" type="edit" href="#{{ item }}_edit"><i class="fa fa-pencil-square-o fa-fw"></i> {% trans "Edit" %}</a>
</li>
<li role="presentation">
<a data-toggle="pill" type="preview" href="#{{ item }}_preview"><i class="fa fa-tv fa-fw"></i> {% trans "Preview" %}</a>
</li>
</ul>
</div>
{% endwith %}
</div>

View File

@@ -22,19 +22,22 @@
Please note that EU Directive 2015/2366 bans surcharging payment fees for most common payment
methods within the European Union. Depending on the payment method, this might affect
selling to consumers only or to business customers as well. Depending on your country, this
legislation might already be in effect or become relevant from January 2018 at the latest. This
is not legal advice. If in doubt, consult a lawyer or refrain from charging payment fees.
legislation might already be in place or become relevant from January 2018 the latest. This is not
legal advice. If in doubt, consult a lawyer or refrain from charging payment fees.
{% endblocktrans %}
</div>
{% for provider in providers %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#{{ provider.identifier }}">
{{ provider.verbose_name }}
<i class="fa fa-angle-down collapse-indicator"></i>
</a>
</h3>
<div class="row">
<div class="col-sm-10">
<h3 class="panel-title">
<a data-toggle="collapse" href="#{{ provider.identifier }}">
{{ provider.verbose_name }}
</a>
</h3>
</div>
</div>
</div>
<div id="{{ provider.identifier }}" class="panel-collapse collapse">
<div class="panel-body">

View File

@@ -11,7 +11,7 @@
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<div class="row row-plugins">
<div class="row">
{% for plugin in plugins %}
<div class="col-md-6 col-sm-12">
<div class="panel panel-{% if plugin.app.compatibility_errors %}warning{% elif plugin.module in plugins_active %}success{% else %}default{% endif %}">

View File

@@ -5,30 +5,9 @@
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
<div class="form-group">
<label class="col-md-3 control-label" for="{{ form.slug.id_for_label }}">{{ form.slug.label }}</label>
<div class="col-md-9 form-inline">
<button class="btn btn-default pull-right" type="button" id="event-slug-random-generate"
data-rng-url="{% url "control:events.add.slugrng" organizer=organizer.slug %}">
{% trans "Set to random" %}
</button>
{% bootstrap_field form.slug form_group_class="helper-display-inline" show_label=False layout="inline" %}
<div class="help-block">
{% blocktrans trimmed %}
This is the address users can buy your tickets at. Should be short, only contain lowercase
letters and numbers, and must be unique among your events. We recommend some kind of
abbreviation or a date with less than 10 characters that can be easily remembered, but you
can also choose to use a random value.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
We will also use this in some places like order codes, invoice numbers or bank transfer
references as an abbreviation to reference this event.
{% endblocktrans %}
</div>
</div>
</div>
{% trans "Random" as rndlabel %}
{% url "control:events.add.slugrng" organizer=organizer.slug as rngurl %}
{% bootstrap_field form.slug layout="horizontal" addon_after='<button class="btn btn-default" type="button" id="event-slug-random-generate" data-rng-url="'|add:rngurl|add:'">'|add:rndlabel|add:'</button>' addon_after_class='input-group-btn' %}
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.location layout="horizontal" %}
@@ -39,11 +18,9 @@
{% bootstrap_field form.locale layout="horizontal" %}
{% bootstrap_field form.timezone layout="horizontal" %}
</fieldset>
{% if form.presale_start %}
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
</fieldset>
{% endif %}
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="horizontal" %}
{% bootstrap_field form.presale_end layout="horizontal" %}
</fieldset>
{% endblock %}

View File

@@ -4,5 +4,4 @@
{% block form %}
{% bootstrap_field form.organizer layout="horizontal" %}
{% bootstrap_field form.locales layout="horizontal" %}
{% bootstrap_field form.has_subevents layout="horizontal" %}
{% endblock %}

View File

@@ -48,7 +48,6 @@
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Short form" %}</th>
<th>{% trans "Organizer" %}</th>
<th>{% trans "Start date" %}</th>
<th>{% trans "End date" %}</th>
@@ -61,7 +60,6 @@
<td>
<strong><a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
</td>
<td>{{ e.slug }}</td>
<td>{{ e.organizer }}</td>
<td>{{ e.get_date_from_display }}</td>
<td>{{ e.get_date_to_display }}</td>

View File

@@ -47,7 +47,6 @@
{% bootstrap_field form.addon_category layout='horizontal' %}
{% bootstrap_field form.min_count layout='horizontal' %}
{% bootstrap_field form.max_count layout='horizontal' %}
{% bootstrap_field form.price_included layout='horizontal' %}
</div>
</div>
{% endfor %}
@@ -79,7 +78,6 @@
{% bootstrap_field formset.empty_form.addon_category layout='horizontal' %}
{% bootstrap_field formset.empty_form.min_count layout='horizontal' %}
{% bootstrap_field formset.empty_form.max_count layout='horizontal' %}
{% bootstrap_field formset.empty_form.price_included layout='horizontal' %}
</div>
</div>
{% endescapescript %}

View File

@@ -2,9 +2,6 @@
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
{% load static %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/hidequota.js" %}"></script>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
@@ -15,19 +12,6 @@
{% bootstrap_field form.category layout="horizontal" %}
{% bootstrap_field form.admission layout="horizontal" %}
</fieldset>
{% if form.quota_option %}
<fieldset>
<legend>{% trans "Quota settings" %}</legend>
{% bootstrap_field form.quota_option layout="horizontal" %}
<div id="existing-quota-group">
{% bootstrap_field form.quota_add_existing layout="horizontal" %}
</div>
<div id="new-quota-group">
{% bootstrap_field form.quota_add_new_name layout="horizontal" %}
{% bootstrap_field form.quota_add_new_size layout="horizontal" %}
</div>
</fieldset>
{% endif %}
<fieldset>
<legend>{% trans "Price settings" %}</legend>
{% bootstrap_field form.default_price layout="horizontal" %}

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% load formset_tags %}
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
{% block inside %}
@@ -58,7 +59,7 @@
<div class="chart" id="question_chart" data-type="{{ question.type }}">
</div>
<script type="application/json" id="question-chart-data">{{ stats_json|safe }}</script>
<script type="application/json" id="question-chart-data">{{ stats_json|escapejson }}</script>
</div>
<div class="col-md-5 col-xs-12">
<table class="table table-bordered table-hover">

Some files were not shown because too many files have changed in this diff Show More