mirror of
https://github.com/pretix/pretix.git
synced 2026-05-17 17:14:04 +00:00
Compare commits
1 Commits
medientaus
...
pajowu/wid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd54c9c5c |
@@ -1,16 +1,11 @@
|
||||
Contributing to pretix
|
||||
======================
|
||||
|
||||
Welcome to pretix, we are happy that you would like to contribute.
|
||||
Before you do so, please make sure to read the following documents:
|
||||
Hey there and welcome to pretix!
|
||||
|
||||
- [Contribution workflow](https://docs.pretix.eu/dev/development/contribution/general.html)
|
||||
- [AI-assisted contribution policy](https://docs.pretix.eu/dev/development/contribution/ai.html)
|
||||
- [Coding style and quality](https://docs.pretix.eu/dev/development/contribution/style.html)
|
||||
- [Development setup](https://docs.pretix.eu/dev/development/setup.html)
|
||||
- [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html)
|
||||
* We've got a contributors guide in [our documentation](https://docs.pretix.eu/dev/development/contribution/) together with notes on the [development setup](https://docs.pretix.eu/dev/development/setup.html).
|
||||
|
||||
Before we can accept your first PR we'll need you to sign [our **Contributor License Agreement** (CLA)](https://pretix.eu/about/en/cla).
|
||||
You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
|
||||
* Please note that we have a [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html) in place that applies to all project contributions, including issues, pull requests, etc.
|
||||
|
||||
* Before we can accept a PR from you we'll need you to sign [our CLA](https://pretix.eu/about/en/cla). You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
|
||||
|
||||
**Before contributing new functionality, always open a discussion first.**
|
||||
@@ -1,24 +0,0 @@
|
||||
.. _`aipolicy`:
|
||||
|
||||
AI-assisted contribution policy
|
||||
===============================
|
||||
|
||||
pretix is maintained by humans.
|
||||
Every discussion, issue, and pull request is read and reviewed by humans (and sometimes machines, too).
|
||||
We ask you to respect the time and effort put in by these humans by not sending low-effort, unqualified work, since it puts the burden of validation on the maintainer.
|
||||
|
||||
Therefore, the pretix project has strict rules for AI usage:
|
||||
|
||||
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
|
||||
|
||||
- **The human-in-the-loop must fully understand all code.** If you can't explain what your changes do and how they interact with the greater system without the aid of AI tools, do not contribute to this project.
|
||||
|
||||
- **Issues and discussions can use AI assistance but must have a full human-in-the-loop.** This means that any content generated with AI must have been reviewed and edited by a human before submission. AI is very good at being overly verbose and including noise that distracts from the main point. Humans must do their research and trim this down.
|
||||
|
||||
- **No AI-generated media is allowed (art, images, videos, audio, etc.).** Text and code are the only acceptable AI-generated content, per the other rules in this policy.
|
||||
|
||||
- **Bad AI drivers will be excluded from the project.** People who produce bad contributions that are clearly AI (slop) will be blocked from our organization without warning.
|
||||
|
||||
This policy was inspired by the `ghostty project`_.
|
||||
|
||||
.. _ghostty project: https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md
|
||||
@@ -1,39 +1,23 @@
|
||||
Contribution workflow
|
||||
=====================
|
||||
General remarks
|
||||
===============
|
||||
|
||||
You are interested in contributing to pretix? That is awesome!
|
||||
|
||||
If you’re new to contributing to open source software, don’t be afraid. We’ll happily review your code and give you
|
||||
constructive and friendly feedback on your changes. Every contribution should go through the following steps.
|
||||
constructive and friendly feedback on your changes.
|
||||
|
||||
Discussion & Design
|
||||
-------------------
|
||||
|
||||
pretix is a large and mature project with more of a decade of history and hopefully many more decades to come.
|
||||
Keeping pretix in good shape over long timeframes is first and foremost a fight against complexity.
|
||||
With every additional feature, complexity grows, and both features and complexity are hard to remove.
|
||||
|
||||
Even if you are doing the initial work of the contribution, accepting the contribution is not free for us.
|
||||
Not only will we need to maintain the feature, but every feature adds cost to the maintenance of every other feature it interacts with, and every feature adds effort for users to understand how pretix works.
|
||||
Therefore, we must carefully select what features we add, based on how well they fit the system in general and of how much use they will be to our larger user base.
|
||||
|
||||
We strongly ask you to **create a discussion on GitHub for every new feature idea** outlining the use case and the proposed implementation design.
|
||||
Pull requests without prior discussion will likely just be closed.
|
||||
|
||||
For bug fixes and very minor changes, you can skip this step and open a PR right away.
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
To develop your contribution, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
|
||||
First of all, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
|
||||
If you run into any problems on your way, please do not hesitate to ask us anytime!
|
||||
|
||||
While developing, please have a look at our :ref:`aipolicy` and our guidelines on :ref:`codestyle`.
|
||||
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
|
||||
assured that we will not tolerate any form of harassment.
|
||||
|
||||
Sending a patch
|
||||
---------------
|
||||
|
||||
Once you have a first draft of your changes, please `create a pull request`_ on our `GitHub repository`_.
|
||||
If you improved pretix in any way, we'd be very happy if you contribute it
|
||||
back to the main code base! The easiest way to do so is to `create a pull request`_
|
||||
on our `GitHub repository`_.
|
||||
|
||||
We recommend that you create a feature branch for every issue you work on so the changes can
|
||||
be reviewed individually.
|
||||
@@ -41,17 +25,14 @@ Please use the test suite to check whether your changes break any existing featu
|
||||
the code style checks to confirm you are consistent with pretix's coding style. You'll
|
||||
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
|
||||
|
||||
We automatically run the tests and the code style check on every pull request through GitHub Actions and we won’t
|
||||
We automatically run the tests and the code style check on every pull request on Travis CI and we won’t
|
||||
accept any pull requests without all tests passing. However, if you don't find out *why* they are not passing,
|
||||
just send the pull request and tell us – we'll be glad to help.
|
||||
|
||||
If you add a new feature, please include appropriate documentation into your patch. If you fix a bug,
|
||||
please include a regression test, i.e. a test that fails without your changes and passes after applying your changes.
|
||||
|
||||
Again: If you get stuck, do not hesitate to contact us through GitHub discussions.
|
||||
|
||||
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
|
||||
assured that we will not tolerate any form of harassment.
|
||||
Again: If you get stuck, do not hesitate to contact any of us, or Raphael personally at mail@raphaelmichel.de.
|
||||
|
||||
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
|
||||
.. _GitHub repository: https://github.com/pretix/pretix
|
||||
|
||||
@@ -6,5 +6,4 @@ Contributing to pretix
|
||||
|
||||
general
|
||||
style
|
||||
ai
|
||||
codeofconduct
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.. spelling:word-list:: Rebase rebasing
|
||||
|
||||
.. _`codestyle`:
|
||||
|
||||
Coding style and quality
|
||||
========================
|
||||
|
||||
@@ -30,6 +28,8 @@ Code
|
||||
Commits and Pull Requests
|
||||
-------------------------
|
||||
|
||||
|
||||
|
||||
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
|
||||
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
|
||||
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
|
||||
|
||||
@@ -33,9 +33,9 @@ dependencies = [
|
||||
"bleach==6.3.*",
|
||||
"celery==5.6.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=46.0.7",
|
||||
"cryptography>=44.0.0",
|
||||
"css-inline==0.20.*",
|
||||
"defusedcsv>=3.0.0",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==5.2.*",
|
||||
"django-bootstrap3==26.1",
|
||||
@@ -93,11 +93,11 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.58.*",
|
||||
"sentry-sdk==2.57.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2026041800",
|
||||
"tlds>=2020041600",
|
||||
"tqdm==4.*",
|
||||
"ua-parser==1.0.*",
|
||||
"vobject==0.9.*",
|
||||
@@ -117,7 +117,7 @@ dev = [
|
||||
"isort==8.0.*",
|
||||
"pep8-naming==0.15.*",
|
||||
"potypo",
|
||||
"pytest-asyncio>=1.3.0",
|
||||
"pytest-asyncio>=0.24",
|
||||
"pytest-cache",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.*",
|
||||
|
||||
@@ -871,7 +871,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'og_image',
|
||||
'name_scheme',
|
||||
'reusable_media_active',
|
||||
'reusable_media_usage_enforced',
|
||||
'reusable_media_type_barcode',
|
||||
'reusable_media_type_barcode_identifier_length',
|
||||
'reusable_media_type_nfc_uid',
|
||||
@@ -886,7 +885,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
readonly_fields = [
|
||||
# These are read-only since they are currently only settable on organizers, not events
|
||||
'reusable_media_active',
|
||||
'reusable_media_usage_enforced',
|
||||
'reusable_media_type_barcode',
|
||||
'reusable_media_type_barcode_identifier_length',
|
||||
'reusable_media_type_nfc_uid',
|
||||
|
||||
@@ -605,7 +605,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
'cookie_consent_dialog_button_yes',
|
||||
'cookie_consent_dialog_button_no',
|
||||
'reusable_media_active',
|
||||
'reusable_media_usage_enforced',
|
||||
'reusable_media_type_barcode',
|
||||
'reusable_media_type_barcode_identifier_length',
|
||||
'reusable_media_type_nfc_uid',
|
||||
|
||||
@@ -69,8 +69,7 @@ from pretix.base.models import (
|
||||
from pretix.base.models.orders import PrintLog
|
||||
from pretix.base.permissions import AnyPermissionOf
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredMediaExchangeError, RequiredQuestionsError, SQLLogic,
|
||||
perform_checkin,
|
||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||
)
|
||||
from pretix.base.signals import checkin_annulled
|
||||
from pretix.helpers import OF_SELF
|
||||
@@ -455,8 +454,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
|
||||
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
||||
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
||||
media_exchange_supported, source_type='barcode', legacy_url_support=False, simulate=False,
|
||||
gate=None, use_order_locale=False):
|
||||
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False):
|
||||
if not checkinlists:
|
||||
raise ValidationError('No check-in list passed.')
|
||||
|
||||
@@ -465,7 +463,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
|
||||
device = auth if isinstance(auth, Device) else None
|
||||
gate = gate or (auth.gate if isinstance(auth, Device) else None)
|
||||
media = None
|
||||
|
||||
context = {
|
||||
'request': request,
|
||||
@@ -747,7 +744,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
datetime=datetime,
|
||||
questions_supported=questions_supported,
|
||||
canceled_supported=canceled_supported,
|
||||
media_exchange_supported=media_exchange_supported,
|
||||
user=user,
|
||||
auth=auth,
|
||||
type=checkin_type,
|
||||
@@ -756,7 +752,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
from_revoked_secret=from_revoked_secret,
|
||||
simulate=simulate,
|
||||
gate=gate,
|
||||
reusable_media=media,
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
return Response({
|
||||
@@ -769,16 +764,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
],
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
except RequiredMediaExchangeError as e:
|
||||
return Response({
|
||||
'status': 'exchange',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'media_policy': e.media_policy,
|
||||
'media_type': e.media_type,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
except CheckInError as e:
|
||||
if not simulate:
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
@@ -928,7 +913,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
||||
questions_supported=self.request.data.get('questions_supported', True),
|
||||
canceled_supported=self.request.data.get('canceled_supported', False),
|
||||
media_exchange_supported=self.request.data.get('media_exchange_supported', False),
|
||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||
legacy_url_support=True,
|
||||
)
|
||||
@@ -965,7 +949,6 @@ class CheckinRPCRedeemView(views.APIView):
|
||||
questions_supported=s.validated_data['questions_supported'],
|
||||
use_order_locale=s.validated_data['use_order_locale'],
|
||||
canceled_supported=True,
|
||||
media_exchange_supported=s.validated_data.get('media_exchange_supported', False),
|
||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||
legacy_url_support=False,
|
||||
)
|
||||
|
||||
@@ -1103,25 +1103,13 @@ class PaymentListExporter(ListExporter):
|
||||
def iterate_list(self, form_data):
|
||||
provider_names = dict(get_all_payment_providers())
|
||||
|
||||
i_numbers = Invoice.objects.filter(
|
||||
order=OuterRef('order_id'),
|
||||
).values('order').annotate(
|
||||
m=GroupConcat('full_invoice_no', delimiter=', ')
|
||||
).values(
|
||||
'm'
|
||||
).order_by()
|
||||
|
||||
payments = OrderPayment.objects.filter(
|
||||
order__event__in=self.events,
|
||||
state__in=form_data.get('payment_states', [])
|
||||
).annotate(
|
||||
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
|
||||
).select_related('order').prefetch_related('order__event').order_by('created')
|
||||
refunds = OrderRefund.objects.filter(
|
||||
order__event__in=self.events,
|
||||
state__in=form_data.get('refund_states', [])
|
||||
).annotate(
|
||||
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
|
||||
).select_related('order').prefetch_related('order__event').order_by('created')
|
||||
|
||||
if form_data.get('end_date_range'):
|
||||
@@ -1147,7 +1135,6 @@ class PaymentListExporter(ListExporter):
|
||||
headers = [
|
||||
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
|
||||
_('Status code'), _('Amount'), _('Payment method'), _('Comment'), _('Matching ID'), _('Payment details'),
|
||||
_('Invoice numbers'),
|
||||
]
|
||||
yield headers
|
||||
|
||||
@@ -1185,7 +1172,6 @@ class PaymentListExporter(ListExporter):
|
||||
obj.comment if isinstance(obj, OrderRefund) else "",
|
||||
matching_id,
|
||||
payment_details,
|
||||
obj.order_invoice_numbers,
|
||||
]
|
||||
yield row
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
|
||||
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import URL_RE, rich_text
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
||||
@@ -227,15 +227,9 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
# bots.
|
||||
r'^[^$€/%§{}<>~]*$',
|
||||
message=_('Please do not use special characters in names.')
|
||||
),
|
||||
RegexValidator(
|
||||
URL_RE,
|
||||
inverse_match=True,
|
||||
message=_('Please do not use special characters in names.')
|
||||
)
|
||||
]
|
||||
}
|
||||
self.max_length = defaults['max_length']
|
||||
self.scheme_name = kwargs.pop('scheme')
|
||||
self.titles = kwargs.pop('titles')
|
||||
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
|
||||
@@ -293,7 +287,7 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
if self.require_all_fields and not all(v for v in value):
|
||||
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
|
||||
|
||||
if sum(len(v) for v in value.values() if v) > (self.max_length or 250):
|
||||
if sum(len(v) for v in value.values() if v) > 250:
|
||||
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
|
||||
|
||||
if value.get("salutation") == "empty":
|
||||
|
||||
@@ -89,7 +89,7 @@ class NfcUidMediaType(BaseMediaType):
|
||||
icon = 'pretixbase/img/media/nfc_uid.svg'
|
||||
medium_created_by_server = False
|
||||
supports_giftcard = True
|
||||
supports_orderposition = True
|
||||
supports_orderposition = False
|
||||
|
||||
def handle_unknown(self, organizer, identifier, user, auth):
|
||||
from pretix.base.models import GiftCard, ReusableMedium
|
||||
@@ -129,7 +129,7 @@ class NfcMf0aesMediaType(BaseMediaType):
|
||||
icon = 'pretixbase/img/media/nfc_secure.svg'
|
||||
medium_created_by_server = False
|
||||
supports_giftcard = True
|
||||
supports_orderposition = True
|
||||
supports_orderposition = False
|
||||
|
||||
def handle_new(self, organizer, medium, user, auth):
|
||||
from pretix.base.models import GiftCard
|
||||
|
||||
@@ -351,7 +351,6 @@ class Checkin(models.Model):
|
||||
REASON_UNAPPROVED = 'unapproved'
|
||||
REASON_INVALID_TIME = 'invalid_time'
|
||||
REASON_ANNULLED = 'annulled'
|
||||
REASON_ALREADY_EXCHANGED = 'already_exchanged'
|
||||
REASONS = (
|
||||
(REASON_CANCELED, _('Order canceled')),
|
||||
(REASON_INVALID, _('Unknown ticket')),
|
||||
@@ -367,7 +366,6 @@ class Checkin(models.Model):
|
||||
(REASON_UNAPPROVED, _('Order not approved')),
|
||||
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
|
||||
(REASON_ANNULLED, _('Check-in annulled')),
|
||||
(REASON_ALREADY_EXCHANGED, _('Ticket already exchanged')),
|
||||
)
|
||||
|
||||
successful = models.BooleanField(
|
||||
|
||||
@@ -867,15 +867,6 @@ class RequiredQuestionsError(Exception):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class RequiredMediaExchangeError(Exception):
|
||||
def __init__(self, msg, code, media_policy, media_type):
|
||||
self.msg = msg
|
||||
self.code = code
|
||||
self.media_policy = media_policy
|
||||
self.media_type = media_type
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def _save_answers(op, answers, given_answers):
|
||||
def _create_answer(question, answer):
|
||||
try:
|
||||
@@ -948,7 +939,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
|
||||
gate=None, media_exchange_supported=False, reusable_media=None):
|
||||
gate=None):
|
||||
"""
|
||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||
not valid at this time.
|
||||
@@ -960,8 +951,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
questions are not filled out.
|
||||
:param ignore_unpaid: When set to True, this will succeed even when the order is unpaid.
|
||||
:param questions_supported: When set to False, questions are ignored
|
||||
:param media_exchange_supported: When set to False, media exchanges are ignored and access with un-exchanged media
|
||||
might be permitted
|
||||
:param nonce: A random nonce to prevent race conditions.
|
||||
:param datetime: The datetime of the checkin, defaults to now.
|
||||
:param simulate: If true, the check-in is not saved.
|
||||
@@ -1112,33 +1101,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
require_answers
|
||||
)
|
||||
|
||||
required_media_policy = op.item.media_policy
|
||||
required_media_type = op.item.media_type
|
||||
linked_media = op.linked_media
|
||||
require_media_exchange = required_media_policy and required_media_type and not linked_media.exists()
|
||||
if require_media_exchange and not force and media_exchange_supported:
|
||||
raise RequiredMediaExchangeError(
|
||||
_('You need to exchange your ticket to complete this check-in.'),
|
||||
'exchange',
|
||||
required_media_policy,
|
||||
required_media_type
|
||||
)
|
||||
|
||||
require_reusable_media_usage = required_media_policy and required_media_type and op.organizer.settings.reusable_media_usage_enforced
|
||||
if require_reusable_media_usage and not force:
|
||||
if not reusable_media and not linked_media.exists() and media_exchange_supported:
|
||||
raise RequiredMediaExchangeError(
|
||||
_('You need to exchange your ticket to complete this check-in.'),
|
||||
'exchange',
|
||||
required_media_policy,
|
||||
required_media_type
|
||||
)
|
||||
elif not reusable_media and linked_media.exists():
|
||||
raise CheckInError(
|
||||
_('This ticket has already been exchanged - use the reusable medium instead.'),
|
||||
'already_exchanged',
|
||||
)
|
||||
|
||||
device = None
|
||||
if isinstance(auth, Device):
|
||||
device = auth
|
||||
|
||||
@@ -38,7 +38,6 @@ SOURCE_NAMES = {
|
||||
None: _('European Central Bank'), # backwards-compatibility
|
||||
'eu:ecb:eurofxref-daily': _('European Central Bank'),
|
||||
'cz:cnb:rate-fixing-daily': _('Czech National Bank'),
|
||||
'pl:nbp:table-a': _('National Bank of Poland'),
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +49,6 @@ def fetch_rates(sender, **kwargs):
|
||||
source_tasks = {
|
||||
'eu:ecb:eurofxref-daily': fetch_ecb_rates,
|
||||
'cz:cnb:rate-fixing-daily': fetch_cnb_cz_rates,
|
||||
'pl:nbp:table-a': fetch_nbp_pl_rates,
|
||||
}
|
||||
|
||||
for source_name, task in source_tasks.items():
|
||||
@@ -146,29 +144,3 @@ def fetch_cnb_cz_rates():
|
||||
rate=rate,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.task()
|
||||
def fetch_nbp_pl_rates():
|
||||
"""
|
||||
Fetches currency rates from the Polish National Bank.
|
||||
"""
|
||||
r = requests.get("https://api.nbp.pl/api/exchangerates/tables/A/", headers={
|
||||
"Accept": "application/json",
|
||||
})
|
||||
r.raise_for_status()
|
||||
data = r.json()[0]
|
||||
|
||||
source_date = datetime.strptime(data["effectiveDate"], "%Y-%m-%d").date()
|
||||
|
||||
for r in data["rates"]:
|
||||
rate = Decimal(r["mid"]).quantize(Decimal('0.000001'))
|
||||
ExchangeRate.objects.update_or_create(
|
||||
source='pl:nbp:table-a',
|
||||
source_currency=r["code"],
|
||||
other_currency='PLN',
|
||||
defaults=dict(
|
||||
source_date=source_date,
|
||||
rate=rate,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -205,19 +205,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
|
||||
invoice.foreign_currency_rate_date = rate.source_date
|
||||
invoice.foreign_currency_source = 'cz:cnb:rate-fixing-daily'
|
||||
elif invoice.event.settings.invoice_eu_currencies == 'PLN' and invoice.event.currency != 'PLN':
|
||||
invoice.foreign_currency_display = 'PLN'
|
||||
if settings.FETCH_ECB_RATES:
|
||||
rate = ExchangeRate.objects.filter(
|
||||
source='pl:nbp:table-a',
|
||||
source_currency=invoice.event.currency,
|
||||
other_currency=invoice.foreign_currency_display,
|
||||
source_date__gt=now().date() - timedelta(days=7)
|
||||
).first()
|
||||
if rate:
|
||||
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
|
||||
invoice.foreign_currency_rate_date = rate.source_date
|
||||
invoice.foreign_currency_source = 'pl:nbp:table-a'
|
||||
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
|
||||
@@ -217,19 +217,6 @@ DEFAULTS = {
|
||||
"later.")
|
||||
)
|
||||
},
|
||||
'reusable_media_usage_enforced': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Enforce the usage of issued re-usable media for check-in"),
|
||||
help_text=_("If enabled, a ticket barcode will not be accepted anymore, if a re-usable medium has been "
|
||||
"created and linked to a ticket. Keeping this option turned off will treat the re-usable "
|
||||
"medium and ticket as equals."),
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-reusable_media_active'}),
|
||||
)
|
||||
},
|
||||
'reusable_media_type_barcode': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -587,7 +574,6 @@ DEFAULTS = {
|
||||
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
|
||||
'country that uses a different currency.')),
|
||||
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
|
||||
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
|
||||
),
|
||||
),
|
||||
'serializer_kwargs': dict(
|
||||
@@ -596,7 +582,6 @@ DEFAULTS = {
|
||||
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
|
||||
'country that uses a different currency.')),
|
||||
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
|
||||
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -192,11 +192,6 @@ class CheckinListSimulatorForm(forms.Form):
|
||||
initial=True,
|
||||
required=False,
|
||||
)
|
||||
media_exchange_supported = forms.BooleanField(
|
||||
label=_("Support for media exchange"),
|
||||
initial=True,
|
||||
required=False,
|
||||
)
|
||||
gate = SafeModelChoiceField(
|
||||
label=_('Gate'),
|
||||
empty_label=_('All gates'),
|
||||
|
||||
@@ -627,7 +627,6 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
'cookie_consent_dialog_button_yes',
|
||||
'cookie_consent_dialog_button_no',
|
||||
'reusable_media_active',
|
||||
'reusable_media_usage_enforced',
|
||||
'reusable_media_type_barcode',
|
||||
'reusable_media_type_barcode_identifier_length',
|
||||
'reusable_media_type_nfc_uid',
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
{% bootstrap_field form.gate layout="control" %}
|
||||
{% bootstrap_field form.ignore_unpaid layout="control" %}
|
||||
{% bootstrap_field form.questions_supported layout="control" %}
|
||||
{% bootstrap_field form.media_exchange_supported layout="control" %}
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@@ -54,8 +53,6 @@
|
||||
<span class="fa fa-check-circle"></span>
|
||||
{% elif result.status == "incomplete" %}
|
||||
<span class="fa fa-question-circle"></span>
|
||||
{% elif result.status == "exchange" %}
|
||||
<span class="fa fa-recycle"></span>
|
||||
{% elif result.status == "error" %}
|
||||
{% if result.reason == "already_redeemed" %}
|
||||
<span class="fa fa-warning"></span>
|
||||
@@ -81,14 +78,6 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif result.status == "exchange" %}
|
||||
<h3 class="nomargin-top">{% trans "Media exchange required" %}</h3>
|
||||
<p>
|
||||
{% blocktrans trimmed with media_policy=media_policies|getitem:result.media_policy media_type=media_types|getitem:result.media_type %}
|
||||
This ticket needs to be exchanged into a <strong>{{ media_type }}</strong> re-usable medium.
|
||||
<strong>{{ media_policy }}</strong>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% elif result.status == "error" %}
|
||||
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
|
||||
{% if result.reason_explanation %}
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Reusable media" %}</legend>
|
||||
{% bootstrap_field sform.reusable_media_active layout="control" %}
|
||||
{% bootstrap_field sform.reusable_media_usage_enforced layout="control" %}
|
||||
<div data-display-dependency="#{{ sform.reusable_media_active.id_for_label }}">
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
@@ -50,7 +50,7 @@ from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.api.views.checkin import _redeem_process
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, Item, LogEntry, Order, OrderPosition
|
||||
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.models.orders import PrintLog
|
||||
from pretix.base.permissions import AnyPermissionOf
|
||||
@@ -532,8 +532,6 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
||||
checkinlist=self.list,
|
||||
result=self.result,
|
||||
reason_labels=dict(Checkin.REASONS),
|
||||
media_policies=dict(Item.MEDIA_POLICIES),
|
||||
media_types=dict(MEDIA_TYPES),
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
@@ -553,7 +551,6 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
||||
pdf_data=False,
|
||||
questions_supported=form.cleaned_data["questions_supported"],
|
||||
canceled_supported=False,
|
||||
media_exchange_supported=form.cleaned_data["media_exchange_supported"],
|
||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||
legacy_url_support=False,
|
||||
simulate=True,
|
||||
|
||||
@@ -619,7 +619,7 @@ def checkinlist_select2(request, **kwargs):
|
||||
|
||||
qs = request.event.checkin_lists.select_related('subevent').filter(
|
||||
qf
|
||||
).order_by('subevent__date_from', 'name', 'pk')
|
||||
).order_by('name')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
|
||||
@@ -32,8 +32,11 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -165,6 +168,46 @@ class QuestionsForm(BaseQuestionsForm):
|
||||
)
|
||||
|
||||
|
||||
class AddOnRadioSelect(forms.RadioSelect):
|
||||
option_template_name = 'pretixpresale/forms/addon_choice_option.html'
|
||||
|
||||
def optgroups(self, name, value, attrs=None):
|
||||
attrs = attrs or {}
|
||||
groups = []
|
||||
has_selected = False
|
||||
for index, (option_value, option_label, option_desc) in enumerate(chain(self.choices)):
|
||||
if option_value is None:
|
||||
option_value = ''
|
||||
if isinstance(option_label, (list, tuple)):
|
||||
raise TypeError('Choice groups are not supported here')
|
||||
group_name = None
|
||||
subgroup = []
|
||||
groups.append((group_name, subgroup, index))
|
||||
|
||||
selected = (
|
||||
force_str(option_value) in value and
|
||||
(has_selected is False or self.allow_multiple_selected)
|
||||
)
|
||||
if selected is True and has_selected is False:
|
||||
has_selected = True
|
||||
attrs['description'] = option_desc
|
||||
subgroup.append(self.create_option(
|
||||
name, option_value, option_label, selected, index,
|
||||
subindex=None, attrs=attrs,
|
||||
))
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
class AddOnVariationField(forms.ChoiceField):
|
||||
def valid_value(self, value):
|
||||
text_value = force_str(value)
|
||||
for k, v, d in self.choices:
|
||||
if value == k or text_value == force_str(k):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MembershipForm(forms.Form):
|
||||
required_css_class = 'required'
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ class RegistrationForm(forms.Form):
|
||||
)
|
||||
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=70,
|
||||
max_length=255,
|
||||
required=True,
|
||||
scheme=request.organizer.settings.name_scheme,
|
||||
titles=request.organizer.settings.name_scheme_titles,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{% load rich_text %}
|
||||
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% include "django/forms/widgets/input.html" %} {{ widget.label }}</label> {% if widget.attrs.description %}<span class="fa fa-info-circle toggle-variation-description" aria-hidden="true"></span>
|
||||
<div class="variation-description addon-variation-description">{{ widget.attrs.description|rich_text }}</div>{% endif %}
|
||||
@@ -114,10 +114,8 @@ class CartMixin:
|
||||
return cached_invoice_address(self.request)
|
||||
|
||||
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
if not get_or_create_cart_id(self.request, create=False) and not order:
|
||||
# The user has no cart, so we can save a lot of work
|
||||
if not self.request.session.session_key and not order:
|
||||
# The user has not even a session ID yet, so they can't have a cart and we can save a lot of work
|
||||
return {
|
||||
'positions': [],
|
||||
# Other keys are not used on non-checkout pages
|
||||
@@ -379,13 +377,9 @@ def cart_exists(request):
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
if not hasattr(request, '_cart_cache'):
|
||||
cid = get_or_create_cart_id(request, create=False)
|
||||
if cid:
|
||||
return CartPosition.objects.filter(
|
||||
cart_id=cid, event=request.event
|
||||
).exists()
|
||||
else:
|
||||
return False
|
||||
return CartPosition.objects.filter(
|
||||
cart_id=get_or_create_cart_id(request), event=request.event
|
||||
).exists()
|
||||
return bool(request._cart_cache)
|
||||
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
|
||||
});
|
||||
|
||||
var form_handlers = function (el) {
|
||||
el.trigger("rescan.areYouSure");
|
||||
el.find("[data-formset]").formset(
|
||||
{
|
||||
animateForms: true,
|
||||
|
||||
@@ -864,9 +864,6 @@ tbody th {
|
||||
.checkin-sim-result-status-incomplete {
|
||||
background: $brand-primary;
|
||||
}
|
||||
.checkin-sim-result-status-exchange {
|
||||
background: $brand-primary;
|
||||
}
|
||||
.checkin-sim-result-status-error {
|
||||
background: $brand-danger;
|
||||
}
|
||||
|
||||
@@ -123,8 +123,6 @@ def env():
|
||||
ExchangeRate.objects.create(source_date=date.today(), source='eu:ecb:eurofxref-daily', source_currency='EUR', other_currency=currency, rate=rate)
|
||||
ExchangeRate.objects.create(source_date=date.today(), source='cz:cnb:rate-fixing-daily', source_currency='EUR',
|
||||
other_currency='CZK', rate=Decimal('25.0000'))
|
||||
ExchangeRate.objects.create(source_date=date.today(), source='pl:nbp:table-a', source_currency='EUR',
|
||||
other_currency='PLN', rate=Decimal('4.2355'))
|
||||
yield event, o
|
||||
|
||||
|
||||
@@ -349,23 +347,6 @@ def test_invoice_indirect_currency_conversion(env):
|
||||
assert inv.foreign_currency_source == 'eu:ecb:eurofxref-daily'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invoice_pln_currency_conversion(env):
|
||||
event, order = env
|
||||
event.settings.invoice_eu_currencies = 'PLN'
|
||||
|
||||
event.settings.set('invoice_language', 'en')
|
||||
InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', zipcode='12345', city='Warsaw',
|
||||
country=Country('PL'), vat_id='PL123456780', vat_id_validated=True, order=order,
|
||||
is_business=True)
|
||||
|
||||
inv = generate_invoice(order)
|
||||
assert inv.foreign_currency_display == "PLN"
|
||||
assert inv.foreign_currency_rate == Decimal("4.2355")
|
||||
assert inv.foreign_currency_rate_date == date.today()
|
||||
assert inv.foreign_currency_source == 'pl:nbp:table-a'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invoice_czk_currency_conversion(env):
|
||||
event, order = env
|
||||
|
||||
@@ -33,7 +33,7 @@ from django.conf import settings
|
||||
from django.core import mail as djmail
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.signing import dumps
|
||||
from django.test import Client, TestCase, TransactionTestCase
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
@@ -4413,18 +4413,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
|
||||
assert len(djmail.outbox) == 1
|
||||
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
|
||||
|
||||
def test_checkout_empty_session_valid_cart(self):
|
||||
client = Client()
|
||||
with scopes_disabled():
|
||||
api_cid = "{}@api".format(get_random_string(48))
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=api_cid, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
|
||||
response = client.get('/%s/%s/w/1234567890abcdef/checkout/questions/' % (self.orga.slug, self.event.slug), query_params={"take_cart_id": api_cid})
|
||||
assert '€23.00' in response.content.decode()
|
||||
|
||||
|
||||
class CheckoutTransactionTestCase(BaseCheckoutTestCase, TransactionTestCase):
|
||||
def test_order_confirmation_mail_invoice_sent_somewhere_else(self):
|
||||
|
||||
Reference in New Issue
Block a user