mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df6247fb5 | ||
|
|
74ada1ca08 |
@@ -16,6 +16,8 @@ recursive-include src/pretix/plugins/banktransfer/templates *
|
|||||||
recursive-include src/pretix/plugins/banktransfer/static *
|
recursive-include src/pretix/plugins/banktransfer/static *
|
||||||
recursive-include src/pretix/plugins/manualpayment/templates *
|
recursive-include src/pretix/plugins/manualpayment/templates *
|
||||||
recursive-include src/pretix/plugins/manualpayment/static *
|
recursive-include src/pretix/plugins/manualpayment/static *
|
||||||
|
recursive-include src/pretix/plugins/paypal/templates *
|
||||||
|
recursive-include src/pretix/plugins/paypal/static *
|
||||||
recursive-include src/pretix/plugins/paypal2/templates *
|
recursive-include src/pretix/plugins/paypal2/templates *
|
||||||
recursive-include src/pretix/plugins/paypal2/static *
|
recursive-include src/pretix/plugins/paypal2/static *
|
||||||
recursive-include src/pretix/plugins/src/pretixdroid/templates *
|
recursive-include src/pretix/plugins/src/pretixdroid/templates *
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
Event Meta Properties
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
An event meta property is used to to define meta information fields for its events.
|
|
||||||
This information can be re-used, for example, in ticket layouts.
|
|
||||||
|
|
||||||
The event meta property resource contains the following public fields:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Unique ID for this property
|
|
||||||
name string Name of the property
|
|
||||||
default string Value of the default option
|
|
||||||
required boolean If ``true``, an event can only be taken live if the
|
|
||||||
property is set. In event series, it's always optional
|
|
||||||
to set a value for individual dates
|
|
||||||
protected boolean If ``true``, the value for an event can only be changed
|
|
||||||
by organizer-level administrators
|
|
||||||
filter_public boolean If ``true``, this property will be shown to filter
|
|
||||||
events in the public event list and calendar
|
|
||||||
public_label string Public name of the property
|
|
||||||
filter_allowed boolean If ``true``, this property will be shown to filter
|
|
||||||
events or reports in the backend, and it can also be
|
|
||||||
used for hidden filter parameters in the frontend
|
|
||||||
choices list of objects List of JSON objects representing all permitted values
|
|
||||||
for this property, or ``null`` for no limitation.
|
|
||||||
Each choice object has a required internal name named
|
|
||||||
``key`` and optional public name named ``label``
|
|
||||||
consisting of a dictionary of i18n string translations
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/
|
|
||||||
|
|
||||||
Returns a list of all meta properties for the organizer.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/meta_properties/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Color",
|
|
||||||
"default": "blue",
|
|
||||||
"required": false,
|
|
||||||
"protected": false,
|
|
||||||
"filter_public": false,
|
|
||||||
"public_label": {},
|
|
||||||
"filter_allowed": true,
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"key": "blue",
|
|
||||||
"label": {
|
|
||||||
"en": "Blue"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
|
|
||||||
|
|
||||||
Returns information on one property, identified by its id.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Color",
|
|
||||||
"default": "blue",
|
|
||||||
"required": false,
|
|
||||||
"protected": false,
|
|
||||||
"filter_public": false,
|
|
||||||
"public_label": {},
|
|
||||||
"filter_allowed": true,
|
|
||||||
"choices": null
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer
|
|
||||||
:param id: The ``id`` field of the meta property to retrieve
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/event_meta_properties/
|
|
||||||
|
|
||||||
Creates a new meta property
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/event_meta_properties/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "ref-code",
|
|
||||||
"default": "abcde",
|
|
||||||
"required": true,
|
|
||||||
"choices": null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "reference",
|
|
||||||
"default": "abcde",
|
|
||||||
"required": true,
|
|
||||||
"protected": false,
|
|
||||||
"filter_public": false,
|
|
||||||
"public_label": null,
|
|
||||||
"filter_allowed": true,
|
|
||||||
"choices": null
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer
|
|
||||||
:statuscode 201: no error
|
|
||||||
:statuscode 400: The meta property could not be created due to invalid submitted data.
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
|
|
||||||
|
|
||||||
Update a meta property. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide
|
|
||||||
all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the
|
|
||||||
fields that you want to change.
|
|
||||||
|
|
||||||
You can change all fields of the resource except the ``id`` field.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
PATCH /api/v1/organizers/bigevents/event_meta_properties/2/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
Content-Length: 94
|
|
||||||
|
|
||||||
{
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "reference",
|
|
||||||
"default": "abcde",
|
|
||||||
"required": false,
|
|
||||||
"protected": false,
|
|
||||||
"filter_public": false,
|
|
||||||
"public_label": null,
|
|
||||||
"filter_allowed": true,
|
|
||||||
"choices": null
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer
|
|
||||||
:param id: The ``id`` field of the meta property to modify
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: The property could not be modified due to invalid submitted data
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
|
||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
|
|
||||||
|
|
||||||
Delete a meta property.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
DELETE /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
Vary: Accept
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer
|
|
||||||
:param id: The ``id`` field of the meta property to delete
|
|
||||||
:statuscode 204: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
|
||||||
@@ -110,7 +110,7 @@ Endpoints
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer",
|
"pretix.plugins.banktransfer",
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.ticketoutputpdf"
|
"pretix.plugins.ticketoutputpdf"
|
||||||
],
|
],
|
||||||
"all_sales_channels": false,
|
"all_sales_channels": false,
|
||||||
@@ -199,7 +199,7 @@ Endpoints
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer",
|
"pretix.plugins.banktransfer",
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.ticketoutputpdf"
|
"pretix.plugins.ticketoutputpdf"
|
||||||
],
|
],
|
||||||
"valid_keys": {
|
"valid_keys": {
|
||||||
@@ -262,7 +262,7 @@ Endpoints
|
|||||||
"item_meta_properties": {},
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"all_sales_channels": true,
|
"all_sales_channels": true,
|
||||||
"limit_sales_channels": []
|
"limit_sales_channels": []
|
||||||
@@ -299,7 +299,7 @@ Endpoints
|
|||||||
"item_meta_properties": {},
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"all_sales_channels": true,
|
"all_sales_channels": true,
|
||||||
"limit_sales_channels": [],
|
"limit_sales_channels": [],
|
||||||
@@ -364,7 +364,7 @@ Endpoints
|
|||||||
"item_meta_properties": {},
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"all_sales_channels": true,
|
"all_sales_channels": true,
|
||||||
"limit_sales_channels": []
|
"limit_sales_channels": []
|
||||||
@@ -401,7 +401,7 @@ Endpoints
|
|||||||
"item_meta_properties": {},
|
"item_meta_properties": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"all_sales_channels": true,
|
"all_sales_channels": true,
|
||||||
"limit_sales_channels": [],
|
"limit_sales_channels": [],
|
||||||
@@ -438,7 +438,7 @@ Endpoints
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer",
|
"pretix.plugins.banktransfer",
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.pretixdroid"
|
"pretix.plugins.pretixdroid"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -475,7 +475,7 @@ Endpoints
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer",
|
"pretix.plugins.banktransfer",
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal2",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.pretixdroid"
|
"pretix.plugins.pretixdroid"
|
||||||
],
|
],
|
||||||
"all_sales_channels": true,
|
"all_sales_channels": true,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ at :ref:`plugin-docs`.
|
|||||||
organizers
|
organizers
|
||||||
events
|
events
|
||||||
subevents
|
subevents
|
||||||
event_meta_properties
|
|
||||||
taxrules
|
taxrules
|
||||||
categories
|
categories
|
||||||
items
|
items
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ Endpoints
|
|||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string code: Only show the voucher with the given voucher code.
|
:query string code: Only show the voucher with the given voucher code.
|
||||||
:query string search: Only show the voucher with the given query found in the code, tag, or comment.
|
|
||||||
:query integer max_usages: Only show vouchers with the given maximal number of usages.
|
:query integer max_usages: Only show vouchers with the given maximal number of usages.
|
||||||
:query integer redeemed: Only show vouchers with the given number of redemptions. Note that this doesn't tell you if
|
:query integer redeemed: Only show vouchers with the given number of redemptions. Note that this doesn't tell you if
|
||||||
the voucher can still be redeemed, as this also depends on ``max_usages``. See the
|
the voucher can still be redeemed, as this also depends on ``max_usages``. See the
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ dependencies = [
|
|||||||
"Pillow==12.3.*",
|
"Pillow==12.3.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==7.36.*",
|
"protobuf==7.36.*",
|
||||||
"psycopg2-binary",
|
"psycopg[binary,pool]",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==3.0",
|
"pycparser==3.0",
|
||||||
"pycryptodome==3.23.*",
|
"pycryptodome==3.23.*",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ ignore =
|
|||||||
src/tests/plugins/*
|
src/tests/plugins/*
|
||||||
src/tests/plugins/badges/*
|
src/tests/plugins/badges/*
|
||||||
src/tests/plugins/banktransfer/*
|
src/tests/plugins/banktransfer/*
|
||||||
|
src/tests/plugins/paypal/*
|
||||||
src/tests/plugins/paypal2/*
|
src/tests/plugins/paypal2/*
|
||||||
src/tests/plugins/pretixdroid/*
|
src/tests/plugins/pretixdroid/*
|
||||||
src/tests/plugins/stripe/*
|
src/tests/plugins/stripe/*
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from django.db import transaction
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from i18nfield.rest_framework import I18nField
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -41,10 +40,9 @@ from pretix.api.serializers.settings import SettingsSerializer
|
|||||||
from pretix.base.auth import get_auth_backends
|
from pretix.base.auth import get_auth_backends
|
||||||
from pretix.base.i18n import get_language_without_region
|
from pretix.base.i18n import get_language_without_region
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, EventMetaProperty, GiftCard, GiftCardAcceptance,
|
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||||
GiftCardTransaction, Membership, MembershipType, OrderPosition, Organizer,
|
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||||
ReusableMedium, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
User,
|
|
||||||
)
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
from pretix.base.permissions import (
|
from pretix.base.permissions import (
|
||||||
@@ -642,88 +640,3 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
)
|
)
|
||||||
# TODO: make sure pub is always correct
|
# TODO: make sure pub is always correct
|
||||||
return 'pub/' + fname
|
return 'pub/' + fname
|
||||||
|
|
||||||
|
|
||||||
class MetaPropertyListField(serializers.ListField):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs["validators"] = kwargs.pop("validators", [])
|
|
||||||
|
|
||||||
def validate_keys_unique(choices):
|
|
||||||
if not choices:
|
|
||||||
return
|
|
||||||
keys = [c.get("key") for c in choices]
|
|
||||||
if len(set(keys)) < len(keys):
|
|
||||||
raise ValidationError("The key for each meta property value option must be unique.")
|
|
||||||
|
|
||||||
kwargs["validators"].append(
|
|
||||||
validate_keys_unique
|
|
||||||
)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class MetaPropertyDictField(serializers.DictField):
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.label_child = kwargs.pop("label_child", I18nField())
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
# django added unneccessary keys DELETE, ORDER through formsets, filter them here for backwards compat
|
|
||||||
d = {
|
|
||||||
"key": value["key"]
|
|
||||||
}
|
|
||||||
if "label" in value:
|
|
||||||
d["label"] = self.label_child.to_representation(value["label"])
|
|
||||||
|
|
||||||
return super().to_representation(d)
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
raise ValidationError("Meta property value options must be a dict.")
|
|
||||||
|
|
||||||
if not isinstance(data.get("key"), str):
|
|
||||||
raise ValidationError("Meta property value options must have a key of type string.")
|
|
||||||
|
|
||||||
if any(k not in {"key", "label"} for k in data.keys()):
|
|
||||||
raise ValidationError("Meta property value options may only have a key and optionally a label.")
|
|
||||||
|
|
||||||
if "label" in data:
|
|
||||||
try:
|
|
||||||
data["label"] = self.label_child.to_internal_value(data["label"])
|
|
||||||
except ValidationError as e:
|
|
||||||
raise ValidationError({"label": e.detail})
|
|
||||||
|
|
||||||
return super().to_internal_value(data)
|
|
||||||
|
|
||||||
|
|
||||||
class EventMetaPropertiesSerializer(I18nAwareModelSerializer):
|
|
||||||
choices = MetaPropertyListField(
|
|
||||||
child=MetaPropertyDictField(
|
|
||||||
label_child=I18nField()
|
|
||||||
),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = EventMetaProperty
|
|
||||||
fields = (
|
|
||||||
'id', 'name', 'default', 'required', 'protected', 'filter_public', 'public_label', 'filter_allowed',
|
|
||||||
'choices'
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
data = super().validate(data)
|
|
||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
|
||||||
full_data.update(data)
|
|
||||||
|
|
||||||
choices = full_data.get("choices")
|
|
||||||
default = full_data.get("default")
|
|
||||||
if choices and default:
|
|
||||||
choice_keys = [c.get("key") for c in choices]
|
|
||||||
if default not in choice_keys:
|
|
||||||
raise ValidationError("You cannot set a default value that is not a valid value.")
|
|
||||||
|
|
||||||
if not choices and "choices" in data:
|
|
||||||
# normalize empty dict to None
|
|
||||||
data["choices"] = None
|
|
||||||
return data
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportVie
|
|||||||
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
||||||
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
|
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
|
||||||
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
|
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
|
||||||
orga_router.register(r'event_meta_properties', organizer.EventMetaPropertiesViewSet)
|
|
||||||
|
|
||||||
team_router = routers.DefaultRouter()
|
team_router = routers.DefaultRouter()
|
||||||
team_router.register(r'members', organizer.TeamMemberViewSet)
|
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||||
|
|||||||
@@ -44,16 +44,15 @@ from pretix.api.models import OAuthAccessToken
|
|||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.organizer import (
|
from pretix.api.serializers.organizer import (
|
||||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||||
EventMetaPropertiesSerializer, GiftCardSerializer,
|
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||||
GiftCardTransactionSerializer, MembershipSerializer,
|
|
||||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||||
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, Event, EventMetaProperty, GiftCard, GiftCardTransaction,
|
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
|
||||||
LogEntry, Membership, MembershipType, Organizer, SalesChannel, SeatingPlan,
|
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
|
||||||
Team, TeamAPIToken, TeamInvite, User,
|
TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.plugins import (
|
from pretix.base.plugins import (
|
||||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||||
@@ -847,49 +846,3 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
|
|||||||
data={'id': instance.pk}
|
data={'id': instance.pk}
|
||||||
)
|
)
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
||||||
|
|
||||||
class EventMetaPropertiesViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = EventMetaPropertiesSerializer
|
|
||||||
queryset = EventMetaProperty.objects.none()
|
|
||||||
write_permission = 'organizer.settings.general:write'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.request.organizer.meta_properties.all()
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx['organizer'] = self.request.organizer
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
instance.log_action(
|
|
||||||
'pretix.property.deleted',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data={'id': instance.pk}
|
|
||||||
)
|
|
||||||
instance.delete()
|
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
inst = serializer.save(organizer_id=self.request.organizer.pk)
|
|
||||||
serializer.instance.log_action(
|
|
||||||
'pretix.property.created',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=self.request.data,
|
|
||||||
)
|
|
||||||
return inst
|
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
inst = serializer.save(organizer_id=self.request.organizer.pk)
|
|
||||||
serializer.instance.log_action(
|
|
||||||
'pretix.property.changed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=self.request.data,
|
|
||||||
)
|
|
||||||
return inst
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ with scopes_disabled():
|
|||||||
class VoucherFilter(FilterSet):
|
class VoucherFilter(FilterSet):
|
||||||
active = BooleanFilter(method='filter_active')
|
active = BooleanFilter(method='filter_active')
|
||||||
code = CharFilter(lookup_expr='iexact')
|
code = CharFilter(lookup_expr='iexact')
|
||||||
search = CharFilter(method='search_qs')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Voucher
|
model = Voucher
|
||||||
@@ -55,9 +54,6 @@ with scopes_disabled():
|
|||||||
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
|
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
|
||||||
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
||||||
|
|
||||||
def search_qs(self, qs, name, value):
|
|
||||||
return qs.filter(Q(code__icontains=value) | Q(tag__icontains=value) | Q(comment__icontains=value))
|
|
||||||
|
|
||||||
|
|
||||||
class VoucherViewSet(viewsets.ModelViewSet):
|
class VoucherViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = VoucherSerializer
|
serializer_class = VoucherSerializer
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ class UserSettingsForm(forms.ModelForm):
|
|||||||
class User2FADeviceAddForm(forms.Form):
|
class User2FADeviceAddForm(forms.Form):
|
||||||
name = forms.CharField(label=_('Device name'), max_length=64)
|
name = forms.CharField(label=_('Device name'), max_length=64)
|
||||||
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
||||||
('otp_totp.totpdevice', _('Smartphone with the Authenticator application')),
|
('totp', _('Smartphone with the Authenticator application')),
|
||||||
('pretixbase.webauthndevice', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1635,15 +1635,10 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
|
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
|
||||||
|
|
||||||
for k, v in list(self.fields.items()):
|
for k, v in list(self.fields.items()):
|
||||||
if k.endswith('_attendee'):
|
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
||||||
if not event.settings.attendee_emails_asked:
|
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
|
||||||
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
|
# the user interface with it
|
||||||
# the user interface with it
|
del self.fields[k]
|
||||||
del self.fields[k]
|
|
||||||
elif 'subject' in k and k.replace("subject", "send") in self.fields:
|
|
||||||
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("subject", "send")}'
|
|
||||||
elif 'text' in k and k.replace("text", "send") in self.fields:
|
|
||||||
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("text", "send")}'
|
|
||||||
|
|
||||||
|
|
||||||
class TicketSettingsForm(SettingsForm):
|
class TicketSettingsForm(SettingsForm):
|
||||||
|
|||||||
@@ -717,10 +717,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
|||||||
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
|
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
|
||||||
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
|
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
|
||||||
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
|
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
|
||||||
'pretix.property.created': _('An organizer meta property has been created.'),
|
|
||||||
'pretix.property.deleted': _('An organizer meta property has been deleted.'),
|
|
||||||
'pretix.property.changed': _('An organizer meta property has been changed.'),
|
|
||||||
'pretix.property.reordered': _('An organizer meta property has been reordered.'),
|
|
||||||
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
|
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
|
||||||
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||||
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
|
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
|
||||||
@@ -778,7 +774,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
|||||||
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
|
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
|
||||||
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
|
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
|
||||||
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
|
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
|
||||||
'pretix.user.settings.2fa.resetdrift': _('Drift and throttle values for two-factor devices have been reset.'),
|
|
||||||
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
|
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
|
||||||
'your account.'),
|
'your account.'),
|
||||||
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
|
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ class PermissionMiddleware:
|
|||||||
"user.settings.2fa.enable",
|
"user.settings.2fa.enable",
|
||||||
"user.settings.2fa.disable",
|
"user.settings.2fa.disable",
|
||||||
"user.settings.2fa.regenemergency",
|
"user.settings.2fa.regenemergency",
|
||||||
"user.settings.2fa.confirm.otp_totp.totpdevice",
|
"user.settings.2fa.confirm.totp",
|
||||||
"user.settings.2fa.confirm.pretixbase.webauthndevice",
|
"user.settings.2fa.confirm.webauthn",
|
||||||
"user.settings.2fa.delete",
|
"user.settings.2fa.delete",
|
||||||
"user.settings.2fa.leaveteams",
|
"user.settings.2fa.leaveteams",
|
||||||
"auth.logout",
|
"auth.logout",
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="form-group buttons">
|
<div class="form-group buttons">
|
||||||
<input type="submit" class="btn btn-large btn-default" value="{% trans "Cancel" %}"/>
|
<input type="submit" class="btn btn-large btn-default" value="Cancel"/>
|
||||||
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
|
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="big-radio radio">
|
<div class="big-radio radio">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" required value="otp_totp.totpdevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "otp_totp.totpdevice" %}checked{% endif %}>
|
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
|
||||||
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
|
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="big-radio radio">
|
<div class="big-radio radio">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" required value="pretixbase.webauthndevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "pretixbase.webauthndevice" %}checked{% endif %}>
|
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
|
||||||
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
|
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
|
|||||||
@@ -116,14 +116,14 @@
|
|||||||
{% for d in devices %}
|
{% for d in devices %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<a class="btn btn-danger btn-xs pull-right flip"
|
<a class="btn btn-danger btn-xs pull-right flip"
|
||||||
href="{% url "control:user.settings.2fa.delete" devicetype=d.model_label device=d.pk %}">
|
href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}">
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
{% if d.model_label == "otp_totp.totpdevice" %}
|
{% if d.devicetype == "totp" %}
|
||||||
<span class="fa fa-mobile"></span>
|
<span class="fa fa-mobile"></span>
|
||||||
{% elif d.model_label == "pretixbase.webauthndevice" %}
|
{% elif d.devicetype == "webauthn" %}
|
||||||
<span class="fa fa-usb"></span>
|
<span class="fa fa-usb"></span>
|
||||||
{% elif d.model_label == "pretixbase.u2fdevice" %}
|
{% elif d.devicetype == "u2f" %}
|
||||||
<span class="fa fa-usb"></span>
|
<span class="fa fa-usb"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ d.name }}
|
{{ d.name }}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{% extends "pretixcontrol/base.html" %}
|
{% extends "pretixcontrol/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load icon %}
|
|
||||||
{% block title %}{% trans "User" %}{% endblock %}
|
{% block title %}{% trans "User" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "User" %} {{ user.email }}</h1>
|
<h1>{% trans "User" %} {{ user.email }}</h1>
|
||||||
@@ -60,83 +59,8 @@
|
|||||||
{% bootstrap_field form.is_verified layout='control' %}
|
{% bootstrap_field form.is_verified layout='control' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field form.last_login layout='control' %}
|
{% bootstrap_field form.last_login layout='control' %}
|
||||||
{% bootstrap_field form.needs_password_change layout='control' %}
|
|
||||||
{% bootstrap_field form.require_2fa layout='control' %}
|
{% bootstrap_field form.require_2fa layout='control' %}
|
||||||
<div class="form-group">
|
{% bootstrap_field form.needs_password_change layout='control' %}
|
||||||
<div class="col-md-9 col-md-offset-3">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<button class="btn btn-default btn-xs pull-right" type="submit" form="resetdriftthrottle">
|
|
||||||
{% trans "Reset drift and throttle" %}
|
|
||||||
</button>
|
|
||||||
<h3 class="panel-title">
|
|
||||||
{% trans "Available two-factor authentication methods" %}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<table class="panel-body table table-hover">
|
|
||||||
{% for d in devices %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if d.model_label == 'otp_totp.totpdevice' %}
|
|
||||||
TOTP
|
|
||||||
{% elif d.model_label == 'pretixbase.u2fdevice' %}
|
|
||||||
U2F
|
|
||||||
{% elif d.model_label == 'pretixbase.webauthndevice' %}
|
|
||||||
WebAuthn
|
|
||||||
{% elif d.model_label == 'otp_static.staticdevice' %}
|
|
||||||
{% trans "Emergency tokens" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if d.confirmed %}
|
|
||||||
{% icon "check" %}
|
|
||||||
{% else %}
|
|
||||||
{% icon "warning" %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ d.name }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if d.throttling_failure_timestamp %}
|
|
||||||
{% blocktrans trimmed with date=d.throttling_failure_timestamp|date:"SHORT_DATETIME_FORMAT" count cnt=d.throttling_failure_count %}
|
|
||||||
1 failed attempt since {{ date }}
|
|
||||||
{% plural %}
|
|
||||||
{{ cnt }} failed attempts since {{ date }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
<br>
|
|
||||||
{% endif %}
|
|
||||||
{% if d.throttling_enabled and not d.verify_is_allowed.0 %}
|
|
||||||
<strong>
|
|
||||||
{% blocktrans trimmed with date=d.verify_is_allowed.1.locked_until|date:"SHORT_DATETIME_FORMAT" %}
|
|
||||||
Currently locked until {{ date }}
|
|
||||||
{% endblocktrans %}
|
|
||||||
</strong>
|
|
||||||
<br>
|
|
||||||
{% endif %}
|
|
||||||
{% if d.model_label == 'otp_totp.totpdevice' %}
|
|
||||||
<small>
|
|
||||||
<code>step = {{ d.step }},
|
|
||||||
t0 = {{ d.t0 }},
|
|
||||||
digits = {{ d.digits }},
|
|
||||||
tolerance = {{ d.tolerance }},
|
|
||||||
drift = {{ d.drift }},
|
|
||||||
last_t = {{ d.last_t }}</code>
|
|
||||||
</small>
|
|
||||||
{% elif d.model_label == 'pretixbase.u2fdevice' %}
|
|
||||||
<small>
|
|
||||||
<code>sign_count = {{ d.sign_count }}</code>
|
|
||||||
</small>
|
|
||||||
{% elif d.model_label == 'otp_static.staticdevice' %}
|
|
||||||
<small>
|
|
||||||
<code>token_count = {{ d.token_set.count }}</code>
|
|
||||||
</small>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Team memberships" %}</legend>
|
<legend>{% trans "Team memberships" %}</legend>
|
||||||
@@ -178,8 +102,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="{% url "control:users.resetdriftthrottle" id=user.pk %}" id="resetdriftthrottle" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ urlpatterns = [
|
|||||||
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
|
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
|
||||||
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
|
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
|
||||||
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
|
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
|
||||||
re_path(r'^users/(?P<id>\d+)/resetdriftthrottle$', users.Reset2FADriftThrottleView.as_view(), name='users.resetdriftthrottle'),
|
|
||||||
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
|
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
|
||||||
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
|
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
|
||||||
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
|
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
|
||||||
@@ -107,9 +106,9 @@ urlpatterns = [
|
|||||||
re_path(r'^settings/2fa/regenemergency', user.User2FARegenerateEmergencyView.as_view(),
|
re_path(r'^settings/2fa/regenemergency', user.User2FARegenerateEmergencyView.as_view(),
|
||||||
name='user.settings.2fa.regenemergency'),
|
name='user.settings.2fa.regenemergency'),
|
||||||
re_path(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
|
re_path(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
|
||||||
name='user.settings.2fa.confirm.otp_totp.totpdevice'),
|
name='user.settings.2fa.confirm.totp'),
|
||||||
re_path(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
|
re_path(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
|
||||||
name='user.settings.2fa.confirm.pretixbase.webauthndevice'),
|
name='user.settings.2fa.confirm.webauthn'),
|
||||||
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
||||||
name='user.settings.2fa.delete'),
|
name='user.settings.2fa.delete'),
|
||||||
re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
|
re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||||
from django_otp import devices_for_user
|
|
||||||
from django_otp.plugins.otp_static.models import StaticDevice
|
from django_otp.plugins.otp_static.models import StaticDevice
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
@@ -86,6 +85,7 @@ from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
|||||||
from pretix.helpers.security import session_reauth
|
from pretix.helpers.security import session_reauth
|
||||||
from pretix.helpers.u2f import websafe_encode
|
from pretix.helpers.u2f import websafe_encode
|
||||||
|
|
||||||
|
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -313,7 +313,17 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
|||||||
except StaticDevice.DoesNotExist:
|
except StaticDevice.DoesNotExist:
|
||||||
ctx['static_tokens_device'] = None
|
ctx['static_tokens_device'] = None
|
||||||
|
|
||||||
ctx['devices'] = [d for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)]
|
ctx['devices'] = []
|
||||||
|
for dt in REAL_DEVICE_TYPES:
|
||||||
|
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
|
||||||
|
for obj in objs:
|
||||||
|
if dt == TOTPDevice:
|
||||||
|
obj.devicetype = 'totp'
|
||||||
|
elif dt == U2FDevice:
|
||||||
|
obj.devicetype = 'u2f'
|
||||||
|
elif dt == WebAuthnDevice:
|
||||||
|
obj.devicetype = 'webauthn'
|
||||||
|
ctx['devices'] += objs
|
||||||
|
|
||||||
ctx['obligatory'] = None
|
ctx['obligatory'] = None
|
||||||
if settings.PRETIX_OBLIGATORY_2FA is True:
|
if settings.PRETIX_OBLIGATORY_2FA is True:
|
||||||
@@ -332,9 +342,9 @@ class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
|
|||||||
template_name = 'pretixcontrol/user/2fa_add.html'
|
template_name = 'pretixcontrol/user/2fa_add.html'
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if form.cleaned_data['devicetype'] == 'otp_totp.totpdevice':
|
if form.cleaned_data['devicetype'] == 'totp':
|
||||||
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
||||||
elif form.cleaned_data['devicetype'] == 'pretixbase.webauthndevice':
|
elif form.cleaned_data['devicetype'] == 'webauthn':
|
||||||
if not self.request.is_secure():
|
if not self.request.is_secure():
|
||||||
messages.error(self.request,
|
messages.error(self.request,
|
||||||
_('Security devices are only available if pretix is served via HTTPS.'))
|
_('Security devices are only available if pretix is served via HTTPS.'))
|
||||||
@@ -354,11 +364,11 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def device(self):
|
def device(self):
|
||||||
if self.kwargs['devicetype'] == 'otp_totp.totpdevice':
|
if self.kwargs['devicetype'] == 'totp':
|
||||||
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||||
elif self.kwargs['devicetype'] == 'pretixbase.webauthndevice':
|
elif self.kwargs['devicetype'] == 'webauthn':
|
||||||
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||||
elif self.kwargs['devicetype'] == 'pretixbase.u2fdevice':
|
elif self.kwargs['devicetype'] == 'u2f':
|
||||||
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@@ -376,7 +386,7 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
|||||||
msgs = [
|
msgs = [
|
||||||
_('A two-factor authentication device has been removed from your account.')
|
_('A two-factor authentication device has been removed from your account.')
|
||||||
]
|
]
|
||||||
if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)):
|
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
|
||||||
self.request.user.require_2fa = False
|
self.request.user.require_2fa = False
|
||||||
self.request.user.save()
|
self.request.user.save()
|
||||||
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
|
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
|
||||||
@@ -451,7 +461,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
|||||||
).first()
|
).first()
|
||||||
if credential_id_exists:
|
if credential_id_exists:
|
||||||
messages.error(request, _('This security device is already registered.'))
|
messages.error(request, _('This security device is already registered.'))
|
||||||
return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={
|
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
|
||||||
'device': self.device.pk
|
'device': self.device.pk
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -465,7 +475,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
|||||||
self.device.save()
|
self.device.save()
|
||||||
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
||||||
'id': self.device.pk,
|
'id': self.device.pk,
|
||||||
'devicetype': 'pretixbase.webauthndevice',
|
'devicetype': 'u2f',
|
||||||
'name': self.device.name,
|
'name': self.device.name,
|
||||||
})
|
})
|
||||||
notices = [
|
notices = [
|
||||||
@@ -493,7 +503,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
|||||||
except Exception:
|
except Exception:
|
||||||
messages.error(request, _('The registration could not be completed. Please try again.'))
|
messages.error(request, _('The registration could not be completed. Please try again.'))
|
||||||
logger.exception('WebAuthn registration failed')
|
logger.exception('WebAuthn registration failed')
|
||||||
return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={
|
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
|
||||||
'device': self.device.pk
|
'device': self.device.pk
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -527,7 +537,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
|
|||||||
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
||||||
'id': self.device.pk,
|
'id': self.device.pk,
|
||||||
'name': self.device.name,
|
'name': self.device.name,
|
||||||
'devicetype': 'otp_totp.totpdevice'
|
'devicetype': 'totp'
|
||||||
})
|
})
|
||||||
notices = [
|
notices = [
|
||||||
_('A new two-factor authentication device has been added to your account.')
|
_('A new two-factor authentication device has been added to your account.')
|
||||||
@@ -553,7 +563,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
|
|||||||
else:
|
else:
|
||||||
messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
|
messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
|
||||||
'that the date and time of your phone are configured correctly.'))
|
'that the date and time of your phone are configured correctly.'))
|
||||||
return redirect(reverse('control:user.settings.2fa.confirm.otp_totp.totpdevice', kwargs={
|
return redirect(reverse('control:user.settings.2fa.confirm.totp', kwargs={
|
||||||
'device': self.device.pk
|
'device': self.device.pk
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -584,7 +594,7 @@ class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
|
|||||||
template_name = 'pretixcontrol/user/2fa_enable.html'
|
template_name = 'pretixcontrol/user/2fa_enable.html'
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)):
|
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
|
||||||
messages.error(request, _('Please configure at least one device before enabling two-factor '
|
messages.error(request, _('Please configure at least one device before enabling two-factor '
|
||||||
'authentication.'))
|
'authentication.'))
|
||||||
return redirect(reverse('control:user.settings.2fa'))
|
return redirect(reverse('control:user.settings.2fa'))
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import ListView, TemplateView
|
from django.views.generic import ListView, TemplateView
|
||||||
from django_otp import devices_for_user
|
|
||||||
from django_otp.plugins.otp_static.models import StaticDevice
|
from django_otp.plugins.otp_static.models import StaticDevice
|
||||||
from hijack import signals
|
from hijack import signals
|
||||||
|
|
||||||
@@ -108,9 +107,6 @@ class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationReq
|
|||||||
ctx['backend'] = (
|
ctx['backend'] = (
|
||||||
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
|
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx['devices'] = devices_for_user(self.object)
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@@ -187,25 +183,6 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
|
|||||||
return reverse('control:users.edit', kwargs=self.kwargs)
|
return reverse('control:users.edit', kwargs=self.kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Reset2FADriftThrottleView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
return redirect(reverse('control:users.edit', kwargs=self.kwargs))
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
|
|
||||||
self.object.totpdevice_set.update(drift=0, throttling_failure_timestamp=None, throttling_failure_count=0)
|
|
||||||
self.object.staticdevice_set.update(throttling_failure_timestamp=None, throttling_failure_count=0)
|
|
||||||
self.object.log_action('pretix.user.settings.2fa.resetdrift', user=self.request.user)
|
|
||||||
messages.success(request, _(
|
|
||||||
'The drift values for TOTP devices have been reset.'
|
|
||||||
))
|
|
||||||
return redirect(self.get_success_url())
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('control:users.edit', kwargs=self.kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
|
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
|
||||||
template_name = "pretixcontrol/users/anonymize.html"
|
template_name = "pretixcontrol/users/anonymize.html"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,3 +19,15 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
|
from paypalrestsdk.api import Api as VendorApi
|
||||||
|
|
||||||
|
|
||||||
|
class Api(VendorApi):
|
||||||
|
def get_token_hash(self, authorization_code=None, refresh_token=None, headers=None):
|
||||||
|
if not authorization_code and not refresh_token:
|
||||||
|
checksum = hashlib.sha256(self.basic_auth().encode()).hexdigest()
|
||||||
|
cache_key_hash = f'pretix_paypal_token_hash_{checksum}'
|
||||||
|
cache_key_request_at = f'pretix_paypal_token_request_at_{checksum}'
|
||||||
|
token_hash = cache.get(cache_key_hash)
|
||||||
|
if token_hash:
|
||||||
|
token_request_at = cache.get(cache_key_request_at)
|
||||||
|
if token_request_at:
|
||||||
|
self.token_hash = json.loads(token_hash)
|
||||||
|
self.token_request_at = token_request_at
|
||||||
|
self.validate_token_hash()
|
||||||
|
if self.token_hash is not None:
|
||||||
|
return self.token_hash
|
||||||
|
|
||||||
|
t = super().get_token_hash(authorization_code, refresh_token, headers)
|
||||||
|
|
||||||
|
if self.token_hash:
|
||||||
|
cache.set(cache_key_hash, json.dumps(self.token_hash), 3600 * 4)
|
||||||
|
cache.set(cache_key_request_at, self.token_request_at, 3600 * 4)
|
||||||
|
|
||||||
|
return t
|
||||||
|
|
||||||
|
return super().get_token_hash(authorization_code, refresh_token, headers)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix import __version__ as version
|
||||||
|
|
||||||
|
|
||||||
|
class PaypalApp(AppConfig):
|
||||||
|
name = 'pretix.plugins.paypal'
|
||||||
|
verbose_name = _("PayPal")
|
||||||
|
|
||||||
|
class PretixPluginMeta:
|
||||||
|
name = _("PayPal")
|
||||||
|
author = _("the pretix team")
|
||||||
|
version = version
|
||||||
|
category = 'PAYMENT'
|
||||||
|
featured = True
|
||||||
|
picture = 'pretixplugins/paypal/paypal_logo.svg'
|
||||||
|
description = _("Accept payments with your PayPal account. PayPal is one of the most popular payment methods "
|
||||||
|
"world-wide.")
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals # NOQA
|
||||||
|
|
||||||
|
def is_available(self, event):
|
||||||
|
return 'pretix.plugins.paypal' in event.plugins.split(',')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def compatibility_errors(self):
|
||||||
|
errs = []
|
||||||
|
try:
|
||||||
|
import paypalrestsdk # NOQA
|
||||||
|
except ImportError:
|
||||||
|
errs.append("Python package 'paypalrestsdk' is not installed.")
|
||||||
|
return errs
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 5.2.17 on 2026-09-08 08:05
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("paypal", "0004_bigint"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.SeparateDatabaseAndState(
|
|
||||||
state_operations=[
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name="ReferencedPayPalObject",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
database_operations=[]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -26,6 +26,3 @@ class ReferencedPayPalObject(models.Model):
|
|||||||
reference = models.CharField(max_length=190, db_index=True, unique=True)
|
reference = models.CharField(max_length=190, db_index=True, unique=True)
|
||||||
order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE)
|
order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE)
|
||||||
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE)
|
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
db_table = 'paypal_referencedpaypalobject'
|
|
||||||
@@ -0,0 +1,715 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, Tobias Kunze
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import paypalrestsdk
|
||||||
|
import paypalrestsdk.exceptions
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext as __, gettext_lazy as _
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
from paypalrestsdk.exceptions import BadRequest, UnauthorizedAccess
|
||||||
|
from paypalrestsdk.openid_connect import Tokeninfo
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
from pretix.base.decimal import round_decimal
|
||||||
|
from pretix.base.forms import SecretKeySettingsField
|
||||||
|
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||||
|
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||||
|
from pretix.base.settings import SettingsSandbox
|
||||||
|
from pretix.base.views.redirect import safelink
|
||||||
|
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||||
|
from pretix.plugins.paypal.api import Api
|
||||||
|
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||||
|
|
||||||
|
logger = logging.getLogger('pretix.plugins.paypal')
|
||||||
|
|
||||||
|
SUPPORTED_CURRENCIES = ['AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD', 'HUF', 'INR', 'ILS', 'JPY', 'MYR', 'MXN',
|
||||||
|
'TWD', 'NZD', 'NOK', 'PHP', 'PLN', 'GBP', 'RUB', 'SGD', 'SEK', 'CHF', 'THB', 'USD']
|
||||||
|
|
||||||
|
LOCAL_ONLY_CURRENCIES = ['INR']
|
||||||
|
|
||||||
|
|
||||||
|
class Paypal(BasePaymentProvider):
|
||||||
|
identifier = 'paypal'
|
||||||
|
verbose_name = _('PayPal')
|
||||||
|
payment_form_fields = OrderedDict([
|
||||||
|
])
|
||||||
|
|
||||||
|
def __init__(self, event: Event):
|
||||||
|
super().__init__(event)
|
||||||
|
self.settings = SettingsSandbox('payment', 'paypal', event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def test_mode_message(self):
|
||||||
|
if self.settings.connect_client_id and not self.settings.secret:
|
||||||
|
# in OAuth mode, sandbox mode needs to be set global
|
||||||
|
is_sandbox = self.settings.connect_endpoint == 'sandbox'
|
||||||
|
else:
|
||||||
|
is_sandbox = self.settings.get('endpoint') == 'sandbox'
|
||||||
|
if is_sandbox:
|
||||||
|
return _('The PayPal sandbox is being used, you can test without actually sending money but you will need a '
|
||||||
|
'PayPal sandbox user to log in.')
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def settings_form_fields(self):
|
||||||
|
if self.settings.connect_client_id and not self.settings.secret:
|
||||||
|
# PayPal connect
|
||||||
|
if self.settings.connect_user_id:
|
||||||
|
fields = [
|
||||||
|
('connect_user_id',
|
||||||
|
forms.CharField(
|
||||||
|
label=_('PayPal account'),
|
||||||
|
disabled=True
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
fields = [
|
||||||
|
('client_id',
|
||||||
|
forms.CharField(
|
||||||
|
label=_('Client ID'),
|
||||||
|
min_length=80,
|
||||||
|
help_text=format_html(
|
||||||
|
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
|
||||||
|
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||||
|
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html',
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
('secret',
|
||||||
|
SecretKeySettingsField(
|
||||||
|
label=_('Secret'),
|
||||||
|
max_length=80,
|
||||||
|
min_length=80,
|
||||||
|
)),
|
||||||
|
('endpoint',
|
||||||
|
forms.ChoiceField(
|
||||||
|
label=_('Endpoint'),
|
||||||
|
initial='live',
|
||||||
|
choices=(
|
||||||
|
('live', 'Live'),
|
||||||
|
('sandbox', 'Sandbox'),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_fields = [
|
||||||
|
('prefix',
|
||||||
|
forms.CharField(
|
||||||
|
label=_('Reference prefix'),
|
||||||
|
help_text=_('Any value entered here will be added in front of the regular booking reference '
|
||||||
|
'containing the order number.'),
|
||||||
|
required=False,
|
||||||
|
)),
|
||||||
|
('postfix',
|
||||||
|
forms.CharField(
|
||||||
|
label=_('Reference postfix'),
|
||||||
|
help_text=_('Any value entered here will be added behind the regular booking reference '
|
||||||
|
'containing the order number.'),
|
||||||
|
required=False,
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
|
||||||
|
d = OrderedDict(
|
||||||
|
fields + extra_fields + list(super().settings_form_fields.items())
|
||||||
|
)
|
||||||
|
|
||||||
|
d.move_to_end('prefix')
|
||||||
|
d.move_to_end('postfix')
|
||||||
|
d.move_to_end('_enabled', False)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def get_connect_url(self, request):
|
||||||
|
request.session['payment_paypal_oauth_event'] = request.event.pk
|
||||||
|
|
||||||
|
self.init_api()
|
||||||
|
return Tokeninfo.authorize_url({'scope': 'openid profile email'})
|
||||||
|
|
||||||
|
def settings_content_render(self, request):
|
||||||
|
settings_content = ""
|
||||||
|
if self.settings.connect_client_id and not self.settings.secret:
|
||||||
|
# Use PayPal connect
|
||||||
|
if not self.settings.connect_user_id:
|
||||||
|
# Migrate User to PayPal v2
|
||||||
|
self.event.disable_plugin("pretix.plugins.paypal")
|
||||||
|
self.event.enable_plugin("pretix.plugins.paypal2")
|
||||||
|
self.event.save()
|
||||||
|
else:
|
||||||
|
settings_content = (
|
||||||
|
"<button formaction='{}' class='btn btn-danger'>{}</button>"
|
||||||
|
).format(
|
||||||
|
reverse('plugins:paypal:oauth.disconnect', kwargs={
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'event': self.event.slug,
|
||||||
|
}),
|
||||||
|
_('Disconnect from PayPal')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Migrate User to PayPal v2
|
||||||
|
self.event.disable_plugin("pretix.plugins.paypal")
|
||||||
|
self.event.enable_plugin("pretix.plugins.paypal2")
|
||||||
|
self.event.save()
|
||||||
|
|
||||||
|
return settings_content
|
||||||
|
|
||||||
|
def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool:
|
||||||
|
return super().is_allowed(request, total) and self.event.currency in SUPPORTED_CURRENCIES
|
||||||
|
|
||||||
|
def init_api(self):
|
||||||
|
if self.settings.connect_client_id and not self.settings.secret:
|
||||||
|
paypalrestsdk.api.__api__ = Api(
|
||||||
|
mode="sandbox" if "sandbox" in self.settings.connect_endpoint else 'live',
|
||||||
|
client_id=self.settings.connect_client_id,
|
||||||
|
client_secret=self.settings.connect_secret_key,
|
||||||
|
openid_client_id=self.settings.connect_client_id,
|
||||||
|
openid_client_secret=self.settings.connect_secret_key
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
paypalrestsdk.api.__api__ = Api(
|
||||||
|
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
|
||||||
|
client_id=self.settings.get('client_id'),
|
||||||
|
client_secret=self.settings.get('secret')
|
||||||
|
)
|
||||||
|
|
||||||
|
def payment_is_valid_session(self, request):
|
||||||
|
return (request.session.get('payment_paypal_id', '') != ''
|
||||||
|
and request.session.get('payment_paypal_payer', '') != '')
|
||||||
|
|
||||||
|
def payment_form_render(self, request) -> str:
|
||||||
|
template = get_template('pretixplugins/paypal/checkout_payment_form.html')
|
||||||
|
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
|
||||||
|
return template.render(ctx)
|
||||||
|
|
||||||
|
def checkout_prepare(self, request, cart):
|
||||||
|
self.init_api()
|
||||||
|
kwargs = {}
|
||||||
|
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
|
||||||
|
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.settings.connect_client_id and not self.settings.secret:
|
||||||
|
if not request.event.settings.payment_paypal_connect_user_id:
|
||||||
|
raise PaymentException('Payment method misconfigured')
|
||||||
|
|
||||||
|
try:
|
||||||
|
tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
|
||||||
|
except BadRequest as ex:
|
||||||
|
ex = json.loads(ex.content)
|
||||||
|
messages.error(request, '{}: {} ({})'.format(
|
||||||
|
_('We had trouble communicating with PayPal'),
|
||||||
|
ex['error_description'],
|
||||||
|
ex['correlation_id'])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
|
||||||
|
# get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
|
||||||
|
try:
|
||||||
|
userinfo = tokeninfo.userinfo()
|
||||||
|
request.event.settings.payment_paypal_connect_user_id = userinfo.email
|
||||||
|
except UnauthorizedAccess:
|
||||||
|
pass
|
||||||
|
|
||||||
|
payee = {
|
||||||
|
"email": request.event.settings.payment_paypal_connect_user_id,
|
||||||
|
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
|
||||||
|
# we should use it instead of the merchant's eMail-address
|
||||||
|
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
payee = {}
|
||||||
|
|
||||||
|
payment = paypalrestsdk.Payment({
|
||||||
|
'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
|
||||||
|
'intent': 'sale',
|
||||||
|
'payer': {
|
||||||
|
"payment_method": "paypal",
|
||||||
|
},
|
||||||
|
"redirect_urls": {
|
||||||
|
"return_url": eventreverse_absolute(request.event, 'plugins:paypal:return', kwargs=kwargs),
|
||||||
|
"cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort', kwargs=kwargs),
|
||||||
|
},
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"item_list": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": '{prefix}{orderstring}{postfix}'.format(
|
||||||
|
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||||
|
orderstring=__('Order for %s') % str(request.event),
|
||||||
|
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||||
|
),
|
||||||
|
"quantity": 1,
|
||||||
|
"price": self.format_price(cart['total']),
|
||||||
|
"currency": request.event.currency
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"currency": request.event.currency,
|
||||||
|
"total": self.format_price(cart['total'])
|
||||||
|
},
|
||||||
|
"description": __('Event tickets for {event}').format(event=request.event.name),
|
||||||
|
"payee": payee,
|
||||||
|
"custom": '{prefix}{slug}{postfix}'.format(
|
||||||
|
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||||
|
slug=request.event.slug.upper(),
|
||||||
|
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
request.session['payment_paypal_payment'] = None
|
||||||
|
return self._create_payment(request, payment)
|
||||||
|
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||||
|
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||||
|
logger.exception('Error on creating payment: ' + str(e))
|
||||||
|
|
||||||
|
def format_price(self, value):
|
||||||
|
return str(round_decimal(value, self.event.currency, {
|
||||||
|
# PayPal behaves differently than Stripe in deciding what currencies have decimal places
|
||||||
|
# Source https://developer.paypal.com/docs/classic/api/currency_codes/
|
||||||
|
'HUF': 0,
|
||||||
|
'JPY': 0,
|
||||||
|
'MYR': 0,
|
||||||
|
'TWD': 0,
|
||||||
|
# However, CLPs are not listed there while PayPal requires us not to send decimal places there. WTF.
|
||||||
|
'CLP': 0,
|
||||||
|
# Let's just guess that the ones listed here are 0-based as well
|
||||||
|
# https://developers.braintreepayments.com/reference/general/currencies
|
||||||
|
'BIF': 0,
|
||||||
|
'DJF': 0,
|
||||||
|
'GNF': 0,
|
||||||
|
'KMF': 0,
|
||||||
|
'KRW': 0,
|
||||||
|
'LAK': 0,
|
||||||
|
'PYG': 0,
|
||||||
|
'RWF': 0,
|
||||||
|
'UGX': 0,
|
||||||
|
'VND': 0,
|
||||||
|
'VUV': 0,
|
||||||
|
'XAF': 0,
|
||||||
|
'XOF': 0,
|
||||||
|
'XPF': 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abort_pending_allowed(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_payment(self, request, payment):
|
||||||
|
if payment.create():
|
||||||
|
if payment.state not in ('created', 'approved', 'pending'):
|
||||||
|
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||||
|
logger.error('Invalid payment state: ' + str(payment))
|
||||||
|
return
|
||||||
|
request.session['payment_paypal_id'] = payment.id
|
||||||
|
for link in payment.links:
|
||||||
|
if link.method == "REDIRECT" and link.rel == "approval_url":
|
||||||
|
if request.session.get('iframe_session', False):
|
||||||
|
return safelink(link.href, framebreak=True)
|
||||||
|
else:
|
||||||
|
return str(link.href)
|
||||||
|
else:
|
||||||
|
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||||
|
logger.error('Error on creating payment: ' + str(payment.error))
|
||||||
|
|
||||||
|
def checkout_confirm_render(self, request) -> str:
|
||||||
|
"""
|
||||||
|
Returns the HTML that should be displayed when the user selected this provider
|
||||||
|
on the 'confirm order' page.
|
||||||
|
"""
|
||||||
|
template = get_template('pretixplugins/paypal/checkout_payment_confirm.html')
|
||||||
|
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
|
||||||
|
return template.render(ctx)
|
||||||
|
|
||||||
|
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||||
|
if (request.session.get('payment_paypal_id', '') == '' or request.session.get('payment_paypal_payer', '') == ''):
|
||||||
|
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||||
|
'proceed.'))
|
||||||
|
|
||||||
|
self.init_api()
|
||||||
|
pp_payment = paypalrestsdk.Payment.find(request.session.get('payment_paypal_id'))
|
||||||
|
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_payment.id)
|
||||||
|
if str(pp_payment.transactions[0].amount.total) != str(payment.amount) or pp_payment.transactions[0].amount.currency \
|
||||||
|
!= self.event.currency:
|
||||||
|
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_payment)))
|
||||||
|
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||||
|
'proceed.'))
|
||||||
|
|
||||||
|
return self._execute_payment(pp_payment, request, payment)
|
||||||
|
|
||||||
|
def _execute_payment(self, payment, request, payment_obj):
|
||||||
|
if payment.state == 'created':
|
||||||
|
payment.replace([
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/transactions/0/item_list",
|
||||||
|
"value": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": '{prefix}{orderstring}{postfix}'.format(
|
||||||
|
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||||
|
orderstring=__('Order {slug}-{code}').format(
|
||||||
|
slug=self.event.slug.upper(),
|
||||||
|
code=payment_obj.order.code
|
||||||
|
),
|
||||||
|
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||||
|
),
|
||||||
|
"quantity": 1,
|
||||||
|
"price": self.format_price(payment_obj.amount),
|
||||||
|
"currency": payment_obj.order.event.currency
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/transactions/0/description",
|
||||||
|
"value": '{prefix}{orderstring}{postfix}'.format(
|
||||||
|
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||||
|
orderstring=__('Order {order} for {event}').format(
|
||||||
|
event=request.event.name,
|
||||||
|
order=payment_obj.order.code
|
||||||
|
),
|
||||||
|
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||||
|
),
|
||||||
|
}
|
||||||
|
])
|
||||||
|
try:
|
||||||
|
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
|
||||||
|
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||||
|
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||||
|
logger.exception('Error on creating payment: ' + str(e))
|
||||||
|
except RequestException as e:
|
||||||
|
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||||
|
logger.exception('Error on creating payment: ' + str(e))
|
||||||
|
|
||||||
|
for trans in payment.transactions:
|
||||||
|
for rr in trans.related_resources:
|
||||||
|
if hasattr(rr, 'sale') and rr.sale:
|
||||||
|
if rr.sale.state == 'pending':
|
||||||
|
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
|
||||||
|
'soon as the payment completed.'))
|
||||||
|
payment_obj.info = json.dumps(payment.to_dict())
|
||||||
|
payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||||
|
payment_obj.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
payment_obj.refresh_from_db()
|
||||||
|
if payment.state == 'pending':
|
||||||
|
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
|
||||||
|
'payment completed.'))
|
||||||
|
payment_obj.info = json.dumps(payment.to_dict())
|
||||||
|
payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||||
|
payment_obj.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
if payment.state != 'approved':
|
||||||
|
payment_obj.fail(info=payment.to_dict())
|
||||||
|
logger.error('Invalid state: %s' % str(payment))
|
||||||
|
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||||
|
'proceed.'))
|
||||||
|
|
||||||
|
if payment_obj.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||||
|
logger.warning('PayPal success event even though order is already marked as paid')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
payment_obj.info = json.dumps(payment.to_dict())
|
||||||
|
payment_obj.save(update_fields=['info'])
|
||||||
|
payment_obj.confirm()
|
||||||
|
except Quota.QuotaExceededException as e:
|
||||||
|
raise PaymentException(str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def payment_pending_render(self, request, payment) -> str:
|
||||||
|
retry = True
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
payment.info
|
||||||
|
and payment.info_data['transactions'][0]['related_resources'][0]['sale']['state'] == 'pending'
|
||||||
|
):
|
||||||
|
retry = False
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
pass
|
||||||
|
template = get_template('pretixplugins/paypal/pending.html')
|
||||||
|
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
|
||||||
|
'retry': retry, 'order': payment.order}
|
||||||
|
return template.render(ctx)
|
||||||
|
|
||||||
|
def matching_id(self, payment: OrderPayment):
|
||||||
|
sale_id = None
|
||||||
|
for trans in payment.info_data.get('transactions', []):
|
||||||
|
for res in trans.get('related_resources', []):
|
||||||
|
if 'sale' in res and 'id' in res['sale']:
|
||||||
|
sale_id = res['sale']['id']
|
||||||
|
return sale_id or payment.info_data.get('id', None)
|
||||||
|
|
||||||
|
def api_payment_details(self, payment: OrderPayment):
|
||||||
|
sale_id = None
|
||||||
|
for trans in payment.info_data.get('transactions', []):
|
||||||
|
for res in trans.get('related_resources', []):
|
||||||
|
if 'sale' in res and 'id' in res['sale']:
|
||||||
|
sale_id = res['sale']['id']
|
||||||
|
return {
|
||||||
|
"payer_email": payment.info_data.get('payer', {}).get('payer_info', {}).get('email'),
|
||||||
|
"payer_id": payment.info_data.get('payer', {}).get('payer_info', {}).get('payer_id'),
|
||||||
|
"cart_id": payment.info_data.get('cart', None),
|
||||||
|
"payment_id": payment.info_data.get('id', None),
|
||||||
|
"sale_id": sale_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
|
||||||
|
template = get_template('pretixplugins/paypal/control.html')
|
||||||
|
sale_id = None
|
||||||
|
for trans in payment.info_data.get('transactions', []):
|
||||||
|
for res in trans.get('related_resources', []):
|
||||||
|
if 'sale' in res and 'id' in res['sale']:
|
||||||
|
sale_id = res['sale']['id']
|
||||||
|
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
|
||||||
|
'payment_info': payment.info_data, 'order': payment.order, 'sale_id': sale_id}
|
||||||
|
return template.render(ctx)
|
||||||
|
|
||||||
|
def payment_control_render_short(self, payment: OrderPayment) -> str:
|
||||||
|
return payment.info_data.get('payer', {}).get('payer_info', {}).get('email', '')
|
||||||
|
|
||||||
|
def payment_partial_refund_supported(self, payment: OrderPayment):
|
||||||
|
# Paypal refunds are possible for 180 days after purchase:
|
||||||
|
# https://www.paypal.com/lc/smarthelp/article/how-do-i-issue-a-refund-faq780#:~:text=Refund%20after%20180%20days%20of,PayPal%20balance%20of%20the%20recipient.
|
||||||
|
return (now() - payment.payment_date).days <= 180
|
||||||
|
|
||||||
|
def payment_refund_supported(self, payment: OrderPayment):
|
||||||
|
self.payment_partial_refund_supported(payment)
|
||||||
|
|
||||||
|
def execute_refund(self, refund: OrderRefund):
|
||||||
|
self.init_api()
|
||||||
|
|
||||||
|
try:
|
||||||
|
sale = None
|
||||||
|
for res in refund.payment.info_data['transactions'][0]['related_resources']:
|
||||||
|
for k, v in res.items():
|
||||||
|
if k == 'sale':
|
||||||
|
sale = paypalrestsdk.Sale.find(v['id'])
|
||||||
|
break
|
||||||
|
|
||||||
|
if not sale:
|
||||||
|
pp_payment = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
|
||||||
|
for res in pp_payment.transactions[0].related_resources:
|
||||||
|
for k, v in res.to_dict().items():
|
||||||
|
if k == 'sale':
|
||||||
|
sale = paypalrestsdk.Sale.find(v['id'])
|
||||||
|
break
|
||||||
|
|
||||||
|
pp_refund = sale.refund({
|
||||||
|
"amount": {
|
||||||
|
"total": self.format_price(refund.amount),
|
||||||
|
"currency": refund.order.event.currency
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||||
|
refund.order.log_action('pretix.event.order.refund.failed', {
|
||||||
|
'local_id': refund.local_id,
|
||||||
|
'provider': refund.provider,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(str(e)))
|
||||||
|
if not pp_refund.success():
|
||||||
|
refund.order.log_action('pretix.event.order.refund.failed', {
|
||||||
|
'local_id': refund.local_id,
|
||||||
|
'provider': refund.provider,
|
||||||
|
'error': str(pp_refund.error)
|
||||||
|
})
|
||||||
|
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(pp_refund.error))
|
||||||
|
else:
|
||||||
|
sale = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
|
||||||
|
refund.payment.info = json.dumps(sale.to_dict())
|
||||||
|
refund.info = json.dumps(pp_refund.to_dict())
|
||||||
|
refund.done()
|
||||||
|
|
||||||
|
def payment_prepare(self, request, payment_obj):
|
||||||
|
self.init_api()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if request.event.settings.payment_paypal_connect_user_id:
|
||||||
|
try:
|
||||||
|
tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
|
||||||
|
except BadRequest as ex:
|
||||||
|
ex = json.loads(ex.content)
|
||||||
|
messages.error(request, '{}: {} ({})'.format(
|
||||||
|
_('We had trouble communicating with PayPal'),
|
||||||
|
ex['error_description'],
|
||||||
|
ex['correlation_id'])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
|
||||||
|
# get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
|
||||||
|
try:
|
||||||
|
userinfo = tokeninfo.userinfo()
|
||||||
|
request.event.settings.payment_paypal_connect_user_id = userinfo.email
|
||||||
|
except UnauthorizedAccess:
|
||||||
|
pass
|
||||||
|
|
||||||
|
payee = {
|
||||||
|
"email": request.event.settings.payment_paypal_connect_user_id,
|
||||||
|
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
|
||||||
|
# we should use it instead of the merchant's eMail-address
|
||||||
|
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
payee = {}
|
||||||
|
|
||||||
|
payment = paypalrestsdk.Payment({
|
||||||
|
'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
|
||||||
|
'intent': 'sale',
|
||||||
|
'payer': {
|
||||||
|
"payment_method": "paypal",
|
||||||
|
},
|
||||||
|
"redirect_urls": {
|
||||||
|
"return_url": eventreverse_absolute(request.event, 'plugins:paypal:return'),
|
||||||
|
"cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort'),
|
||||||
|
},
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"item_list": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": '{prefix}{orderstring}{postfix}'.format(
|
||||||
|
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||||
|
orderstring=__('Order {slug}-{code}').format(
|
||||||
|
slug=self.event.slug.upper(),
|
||||||
|
code=payment_obj.order.code
|
||||||
|
),
|
||||||
|
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||||
|
),
|
||||||
|
"quantity": 1,
|
||||||
|
"price": self.format_price(payment_obj.amount),
|
||||||
|
"currency": payment_obj.order.event.currency
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"currency": request.event.currency,
|
||||||
|
"total": self.format_price(payment_obj.amount)
|
||||||
|
},
|
||||||
|
"description": '{prefix}{orderstring}{postfix}'.format(
|
||||||
|
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||||
|
orderstring=__('Order {order} for {event}').format(
|
||||||
|
event=request.event.name,
|
||||||
|
order=payment_obj.order.code
|
||||||
|
),
|
||||||
|
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||||
|
),
|
||||||
|
"payee": payee,
|
||||||
|
"custom": '{prefix}{slug}-{code}{postfix}'.format(
|
||||||
|
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||||
|
slug=self.event.slug.upper(),
|
||||||
|
code=payment_obj.order.code,
|
||||||
|
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
request.session['payment_paypal_payment'] = payment_obj.pk
|
||||||
|
return self._create_payment(request, payment)
|
||||||
|
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||||
|
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||||
|
logger.exception('Error on creating payment: ' + str(e))
|
||||||
|
|
||||||
|
def shred_payment_info(self, obj):
|
||||||
|
if obj.info:
|
||||||
|
d = json.loads(obj.info)
|
||||||
|
new = {
|
||||||
|
'id': d.get('id'),
|
||||||
|
'payer': {
|
||||||
|
'payer_info': {
|
||||||
|
'email': '█'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'update_time': d.get('update_time'),
|
||||||
|
'transactions': [
|
||||||
|
{
|
||||||
|
'amount': t.get('amount')
|
||||||
|
} for t in d.get('transactions', [])
|
||||||
|
],
|
||||||
|
'_shredded': True
|
||||||
|
}
|
||||||
|
obj.info = json.dumps(new)
|
||||||
|
obj.save(update_fields=['info'])
|
||||||
|
|
||||||
|
for le in obj.order.all_logentries().filter(action_type="pretix.plugins.paypal.event").exclude(data=""):
|
||||||
|
d = le.parsed_data
|
||||||
|
if 'resource' in d:
|
||||||
|
d['resource'] = {
|
||||||
|
'id': d['resource'].get('id'),
|
||||||
|
'sale_id': d['resource'].get('sale_id'),
|
||||||
|
'parent_payment': d['resource'].get('parent_payment'),
|
||||||
|
}
|
||||||
|
le.data = json.dumps(d)
|
||||||
|
le.shredded = True
|
||||||
|
le.save(update_fields=['data', 'shredded'])
|
||||||
|
|
||||||
|
def render_invoice_text(self, order: Order, payment: OrderPayment) -> str:
|
||||||
|
if order.status == Order.STATUS_PAID:
|
||||||
|
if payment.info_data.get('id', None):
|
||||||
|
try:
|
||||||
|
return '{}\r\n{}: {}\r\n{}: {}'.format(
|
||||||
|
_('The payment for this invoice has already been received.'),
|
||||||
|
_('PayPal payment ID'),
|
||||||
|
payment.info_data['id'],
|
||||||
|
_('PayPal sale ID'),
|
||||||
|
payment.info_data['transactions'][0]['related_resources'][0]['sale']['id']
|
||||||
|
)
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
return '{}\r\n{}: {}'.format(
|
||||||
|
_('The payment for this invoice has already been received.'),
|
||||||
|
_('PayPal payment ID'),
|
||||||
|
payment.info_data['id']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return super().render_invoice_text(order, payment)
|
||||||
|
|
||||||
|
return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from pretix.base.signals import register_payment_providers
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_payment_providers, dispatch_uid="payment_paypal")
|
||||||
|
def register_payment_provider(sender, **kwargs):
|
||||||
|
from .payment import Paypal
|
||||||
|
return Paypal
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="124px" height="33px" viewBox="0 0 124 33" enable-background="new 0 0 124 33" xml:space="preserve">
|
||||||
|
<path fill="#253B80" d="M46.211,6.749h-6.839c-0.468,0-0.866,0.34-0.939,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.564,0.658 h3.265c0.468,0,0.866-0.34,0.939-0.803l0.746-4.73c0.072-0.463,0.471-0.803,0.938-0.803h2.165c4.505,0,7.105-2.18,7.784-6.5 c0.306-1.89,0.013-3.375-0.872-4.415C50.224,7.353,48.5,6.749,46.211,6.749z M47,13.154c-0.374,2.454-2.249,2.454-4.062,2.454 h-1.032l0.724-4.583c0.043-0.277,0.283-0.481,0.563-0.481h0.473c1.235,0,2.4,0,3.002,0.704C47.027,11.668,47.137,12.292,47,13.154z"/>
|
||||||
|
<path fill="#253B80" d="M66.654,13.075h-3.275c-0.279,0-0.52,0.204-0.563,0.481l-0.145,0.916l-0.229-0.332 c-0.709-1.029-2.29-1.373-3.868-1.373c-3.619,0-6.71,2.741-7.312,6.586c-0.313,1.918,0.132,3.752,1.22,5.031 c0.998,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.562,0.66h2.95 c0.469,0,0.865-0.34,0.939-0.803l1.77-11.209C67.271,13.388,67.004,13.075,66.654,13.075z M62.089,19.449 c-0.316,1.871-1.801,3.127-3.695,3.127c-0.951,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.668-1.391-0.514-2.301 c0.295-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C62.034,17.721,62.232,18.543,62.089,19.449z"/>
|
||||||
|
<path fill="#253B80" d="M84.096,13.075h-3.291c-0.314,0-0.609,0.156-0.787,0.417l-4.539,6.686l-1.924-6.425 c-0.121-0.402-0.492-0.678-0.912-0.678h-3.234c-0.393,0-0.666,0.384-0.541,0.754l3.625,10.638l-3.408,4.811 c-0.268,0.379,0.002,0.9,0.465,0.9h3.287c0.312,0,0.604-0.152,0.781-0.408L84.564,13.97C84.826,13.592,84.557,13.075,84.096,13.075z "/>
|
||||||
|
<path fill="#179BD7" d="M94.992,6.749h-6.84c-0.467,0-0.865,0.34-0.938,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.562,0.658 h3.51c0.326,0,0.605-0.238,0.656-0.562l0.785-4.971c0.072-0.463,0.471-0.803,0.938-0.803h2.164c4.506,0,7.105-2.18,7.785-6.5 c0.307-1.89,0.012-3.375-0.873-4.415C99.004,7.353,97.281,6.749,94.992,6.749z M95.781,13.154c-0.373,2.454-2.248,2.454-4.062,2.454 h-1.031l0.725-4.583c0.043-0.277,0.281-0.481,0.562-0.481h0.473c1.234,0,2.4,0,3.002,0.704 C95.809,11.668,95.918,12.292,95.781,13.154z"/>
|
||||||
|
<path fill="#179BD7" d="M115.434,13.075h-3.273c-0.281,0-0.52,0.204-0.562,0.481l-0.145,0.916l-0.23-0.332 c-0.709-1.029-2.289-1.373-3.867-1.373c-3.619,0-6.709,2.741-7.311,6.586c-0.312,1.918,0.131,3.752,1.219,5.031 c1,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.564,0.66h2.949 c0.467,0,0.865-0.34,0.938-0.803l1.771-11.209C116.053,13.388,115.785,13.075,115.434,13.075z M110.869,19.449 c-0.314,1.871-1.801,3.127-3.695,3.127c-0.949,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.666-1.391-0.514-2.301 c0.297-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C110.816,17.721,111.014,18.543,110.869,19.449z"/>
|
||||||
|
<path fill="#179BD7" d="M119.295,7.23l-2.807,17.858c-0.055,0.346,0.213,0.658,0.562,0.658h2.822c0.469,0,0.867-0.34,0.939-0.803 l2.768-17.536c0.055-0.346-0.213-0.659-0.562-0.659h-3.16C119.578,6.749,119.338,6.953,119.295,7.23z"/>
|
||||||
|
<path fill="#253B80" d="M7.266,29.154l0.523-3.322l-1.165-0.027H1.061L4.927,1.292C4.939,1.218,4.978,1.149,5.035,1.1 c0.057-0.049,0.13-0.076,0.206-0.076h9.38c3.114,0,5.263,0.648,6.385,1.927c0.526,0.6,0.861,1.227,1.023,1.917 c0.17,0.724,0.173,1.589,0.007,2.644l-0.012,0.077v0.676l0.526,0.298c0.443,0.235,0.795,0.504,1.065,0.812 c0.45,0.513,0.741,1.165,0.864,1.938c0.127,0.795,0.085,1.741-0.123,2.812c-0.24,1.232-0.628,2.305-1.152,3.183 c-0.482,0.809-1.096,1.48-1.825,2c-0.696,0.494-1.523,0.869-2.458,1.109c-0.906,0.236-1.939,0.355-3.072,0.355h-0.73 c-0.522,0-1.029,0.188-1.427,0.525c-0.399,0.344-0.663,0.814-0.744,1.328l-0.055,0.299l-0.924,5.855l-0.042,0.215 c-0.011,0.068-0.03,0.102-0.058,0.125c-0.025,0.021-0.061,0.035-0.096,0.035H7.266z"/>
|
||||||
|
<path fill="#179BD7" d="M23.048,7.667L23.048,7.667L23.048,7.667c-0.028,0.179-0.06,0.362-0.096,0.55 c-1.237,6.351-5.469,8.545-10.874,8.545H9.326c-0.661,0-1.218,0.48-1.321,1.132l0,0l0,0L6.596,26.83l-0.399,2.533 c-0.067,0.428,0.263,0.814,0.695,0.814h4.881c0.578,0,1.069-0.42,1.16-0.99l0.048-0.248l0.919-5.832l0.059-0.32 c0.09-0.572,0.582-0.992,1.16-0.992h0.73c4.729,0,8.431-1.92,9.513-7.476c0.452-2.321,0.218-4.259-0.978-5.622 C24.022,8.286,23.573,7.945,23.048,7.667z"/>
|
||||||
|
<path fill="#222D65" d="M21.754,7.151c-0.189-0.055-0.384-0.105-0.584-0.15c-0.201-0.044-0.407-0.083-0.619-0.117 c-0.742-0.12-1.555-0.177-2.426-0.177h-7.352c-0.181,0-0.353,0.041-0.507,0.115C9.927,6.985,9.675,7.306,9.614,7.699L8.05,17.605 l-0.045,0.289c0.103-0.652,0.66-1.132,1.321-1.132h2.752c5.405,0,9.637-2.195,10.874-8.545c0.037-0.188,0.068-0.371,0.096-0.55 c-0.313-0.166-0.652-0.308-1.017-0.429C21.941,7.208,21.848,7.179,21.754,7.151z"/>
|
||||||
|
<path fill="#253B80" d="M9.614,7.699c0.061-0.393,0.313-0.714,0.652-0.876c0.155-0.074,0.326-0.115,0.507-0.115h7.352 c0.871,0,1.684,0.057,2.426,0.177c0.212,0.034,0.418,0.073,0.619,0.117c0.2,0.045,0.395,0.095,0.584,0.15 c0.094,0.028,0.187,0.057,0.278,0.086c0.365,0.121,0.704,0.264,1.017,0.429c0.368-2.347-0.003-3.945-1.272-5.392 C20.378,0.682,17.853,0,14.622,0h-9.38c-0.66,0-1.223,0.48-1.325,1.133L0.01,25.898c-0.077,0.49,0.301,0.932,0.795,0.932h5.791 l1.454-9.225L9.614,7.699z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
The total amount listed above will be withdrawn from your PayPal account after the
|
||||||
|
confirmation of your purchase.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
After you clicked continue, we will redirect you to PayPal to fill in your payment
|
||||||
|
details. You will then be redirected back here to review and confirm your order.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if payment_info %}
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>{% trans "Payment ID" %}</dt>
|
||||||
|
<dd>{{ payment_info.id }}</dd>
|
||||||
|
<dt>{% trans "Sale ID" %}</dt>
|
||||||
|
<dd>{{ sale_id|default_if_none:"?" }}</dd>
|
||||||
|
<dt>{% trans "Payer" %}</dt>
|
||||||
|
<dd>{{ payment_info.payer.payer_info.email }}</dd>
|
||||||
|
<dt>{% trans "Last update" %}</dt>
|
||||||
|
<dd>{{ payment_info.update_time }}</dd>
|
||||||
|
<dt>{% trans "Total value" %}</dt>
|
||||||
|
<dd>{{ payment_info.transactions.0.amount.total }}</dd>
|
||||||
|
<dt>{% trans "Currency" %}</dt>
|
||||||
|
<dd>{{ payment_info.transactions.0.amount.currency }}</dd>
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if retry %}
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
Our attempt to execute your Payment via PayPal has failed. Please try again or contact us.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
We're waiting for an answer from PayPal regarding your payment. Please contact us, if this
|
||||||
|
takes more than a few hours.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django.urls import include, re_path
|
||||||
|
|
||||||
|
from .views import abort, oauth_disconnect, success
|
||||||
|
|
||||||
|
event_patterns = [
|
||||||
|
re_path(r'^paypal/', include([
|
||||||
|
re_path(r'^abort/$', abort, name='abort'),
|
||||||
|
re_path(r'^return/$', success, name='return'),
|
||||||
|
|
||||||
|
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
|
||||||
|
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
|
||||||
|
])),
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/disconnect/',
|
||||||
|
oauth_disconnect, name='oauth.disconnect'),
|
||||||
|
]
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Flavia Bastos
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import paypalrestsdk
|
||||||
|
import paypalrestsdk.exceptions
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.db.models import Sum
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from pretix.base.models import Order, OrderPayment, OrderRefund, Quota
|
||||||
|
from pretix.base.payment import PaymentException
|
||||||
|
from pretix.control.permissions import event_permission_required
|
||||||
|
from pretix.helpers.http import redirect_to_url
|
||||||
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
|
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||||
|
from pretix.plugins.paypal.payment import Paypal
|
||||||
|
|
||||||
|
logger = logging.getLogger('pretix.plugins.paypal')
|
||||||
|
|
||||||
|
|
||||||
|
def success(request, *args, **kwargs):
|
||||||
|
pid = request.GET.get('paymentId')
|
||||||
|
token = request.GET.get('token')
|
||||||
|
payer = request.GET.get('PayerID')
|
||||||
|
request.session['payment_paypal_token'] = token
|
||||||
|
request.session['payment_paypal_payer'] = payer
|
||||||
|
|
||||||
|
urlkwargs = {}
|
||||||
|
if 'cart_namespace' in kwargs:
|
||||||
|
urlkwargs['cart_namespace'] = kwargs['cart_namespace']
|
||||||
|
|
||||||
|
if request.session.get('payment_paypal_payment'):
|
||||||
|
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
|
||||||
|
else:
|
||||||
|
payment = None
|
||||||
|
|
||||||
|
if pid == request.session.get('payment_paypal_id', None):
|
||||||
|
if payment:
|
||||||
|
prov = Paypal(request.event)
|
||||||
|
try:
|
||||||
|
resp = prov.execute_payment(request, payment)
|
||||||
|
except PaymentException as e:
|
||||||
|
messages.error(request, str(e))
|
||||||
|
urlkwargs['step'] = 'payment'
|
||||||
|
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
else:
|
||||||
|
messages.error(request, _('Invalid response from PayPal received.'))
|
||||||
|
logger.error('Session did not contain payment_paypal_id')
|
||||||
|
urlkwargs['step'] = 'payment'
|
||||||
|
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||||
|
|
||||||
|
if payment:
|
||||||
|
return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||||
|
'order': payment.order.code,
|
||||||
|
'secret': payment.order.secret
|
||||||
|
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
|
||||||
|
else:
|
||||||
|
urlkwargs['step'] = 'confirm'
|
||||||
|
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def abort(request, *args, **kwargs):
|
||||||
|
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
||||||
|
|
||||||
|
if request.session.get('payment_paypal_payment'):
|
||||||
|
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
|
||||||
|
else:
|
||||||
|
payment = None
|
||||||
|
|
||||||
|
if payment:
|
||||||
|
return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||||
|
'order': payment.order.code,
|
||||||
|
'secret': payment.order.secret
|
||||||
|
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
|
||||||
|
else:
|
||||||
|
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_POST
|
||||||
|
@scopes_disabled()
|
||||||
|
def webhook(request, *args, **kwargs):
|
||||||
|
event_body = request.body.decode('utf-8').strip()
|
||||||
|
event_json = json.loads(event_body)
|
||||||
|
|
||||||
|
# We do not check the signature, we just use it as a trigger to look the charge up.
|
||||||
|
if 'resource_type' not in event_json:
|
||||||
|
return HttpResponse("Invalid body, no resource_type given", status=400)
|
||||||
|
if event_json['resource_type'] not in ('sale', 'refund'):
|
||||||
|
return HttpResponse("Not interested in this resource type", status=200)
|
||||||
|
|
||||||
|
if event_json['resource_type'] == 'sale':
|
||||||
|
saleid = event_json['resource']['id']
|
||||||
|
else:
|
||||||
|
saleid = event_json['resource']['sale_id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
refs = [saleid]
|
||||||
|
if event_json['resource'].get('parent_payment'):
|
||||||
|
refs.append(event_json['resource'].get('parent_payment'))
|
||||||
|
|
||||||
|
rso = ReferencedPayPalObject.objects.select_related('order', 'order__event').get(
|
||||||
|
reference__in=refs
|
||||||
|
)
|
||||||
|
event = rso.order.event
|
||||||
|
except ReferencedPayPalObject.DoesNotExist:
|
||||||
|
rso = None
|
||||||
|
if hasattr(request, 'event'):
|
||||||
|
event = request.event
|
||||||
|
else:
|
||||||
|
return HttpResponse("Unable to detect event", status=200)
|
||||||
|
|
||||||
|
prov = Paypal(event)
|
||||||
|
prov.init_api()
|
||||||
|
|
||||||
|
try:
|
||||||
|
sale = paypalrestsdk.Sale.find(saleid)
|
||||||
|
except paypalrestsdk.exceptions.ConnectionError:
|
||||||
|
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||||
|
return HttpResponse('Sale not found', status=500)
|
||||||
|
|
||||||
|
if rso and rso.payment:
|
||||||
|
payment = rso.payment
|
||||||
|
else:
|
||||||
|
payments = OrderPayment.objects.filter(order__event=event, provider='paypal',
|
||||||
|
info__icontains=sale['id'])
|
||||||
|
payment = None
|
||||||
|
for p in payments:
|
||||||
|
payment_info = p.info_data
|
||||||
|
for res in payment_info['transactions'][0]['related_resources']:
|
||||||
|
for k, v in res.items():
|
||||||
|
if k == 'sale' and v['id'] == sale['id']:
|
||||||
|
payment = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not payment:
|
||||||
|
return HttpResponse('Payment not found', status=200)
|
||||||
|
|
||||||
|
payment.order.log_action('pretix.plugins.paypal.event', data=event_json)
|
||||||
|
|
||||||
|
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['state'] in ('partially_refunded', 'refunded'):
|
||||||
|
if event_json['resource_type'] == 'refund':
|
||||||
|
try:
|
||||||
|
refund = paypalrestsdk.Refund.find(event_json['resource']['id'])
|
||||||
|
except paypalrestsdk.exceptions.ConnectionError:
|
||||||
|
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||||
|
return HttpResponse('Refund not found', status=500)
|
||||||
|
|
||||||
|
known_refunds = {r.info_data.get('id'): r for r in payment.refunds.all()}
|
||||||
|
if refund['id'] not in known_refunds:
|
||||||
|
payment.create_external_refund(
|
||||||
|
amount=abs(Decimal(refund['amount']['total'])),
|
||||||
|
info=json.dumps(refund.to_dict() if not isinstance(refund, dict) else refund)
|
||||||
|
)
|
||||||
|
elif known_refunds.get(refund['id']).state in (
|
||||||
|
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT) and refund['state'] == 'completed':
|
||||||
|
known_refunds.get(refund['id']).done()
|
||||||
|
|
||||||
|
if 'total_refunded_amount' in refund:
|
||||||
|
known_sum = payment.refunds.filter(
|
||||||
|
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||||
|
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
|
||||||
|
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||||
|
total_refunded_amount = Decimal(refund['total_refunded_amount']['value'])
|
||||||
|
if known_sum < total_refunded_amount:
|
||||||
|
payment.create_external_refund(
|
||||||
|
amount=total_refunded_amount - known_sum
|
||||||
|
)
|
||||||
|
elif sale['state'] == 'refunded':
|
||||||
|
known_sum = payment.refunds.filter(
|
||||||
|
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||||
|
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
|
||||||
|
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||||
|
|
||||||
|
if known_sum < payment.amount:
|
||||||
|
payment.create_external_refund(
|
||||||
|
amount=payment.amount - known_sum
|
||||||
|
)
|
||||||
|
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED,
|
||||||
|
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED) and sale['state'] == 'completed':
|
||||||
|
try:
|
||||||
|
payment.confirm()
|
||||||
|
except Quota.QuotaExceededException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@event_permission_required('event.settings.general:write')
|
||||||
|
@require_POST
|
||||||
|
def oauth_disconnect(request, **kwargs):
|
||||||
|
del request.event.settings.payment_paypal_connect_refresh_token
|
||||||
|
del request.event.settings.payment_paypal_connect_user_id
|
||||||
|
request.event.settings.payment_paypal__enabled = False
|
||||||
|
messages.success(request, _('Your PayPal account has been disconnected.'))
|
||||||
|
|
||||||
|
# Migrate User to PayPal v2
|
||||||
|
event = request.event
|
||||||
|
event.disable_plugin("pretix.plugins.paypal")
|
||||||
|
event.enable_plugin("pretix.plugins.paypal2")
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={
|
||||||
|
'organizer': request.event.organizer.slug,
|
||||||
|
'event': request.event.slug,
|
||||||
|
'provider': 'paypal_settings'
|
||||||
|
}))
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Generated by Django 5.2.17 on 2026-09-08 08:01
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0310_question_valid_string_length_min"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.SeparateDatabaseAndState(
|
|
||||||
state_operations=[
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="ReferencedPayPalObject",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True, primary_key=True, serialize=False
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"reference",
|
|
||||||
models.CharField(db_index=True, max_length=190, unique=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"order",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="pretixbase.order",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"payment",
|
|
||||||
models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="pretixbase.orderpayment",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"db_table": "paypal_referencedpaypalobject",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
database_operations=[]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -68,7 +68,7 @@ from pretix.plugins.paypal2.client.core.paypal_http_client import (
|
|||||||
from pretix.plugins.paypal2.client.customer.partner_referral_create_request import (
|
from pretix.plugins.paypal2.client.customer.partner_referral_create_request import (
|
||||||
PartnerReferralCreateRequest,
|
PartnerReferralCreateRequest,
|
||||||
)
|
)
|
||||||
from pretix.plugins.paypal2.models import ReferencedPayPalObject
|
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.plugins.paypal2')
|
logger = logging.getLogger('pretix.plugins.paypal2')
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ from pretix.multidomain.urlreverse import eventreverse
|
|||||||
from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_request import (
|
from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_request import (
|
||||||
PartnersMerchantIntegrationsGetRequest,
|
PartnersMerchantIntegrationsGetRequest,
|
||||||
)
|
)
|
||||||
from pretix.plugins.paypal2.models import ReferencedPayPalObject
|
|
||||||
from pretix.plugins.paypal2.payment import (
|
from pretix.plugins.paypal2.payment import (
|
||||||
PaypalMethod, PaypalMethod as Paypal, PaypalWallet,
|
PaypalMethod, PaypalMethod as Paypal, PaypalWallet,
|
||||||
)
|
)
|
||||||
|
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||||
from pretix.presale.views import get_cart
|
from pretix.presale.views import get_cart
|
||||||
from pretix.presale.views.cart import cart_session
|
from pretix.presale.views.cart import cart_session
|
||||||
|
|
||||||
@@ -350,8 +350,8 @@ def webhook(request, *args, **kwargs):
|
|||||||
return HttpResponse("Invalid body, no event_type given", status=400)
|
return HttpResponse("Invalid body, no event_type given", status=400)
|
||||||
|
|
||||||
if event_json['event_type'].startswith('PAYMENT.SALE.'):
|
if event_json['event_type'].startswith('PAYMENT.SALE.'):
|
||||||
logger.info(f"Received PPv1 webhook: {json.dumps(event_json)}")
|
from pretix.plugins.paypal.views import webhook
|
||||||
return HttpResponse("PayPal V1 no longer supported", status=400)
|
return webhook(request, *args, **kwargs)
|
||||||
# V1/V2 Sorting -- End
|
# V1/V2 Sorting -- End
|
||||||
|
|
||||||
# We do not check the signature, we just use it as a trigger to look the charge up.
|
# We do not check the signature, we just use it as a trigger to look the charge up.
|
||||||
|
|||||||
@@ -247,15 +247,14 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if item.hidden_if_item_available:
|
if item.hidden_if_item_available:
|
||||||
time_available = item.hidden_if_item_available.is_available()
|
|
||||||
if item.hidden_if_item_available.has_variations:
|
if item.hidden_if_item_available.has_variations:
|
||||||
item._dependency_available = any(
|
item._dependency_available = any(
|
||||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
||||||
# is_available on variant is evaluated called by available_variations
|
|
||||||
for var in item.hidden_if_item_available.available_variations
|
for var in item.hidden_if_item_available.available_variations
|
||||||
) and time_available
|
)
|
||||||
else:
|
else:
|
||||||
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||||
|
time_available = item.hidden_if_item_available.is_available()
|
||||||
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
|
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
|
||||||
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
|
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
|
||||||
item._remove = True
|
item._remove = True
|
||||||
|
|||||||
@@ -139,6 +139,9 @@
|
|||||||
{% if event_logo and event_logo_show_title %}
|
{% if event_logo and event_logo_show_title %}
|
||||||
<h2 class="content-header">
|
<h2 class="content-header">
|
||||||
{{ event.name }}
|
{{ event.name }}
|
||||||
|
{% if request.event.settings.show_dates_on_frontpage %}
|
||||||
|
<small>{{ event.get_date_range_display_as_html }}</small>
|
||||||
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if frontpage_text %}
|
{% if frontpage_text %}
|
||||||
|
|||||||
+19
-1
@@ -128,6 +128,23 @@ elif 'mysql' in db_backend:
|
|||||||
DATABASE_ADVISORY_LOCK_INDEX = config.getint('database', 'advisory_lock_index', fallback=0)
|
DATABASE_ADVISORY_LOCK_INDEX = config.getint('database', 'advisory_lock_index', fallback=0)
|
||||||
|
|
||||||
db_options = {}
|
db_options = {}
|
||||||
|
if 'postgresql' in db_backend:
|
||||||
|
db_options["pool"] = {
|
||||||
|
# https://docs.djangoproject.com/en/6.1/ref/databases/#connection-pool
|
||||||
|
# "Django maintains a separate pool for each database alias in each process"
|
||||||
|
# We run both celery and gunicorn in multi-process mode, so we should get away with one connection per process
|
||||||
|
# per alias, so let's try to use a really small pool size.
|
||||||
|
"min_size": 1,
|
||||||
|
"max_size": 2,
|
||||||
|
# We set low lifetime values since we want to quickly get rid of all old connections if something changes
|
||||||
|
# on the database end, e.g. a cluster failover.
|
||||||
|
"max_idle": 120,
|
||||||
|
"max_lifetime": 120,
|
||||||
|
# A check callable is set by Django automatically since we have CONN_HEALTH_CHECKS enabled.
|
||||||
|
# In the future, we could provide different callables for default and replica databases if we want to test
|
||||||
|
# actual writability for the default database, but it might not be necessary if a failover triggers a reconnect
|
||||||
|
# anyways.
|
||||||
|
}
|
||||||
|
|
||||||
postgresql_sslmode = config.get('database', 'sslmode', fallback='disable')
|
postgresql_sslmode = config.get('database', 'sslmode', fallback='disable')
|
||||||
USE_DATABASE_TLS = postgresql_sslmode != 'disable'
|
USE_DATABASE_TLS = postgresql_sslmode != 'disable'
|
||||||
@@ -162,7 +179,7 @@ DATABASES = {
|
|||||||
'PASSWORD': config.get('database', 'password', fallback=''),
|
'PASSWORD': config.get('database', 'password', fallback=''),
|
||||||
'HOST': config.get('database', 'host', fallback=''),
|
'HOST': config.get('database', 'host', fallback=''),
|
||||||
'PORT': config.get('database', 'port', fallback=''),
|
'PORT': config.get('database', 'port', fallback=''),
|
||||||
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120,
|
'CONN_MAX_AGE': 0,
|
||||||
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3',
|
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3',
|
||||||
'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors,
|
'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors,
|
||||||
'OPTIONS': db_options,
|
'OPTIONS': db_options,
|
||||||
@@ -382,6 +399,7 @@ if HAS_CELERY:
|
|||||||
if HAS_CELERY_BACKEND_TRANSPORT_OPTS:
|
if HAS_CELERY_BACKEND_TRANSPORT_OPTS:
|
||||||
CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = loads(config.get('celery', 'backend_transport_options'))
|
CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = loads(config.get('celery', 'backend_transport_options'))
|
||||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
||||||
|
CELERY_DB_REUSE_MAX = sys.maxsize # Actual reuse will be governed by psycopg3 pool features
|
||||||
|
|
||||||
if CELERY_BROKER_URL.startswith("amqp://"):
|
if CELERY_BROKER_URL.startswith("amqp://"):
|
||||||
# https://docs.celeryq.dev/en/latest/userguide/routing.html#routing-options-rabbitmq-priorities
|
# https://docs.celeryq.dev/en/latest/userguide/routing.html#routing-options-rabbitmq-priorities
|
||||||
|
|||||||
@@ -448,7 +448,6 @@ var editor = {
|
|||||||
var d = data[i], o
|
var d = data[i], o
|
||||||
editor._add_from_data(d)
|
editor._add_from_data(d)
|
||||||
}
|
}
|
||||||
editor.fabric.discardActiveObject()
|
|
||||||
editor.fabric.renderAll()
|
editor.fabric.renderAll()
|
||||||
editor._update_toolbox_values()
|
editor._update_toolbox_values()
|
||||||
},
|
},
|
||||||
@@ -957,7 +956,6 @@ var editor = {
|
|||||||
mtr: true
|
mtr: true
|
||||||
})
|
})
|
||||||
editor.fabric.add(text)
|
editor.fabric.add(text)
|
||||||
editor.fabric.setActiveObject(text)
|
|
||||||
editor._create_savepoint()
|
editor._create_savepoint()
|
||||||
return text
|
return text
|
||||||
},
|
},
|
||||||
@@ -974,7 +972,6 @@ var editor = {
|
|||||||
rect.scaleToHeight(126)
|
rect.scaleToHeight(126)
|
||||||
rect.setControlsVisibility({ mtr: false, mb: false, mt: false, mr: false, ml: false })
|
rect.setControlsVisibility({ mtr: false, mb: false, mt: false, mr: false, ml: false })
|
||||||
editor.fabric.add(rect)
|
editor.fabric.add(rect)
|
||||||
editor.fabric.setActiveObject(rect)
|
|
||||||
editor._create_savepoint()
|
editor._create_savepoint()
|
||||||
return rect
|
return rect
|
||||||
},
|
},
|
||||||
@@ -1012,7 +1009,6 @@ var editor = {
|
|||||||
mtr: true
|
mtr: true
|
||||||
})
|
})
|
||||||
editor.fabric.add(rect)
|
editor.fabric.add(rect)
|
||||||
editor.fabric.setActiveObject(rect)
|
|
||||||
editor._create_savepoint()
|
editor._create_savepoint()
|
||||||
$('#version-notice').show()
|
$('#version-notice').show()
|
||||||
return rect
|
return rect
|
||||||
@@ -1030,7 +1026,6 @@ var editor = {
|
|||||||
})
|
})
|
||||||
rect.setControlsVisibility({ mtr: false })
|
rect.setControlsVisibility({ mtr: false })
|
||||||
editor.fabric.add(rect)
|
editor.fabric.add(rect)
|
||||||
editor.fabric.setActiveObject(rect)
|
|
||||||
editor._create_savepoint()
|
editor._create_savepoint()
|
||||||
return rect
|
return rect
|
||||||
},
|
},
|
||||||
@@ -1049,7 +1044,6 @@ var editor = {
|
|||||||
})
|
})
|
||||||
rect.setControlsVisibility({ mtr: false, mb: false, mt: false, mr: false, ml: false })
|
rect.setControlsVisibility({ mtr: false, mb: false, mt: false, mr: false, ml: false })
|
||||||
editor.fabric.add(rect)
|
editor.fabric.add(rect)
|
||||||
editor.fabric.setActiveObject(rect)
|
|
||||||
editor._create_savepoint()
|
editor._create_savepoint()
|
||||||
return rect
|
return rect
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -401,9 +401,6 @@ let form_handlers = function (el) {
|
|||||||
if (tagName !== 'div' && tagName !== 'button') {
|
if (tagName !== 'div' && tagName !== 'button') {
|
||||||
$toggling = dependent.closest('.form-group')
|
$toggling = dependent.closest('.form-group')
|
||||||
}
|
}
|
||||||
if ($toggling.find(".has-error").length > 0) {
|
|
||||||
enabled = true // Don't hide error message that makes form unsubmittable
|
|
||||||
}
|
|
||||||
if (ev) {
|
if (ev) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
$toggling.stop().slideDown()
|
$toggling.stop().slideDown()
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ td(
|
|||||||
)
|
)
|
||||||
.pretix-widget-event-calendar-day(v-if="day", :aria-label="dateStr") {{ daynum }}
|
.pretix-widget-event-calendar-day(v-if="day", :aria-label="dateStr") {{ daynum }}
|
||||||
.pretix-widget-event-calendar-events(v-if="day")
|
.pretix-widget-event-calendar-events(v-if="day")
|
||||||
EventCalendarEvent(v-for="e in day.events", :key="e.event_url+'-'+(e.subevent||'')", :event="e")
|
EventCalendarEvent(v-for="e in day.events", :key="e.event_url", :event="e")
|
||||||
</template>
|
</template>
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ CACHES = {
|
|||||||
DATABASE_REPLICA = 'default'
|
DATABASE_REPLICA = 'default'
|
||||||
DATABASES['default']['CONN_MAX_AGE'] = 0
|
DATABASES['default']['CONN_MAX_AGE'] = 0
|
||||||
DATABASES.pop('replica', None)
|
DATABASES.pop('replica', None)
|
||||||
|
if 'pool' in DATABASES['default']['OPTIONS']:
|
||||||
|
DATABASES['default']['OPTIONS']['pool']['max_size'] = 5
|
||||||
|
|
||||||
MIDDLEWARE.insert(0, 'pretix.testutils.middleware.DebugFlagMiddleware')
|
MIDDLEWARE.insert(0, 'pretix.testutils.middleware.DebugFlagMiddleware')
|
||||||
|
|
||||||
|
|||||||
@@ -61,15 +61,6 @@ def meta_prop(organizer):
|
|||||||
return organizer.meta_properties.create(name="type", default="Concert")
|
return organizer.meta_properties.create(name="type", default="Concert")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
@scopes_disabled()
|
|
||||||
def meta_prop_choices(organizer):
|
|
||||||
return organizer.meta_properties.create(name="department", default="A", choices=[
|
|
||||||
{"key": "A", "label": {"en": "Group A"}},
|
|
||||||
{"key": "B", "label": {"en": "Group B"}},
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def event(organizer, meta_prop):
|
def event(organizer, meta_prop):
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
import pytest
|
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from i18nfield.strings import LazyI18nString
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def event_meta_property(organizer):
|
|
||||||
return organizer.meta_properties.create(
|
|
||||||
name="Color",
|
|
||||||
default="Red",
|
|
||||||
required=False,
|
|
||||||
choices=[
|
|
||||||
{
|
|
||||||
"key": "Red",
|
|
||||||
"label": LazyI18nString("Rot"),
|
|
||||||
"DELETE": False,
|
|
||||||
"ORDER": 1,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
TEST_TYPE_RES = {
|
|
||||||
"name": "Color",
|
|
||||||
"default": "Red",
|
|
||||||
"required": False,
|
|
||||||
"choices": [{"key": "Red", "label": {"en": "Rot"}}],
|
|
||||||
'filter_allowed': True,
|
|
||||||
'filter_public': False,
|
|
||||||
'protected': False,
|
|
||||||
'public_label': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_meta_property_list(token_client, organizer, event_meta_property):
|
|
||||||
res = dict(TEST_TYPE_RES)
|
|
||||||
|
|
||||||
resp = token_client.get('/api/v1/organizers/{}/event_meta_properties/'.format(organizer.slug))
|
|
||||||
assert resp.status_code == 200
|
|
||||||
event_meta_property.refresh_from_db()
|
|
||||||
res["id"] = event_meta_property.pk
|
|
||||||
assert res in resp.data['results']
|
|
||||||
assert len(resp.data['results']) == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_meta_property_detail(token_client, organizer, event_meta_property):
|
|
||||||
res = TEST_TYPE_RES
|
|
||||||
resp = token_client.get('/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk))
|
|
||||||
assert resp.status_code == 200
|
|
||||||
event_meta_property.refresh_from_db()
|
|
||||||
res["id"] = event_meta_property.pk
|
|
||||||
assert res == resp.data
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_meta_property_create(token_client, organizer):
|
|
||||||
url = '/api/v1/organizers/{}/event_meta_properties/'.format(organizer.slug)
|
|
||||||
resp = token_client.post(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
"name": "Color",
|
|
||||||
"default": "",
|
|
||||||
"required": False,
|
|
||||||
"choices": [
|
|
||||||
{"key": {"foo": "bar"}},
|
|
||||||
"blabla",
|
|
||||||
{"label": "Green"},
|
|
||||||
{"key": "g", "label": "Green", "foo": "bar"},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert str(resp.data["choices"][0][0]) == "Meta property value options must have a key of type string."
|
|
||||||
assert str(resp.data["choices"][1][0]) == "Meta property value options must be a dict."
|
|
||||||
assert str(resp.data["choices"][2][0]) == "Meta property value options must have a key of type string."
|
|
||||||
assert str(resp.data["choices"][3][0]) == "Meta property value options may only have a key and optionally a label."
|
|
||||||
|
|
||||||
resp = token_client.post(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
"name": "Color",
|
|
||||||
"default": "Red",
|
|
||||||
"required": False,
|
|
||||||
"choices": {"key": "r", "label": "Red"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert str(resp.data["choices"][0]) == 'Expected a list of items but got type "dict".'
|
|
||||||
|
|
||||||
resp = token_client.post(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
"name": "Color",
|
|
||||||
"default": "r",
|
|
||||||
"required": False,
|
|
||||||
"choices": [
|
|
||||||
{"key": "r", "label": {"en": "Red"}},
|
|
||||||
{"key": "r", "label": {"en": "Razzmatazz"}},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert str(resp.data["choices"][0]) == "The key for each meta property value option must be unique."
|
|
||||||
|
|
||||||
choices = [
|
|
||||||
{"key": "r", "label": "Red"},
|
|
||||||
{"key": "g", "label": "Green"},
|
|
||||||
{"key": "b", "label": "Blue"},
|
|
||||||
]
|
|
||||||
resp = token_client.post(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
"name": "Color",
|
|
||||||
"default": "k",
|
|
||||||
"required": False,
|
|
||||||
"choices": choices,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert str(resp.data["non_field_errors"][0]) == "You cannot set a default value that is not a valid value."
|
|
||||||
|
|
||||||
resp = token_client.post(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
"name": "Color",
|
|
||||||
"default": "r",
|
|
||||||
"required": False,
|
|
||||||
"choices": choices,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 201
|
|
||||||
with scopes_disabled():
|
|
||||||
event_meta_property = organizer.meta_properties.get(id=resp.data['id'])
|
|
||||||
assert event_meta_property.name == "Color"
|
|
||||||
assert event_meta_property.default == "r"
|
|
||||||
assert event_meta_property.choices == choices
|
|
||||||
assert not event_meta_property.required
|
|
||||||
assert len(organizer.meta_properties.all()) == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_meta_property_patch(token_client, organizer, event_meta_property):
|
|
||||||
url = '/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk)
|
|
||||||
resp = token_client.patch(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
# existing default is not in choices
|
|
||||||
"choices": [{'key': 'k', 'label': 'Black'}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert str(resp.data["non_field_errors"][0]) == "You cannot set a default value that is not a valid value."
|
|
||||||
|
|
||||||
resp = token_client.patch(
|
|
||||||
"/api/v1/organizers/{}/event_meta_properties/{}/"
|
|
||||||
.format(organizer.slug, event_meta_property.pk),
|
|
||||||
format="json",
|
|
||||||
data={
|
|
||||||
"choices": [
|
|
||||||
{"key": "r", "label": ["wrong"]},
|
|
||||||
{"key": "g", "label": {"de": {"de": 123, "en": "wrong"}}}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert str(resp.data["choices"][0]["label"][0]) == "Invalid data type."
|
|
||||||
assert str(resp.data["choices"][1]["label"][0]) == "All entries must be strings."
|
|
||||||
|
|
||||||
resp = token_client.patch(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
"choices": [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200
|
|
||||||
event_meta_property.refresh_from_db()
|
|
||||||
assert event_meta_property.choices is None
|
|
||||||
|
|
||||||
resp = token_client.patch(
|
|
||||||
url,
|
|
||||||
format='json',
|
|
||||||
data={
|
|
||||||
"required": True,
|
|
||||||
"choices": None,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200
|
|
||||||
event_meta_property.refresh_from_db()
|
|
||||||
assert event_meta_property.required
|
|
||||||
assert event_meta_property.choices is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_meta_property_delete(token_client, organizer, event_meta_property):
|
|
||||||
resp = token_client.delete(
|
|
||||||
'/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk),
|
|
||||||
)
|
|
||||||
assert resp.status_code == 204
|
|
||||||
assert len(organizer.meta_properties.all()) == 0
|
|
||||||
@@ -703,7 +703,7 @@ def test_event_delete_with_clone(token_client, organizer, event, meta_prop):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_event_update(token_client, team, organizer, event, item, meta_prop, meta_prop_choices):
|
def test_event_update(token_client, organizer, event, item, meta_prop):
|
||||||
resp = token_client.patch(
|
resp = token_client.patch(
|
||||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||||
{
|
{
|
||||||
@@ -782,26 +782,6 @@ def test_event_update(token_client, team, organizer, event, item, meta_prop, met
|
|||||||
property__name=meta_prop.name, value="Workshop"
|
property__name=meta_prop.name, value="Workshop"
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
resp = token_client.patch(
|
|
||||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
|
||||||
{
|
|
||||||
"meta_data": {
|
|
||||||
meta_prop.name: "Workshop",
|
|
||||||
meta_prop_choices.name: "B"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
format='json'
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert resp.data["meta_data"] == {
|
|
||||||
meta_prop.name: "Workshop",
|
|
||||||
meta_prop_choices.name: "B"
|
|
||||||
}
|
|
||||||
with scopes_disabled():
|
|
||||||
assert organizer.events.get(slug=resp.data['slug']).meta_values.filter(
|
|
||||||
property__name=meta_prop_choices.name, value="B"
|
|
||||||
).exists()
|
|
||||||
|
|
||||||
resp = token_client.patch(
|
resp = token_client.patch(
|
||||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||||
{
|
{
|
||||||
@@ -816,51 +796,6 @@ def test_event_update(token_client, team, organizer, event, item, meta_prop, met
|
|||||||
property__name=meta_prop.name
|
property__name=meta_prop.name
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
resp = token_client.patch(
|
|
||||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
|
||||||
{
|
|
||||||
"meta_data": {
|
|
||||||
meta_prop_choices.name: "invalid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
format='json'
|
|
||||||
)
|
|
||||||
assert resp.status_code == 400
|
|
||||||
assert resp.content.decode() == '{"meta_data":["Meta data property \'department\' does not allow value \'invalid\'."]}'
|
|
||||||
|
|
||||||
meta_prop_choices.protected = True
|
|
||||||
meta_prop_choices.save()
|
|
||||||
team.all_organizer_permissions = False
|
|
||||||
team.limit_organizer_permissions = {}
|
|
||||||
team.save()
|
|
||||||
resp = token_client.patch(
|
|
||||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
|
||||||
{
|
|
||||||
"meta_data": {
|
|
||||||
meta_prop_choices.name: "A"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
format='json'
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200 # silently ignored
|
|
||||||
assert resp.data["meta_data"] == {}
|
|
||||||
|
|
||||||
team.all_organizer_permissions = True
|
|
||||||
team.save()
|
|
||||||
resp = token_client.patch(
|
|
||||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
|
||||||
{
|
|
||||||
"meta_data": {
|
|
||||||
meta_prop_choices.name: "A"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
format='json'
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert resp.data["meta_data"] == {
|
|
||||||
meta_prop_choices.name: "A"
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = token_client.patch(
|
resp = token_client.patch(
|
||||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -223,8 +223,6 @@ org_permission_sub_urls = [
|
|||||||
('post', 'organizer.customers:write', 'customers/1/anonymize/', 404),
|
('post', 'organizer.customers:write', 'customers/1/anonymize/', 404),
|
||||||
('put', 'organizer.customers:write', 'customers/1/', 404),
|
('put', 'organizer.customers:write', 'customers/1/', 404),
|
||||||
('delete', 'organizer.customers:write', 'customers/1/', 404),
|
('delete', 'organizer.customers:write', 'customers/1/', 404),
|
||||||
('post', 'organizer.settings.general:write', 'event_meta_properties/', 400),
|
|
||||||
('patch', 'organizer.settings.general:write', 'event_meta_properties/0/', 404),
|
|
||||||
('get', 'organizer.customers:read', 'memberships/', 200),
|
('get', 'organizer.customers:read', 'memberships/', 200),
|
||||||
('post', 'organizer.customers:write', 'memberships/', 400),
|
('post', 'organizer.customers:write', 'memberships/', 400),
|
||||||
('get', 'organizer.customers:read', 'memberships/1/', 404),
|
('get', 'organizer.customers:read', 'memberships/1/', 404),
|
||||||
|
|||||||
@@ -115,15 +115,6 @@ def test_voucher_list(token_client, organizer, event, voucher, item, quota, sube
|
|||||||
)
|
)
|
||||||
assert [] == resp.data['results']
|
assert [] == resp.data['results']
|
||||||
|
|
||||||
resp = token_client.get(
|
|
||||||
'/api/v1/organizers/{}/events/{}/vouchers/?search={}'.format(organizer.slug, event.slug, voucher.code[:3])
|
|
||||||
)
|
|
||||||
assert [res] == resp.data['results']
|
|
||||||
resp = token_client.get(
|
|
||||||
'/api/v1/organizers/{}/events/{}/vouchers/?code=RAvRcnbDehWD59oywV5x'.format(organizer.slug, event.slug)
|
|
||||||
)
|
|
||||||
assert [] == resp.data['results']
|
|
||||||
|
|
||||||
resp = token_client.get(
|
resp = token_client.get(
|
||||||
'/api/v1/organizers/{}/events/{}/vouchers/?max_usages=1'.format(organizer.slug, event.slug)
|
'/api/v1/organizers/{}/events/{}/vouchers/?max_usages=1'.format(organizer.slug, event.slug)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def event():
|
|||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def event():
|
|||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def env():
|
|||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
||||||
|
|||||||
@@ -349,25 +349,25 @@ class UserSettings2FATest(SoupTest):
|
|||||||
|
|
||||||
def test_delete_u2f(self):
|
def test_delete_u2f(self):
|
||||||
d = U2FDevice.objects.create(user=self.user, name='Test')
|
d = U2FDevice.objects.create(user=self.user, name='Test')
|
||||||
self.client.get('/control/settings/2fa/pretixbase.u2fdevice/{}/delete'.format(d.pk))
|
self.client.get('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
|
||||||
self.client.post('/control/settings/2fa/pretixbase.u2fdevice/{}/delete'.format(d.pk))
|
self.client.post('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
|
||||||
assert not U2FDevice.objects.exists()
|
assert not U2FDevice.objects.exists()
|
||||||
|
|
||||||
def test_delete_webauthn(self):
|
def test_delete_webauthn(self):
|
||||||
d = WebAuthnDevice.objects.create(user=self.user, name='Test')
|
d = WebAuthnDevice.objects.create(user=self.user, name='Test')
|
||||||
self.client.get('/control/settings/2fa/pretixbase.webauthndevice/{}/delete'.format(d.pk))
|
self.client.get('/control/settings/2fa/webauthn/{}/delete'.format(d.pk))
|
||||||
self.client.post('/control/settings/2fa/pretixbase.webauthndevice/{}/delete'.format(d.pk))
|
self.client.post('/control/settings/2fa/webauthn/{}/delete'.format(d.pk))
|
||||||
assert not WebAuthnDevice.objects.exists()
|
assert not WebAuthnDevice.objects.exists()
|
||||||
|
|
||||||
def test_delete_totp(self):
|
def test_delete_totp(self):
|
||||||
d = TOTPDevice.objects.create(user=self.user, name='Test')
|
d = TOTPDevice.objects.create(user=self.user, name='Test')
|
||||||
self.client.get('/control/settings/2fa/otp_totp.totpdevice/{}/delete'.format(d.pk))
|
self.client.get('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
||||||
self.client.post('/control/settings/2fa/otp_totp.totpdevice/{}/delete'.format(d.pk))
|
self.client.post('/control/settings/2fa/totp/{}/delete'.format(d.pk))
|
||||||
assert not TOTPDevice.objects.exists()
|
assert not TOTPDevice.objects.exists()
|
||||||
|
|
||||||
def test_create_webauthn_require_https(self):
|
def test_create_webauthn_require_https(self):
|
||||||
r = self.client.post('/control/settings/2fa/add', {
|
r = self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'pretixbase.webauthndevice',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
})
|
})
|
||||||
assert 'alert-danger' in r.content.decode()
|
assert 'alert-danger' in r.content.decode()
|
||||||
@@ -376,7 +376,7 @@ class UserSettings2FATest(SoupTest):
|
|||||||
with mocker_context() as mocker:
|
with mocker_context() as mocker:
|
||||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'pretixbase.webauthndevice',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
})
|
})
|
||||||
d = WebAuthnDevice.objects.first()
|
d = WebAuthnDevice.objects.first()
|
||||||
@@ -385,7 +385,7 @@ class UserSettings2FATest(SoupTest):
|
|||||||
|
|
||||||
def test_create_totp(self):
|
def test_create_totp(self):
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'otp_totp.totpdevice',
|
'devicetype': 'totp',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
})
|
})
|
||||||
d = TOTPDevice.objects.first()
|
d = TOTPDevice.objects.first()
|
||||||
@@ -393,7 +393,7 @@ class UserSettings2FATest(SoupTest):
|
|||||||
|
|
||||||
def test_confirm_totp(self):
|
def test_confirm_totp(self):
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'otp_totp.totpdevice',
|
'devicetype': 'totp',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
d = TOTPDevice.objects.first()
|
d = TOTPDevice.objects.first()
|
||||||
@@ -411,7 +411,7 @@ class UserSettings2FATest(SoupTest):
|
|||||||
|
|
||||||
def test_confirm_totp_failed(self):
|
def test_confirm_totp_failed(self):
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'otp_totp.totpdevice',
|
'devicetype': 'totp',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
d = TOTPDevice.objects.first()
|
d = TOTPDevice.objects.first()
|
||||||
@@ -428,7 +428,7 @@ class UserSettings2FATest(SoupTest):
|
|||||||
with mocker_context() as mocker:
|
with mocker_context() as mocker:
|
||||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'pretixbase.webauthndevice',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
d = WebAuthnDevice.objects.first()
|
d = WebAuthnDevice.objects.first()
|
||||||
@@ -443,7 +443,7 @@ class UserSettings2FATest(SoupTest):
|
|||||||
with mocker_context() as mocker:
|
with mocker_context() as mocker:
|
||||||
mocker.patch('django.http.request.HttpRequest.is_secure')
|
mocker.patch('django.http.request.HttpRequest.is_secure')
|
||||||
self.client.post('/control/settings/2fa/add', {
|
self.client.post('/control/settings/2fa/add', {
|
||||||
'devicetype': 'pretixbase.webauthndevice',
|
'devicetype': 'webauthn',
|
||||||
'name': 'Foo'
|
'name': 'Foo'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
|
|
||||||
|
|||||||
@@ -37,26 +37,26 @@ def event():
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_require_plugin(event, client):
|
def test_require_plugin(event, client):
|
||||||
event.plugins = 'pretix.plugins.paypal2'
|
event.plugins = 'pretix.plugins.paypal'
|
||||||
event.live = True
|
event.live = True
|
||||||
event.save()
|
event.save()
|
||||||
r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
|
r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
event.plugins = ''
|
event.plugins = ''
|
||||||
event.save()
|
event.save()
|
||||||
r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
|
r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_require_live(event, client):
|
def test_require_live(event, client):
|
||||||
event.plugins = 'pretix.plugins.paypal2'
|
event.plugins = 'pretix.plugins.paypal'
|
||||||
event.live = True
|
event.live = True
|
||||||
event.save()
|
event.save()
|
||||||
r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
|
r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
|
|
||||||
event.live = False
|
event.live = False
|
||||||
event.save()
|
event.save()
|
||||||
r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
|
r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def env():
|
|||||||
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
event.settings.invoice_numbers_prefix = 'INV-'
|
event.settings.invoice_numbers_prefix = 'INV-'
|
||||||
event.settings.invoice_numbers_counter_length = 3
|
event.settings.invoice_numbers_counter_length = 3
|
||||||
@@ -843,7 +843,7 @@ def test_event_name_prefix_contains_dash(env, orga_job):
|
|||||||
for slug in slugs:
|
for slug in slugs:
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=event.organizer, name=slug.upper(), slug=slug,
|
organizer=event.organizer, name=slug.upper(), slug=slug,
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
o1.event = Event.objects.get(slug="dummy2345")
|
o1.event = Event.objects.get(slug="dummy2345")
|
||||||
@@ -871,7 +871,7 @@ def test_event_name_prefix_multiple_dashes(env, orga_job):
|
|||||||
for slug in slugs:
|
for slug in slugs:
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=event.organizer, name=slug.upper(), slug=slug,
|
organizer=event.organizer, name=slug.upper(), slug=slug,
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def env():
|
|||||||
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def env():
|
|||||||
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
||||||
@@ -69,7 +69,7 @@ def env():
|
|||||||
def refund_huf(env):
|
def refund_huf(env):
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=env[0].organizer, name='Dummy', slug='dummy2', currency='HUF',
|
organizer=env[0].organizer, name='Dummy', slug='dummy2', currency='HUF',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
)
|
)
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
code='HUFFY', event=event, email='admin@localhost',
|
code='HUFFY', event=event, email='admin@localhost',
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from pretix.base.models import (
|
||||||
|
CartPosition, Event, Item, ItemCategory, Organizer, Quota,
|
||||||
|
)
|
||||||
|
from pretix.testutils.sessions import add_cart_session, get_cart_session_key
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def env(client):
|
||||||
|
orga = Organizer.objects.create(name='CCC', slug='ccc')
|
||||||
|
event = Event.objects.create(
|
||||||
|
organizer=orga, name='30C3', slug='30c3',
|
||||||
|
date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
|
||||||
|
plugins='pretix.plugins.paypal',
|
||||||
|
live=True
|
||||||
|
)
|
||||||
|
category = ItemCategory.objects.create(event=event, name="Everything", position=0)
|
||||||
|
quota_tickets = Quota.objects.create(event=event, name='Tickets', size=5)
|
||||||
|
ticket = Item.objects.create(event=event, name='Early-bird ticket',
|
||||||
|
category=category, default_price=23, admission=True)
|
||||||
|
quota_tickets.items.add(ticket)
|
||||||
|
event.settings.set('attendee_names_asked', False)
|
||||||
|
event.settings.set('payment_paypal__enabled', True)
|
||||||
|
event.settings.set('payment_paypal__fee_abs', 3)
|
||||||
|
event.settings.set('payment_paypal_endpoint', 'sandbox')
|
||||||
|
event.settings.set('payment_paypal_client_id', '12345')
|
||||||
|
event.settings.set('payment_paypal_secret', '12345')
|
||||||
|
add_cart_session(client, event, {'email': 'admin@localhost'})
|
||||||
|
return client, ticket
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_payment(env, monkeypatch):
|
||||||
|
def create_payment(self, request, payment):
|
||||||
|
assert payment['intent'] == 'sale'
|
||||||
|
assert payment['transactions'][0]['amount']['currency'] == 'EUR'
|
||||||
|
assert payment['transactions'][0]['amount']['total'] == '26.00'
|
||||||
|
create_payment.called = True
|
||||||
|
return 'https://approve.url'
|
||||||
|
monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal._create_payment", create_payment)
|
||||||
|
|
||||||
|
client, ticket = env
|
||||||
|
session_key = get_cart_session_key(client, ticket.event)
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=ticket.event, cart_id=session_key, item=ticket,
|
||||||
|
price=23, expires=now() + datetime.timedelta(minutes=10)
|
||||||
|
)
|
||||||
|
client.get('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), follow=True)
|
||||||
|
client.post('/%s/%s/checkout/questions/' % (ticket.event.organizer.slug, ticket.event.slug), {
|
||||||
|
'email': 'admin@localhost'
|
||||||
|
}, follow=True)
|
||||||
|
response = client.post('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), {
|
||||||
|
'payment': 'paypal'
|
||||||
|
})
|
||||||
|
assert response['Location'] == 'https://approve.url'
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||||
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||||
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||||
|
#
|
||||||
|
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pretix.base.models import Event, Organizer, Team, User
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def env(client):
|
||||||
|
orga = Organizer.objects.create(name='CCC', slug='ccc')
|
||||||
|
event = Event.objects.create(
|
||||||
|
organizer=orga, name='30C3', slug='30c3',
|
||||||
|
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
|
||||||
|
plugins='pretix.plugins.paypal',
|
||||||
|
live=True
|
||||||
|
)
|
||||||
|
event.settings.set('attendee_names_asked', False)
|
||||||
|
event.settings.set('payment_paypal__enabled', True)
|
||||||
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
|
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
||||||
|
t.members.add(user)
|
||||||
|
t.limit_events.add(event)
|
||||||
|
client.force_login(user)
|
||||||
|
return client, event
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_settings(env):
|
||||||
|
client, event = env
|
||||||
|
response = client.get('/control/event/%s/%s/settings/payment/paypal' % (event.organizer.slug, event.slug),
|
||||||
|
follow=True)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 'paypal__enabled' in response.rendered_content
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from pretix.base.models import (
|
||||||
|
Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
|
||||||
|
)
|
||||||
|
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def env():
|
||||||
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||||
|
event = Event.objects.create(
|
||||||
|
organizer=o, name='Dummy', slug='dummy', plugins='pretix.plugins.paypal',
|
||||||
|
date_from=now(), live=True
|
||||||
|
)
|
||||||
|
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
|
||||||
|
t.members.add(user)
|
||||||
|
t.limit_events.add(event)
|
||||||
|
o1 = Order.objects.create(
|
||||||
|
code='FOOBAR', event=event, email='dummy@dummy.test',
|
||||||
|
status=Order.STATUS_PAID,
|
||||||
|
datetime=now(), expires=now() + timedelta(days=10),
|
||||||
|
total=Decimal('13.37'),
|
||||||
|
sales_channel=o.sales_channels.get(identifier="web"),
|
||||||
|
)
|
||||||
|
o1.payments.create(
|
||||||
|
amount=o1.total,
|
||||||
|
provider='paypal',
|
||||||
|
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||||
|
info=json.dumps({
|
||||||
|
"id": "PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"create_time": "2013-02-19T22:01:53Z",
|
||||||
|
"update_time": "2013-02-19T22:01:55Z",
|
||||||
|
"state": "approved",
|
||||||
|
"intent": "sale",
|
||||||
|
"payer": {
|
||||||
|
"payment_method": "credit_card",
|
||||||
|
"funding_instruments": [
|
||||||
|
{
|
||||||
|
"credit_card": {
|
||||||
|
"type": "mastercard",
|
||||||
|
"number": "xxxxxxxxxxxx5559",
|
||||||
|
"expire_month": 2,
|
||||||
|
"expire_year": 2018,
|
||||||
|
"first_name": "Betsy",
|
||||||
|
"last_name": "Buyer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"amount": {
|
||||||
|
"total": "7.47",
|
||||||
|
"currency": "USD",
|
||||||
|
"details": {
|
||||||
|
"subtotal": "7.47"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "This is the payment transaction description.",
|
||||||
|
"note_to_payer": "Contact us for any questions on your order.",
|
||||||
|
"related_resources": [
|
||||||
|
{
|
||||||
|
"sale": {
|
||||||
|
"id": "36C38912MN9658832",
|
||||||
|
"create_time": "2013-02-19T22:01:53Z",
|
||||||
|
"update_time": "2013-02-19T22:01:55Z",
|
||||||
|
"state": "completed",
|
||||||
|
"amount": {
|
||||||
|
"total": "7.47",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
"protection_eligibility": "ELIGIBLE",
|
||||||
|
"protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE",
|
||||||
|
"transaction_fee": {
|
||||||
|
"value": "1.75",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
"parent_payment": "PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832",
|
||||||
|
"rel": "self",
|
||||||
|
"method": "GET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832/refund",
|
||||||
|
"rel": "refund",
|
||||||
|
"method": "POST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href":
|
||||||
|
"https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"rel": "parent_payment",
|
||||||
|
"method": "GET"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"rel": "self",
|
||||||
|
"method": "GET"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return event, o1
|
||||||
|
|
||||||
|
|
||||||
|
def get_test_charge(order: Order):
|
||||||
|
return {
|
||||||
|
"id": "36C38912MN9658832",
|
||||||
|
"create_time": "2013-02-19T22:01:53Z",
|
||||||
|
"update_time": "2013-02-19T22:01:55Z",
|
||||||
|
"state": "completed",
|
||||||
|
"amount": {
|
||||||
|
"total": "7.47",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
"protection_eligibility": "ELIGIBLE",
|
||||||
|
"protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
|
||||||
|
"transaction_fee": {
|
||||||
|
"value": "1.75",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
"parent_payment": "PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832",
|
||||||
|
"rel": "self",
|
||||||
|
"method": "GET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832/refund",
|
||||||
|
"rel": "refund",
|
||||||
|
"method": "POST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"rel": "parent_payment",
|
||||||
|
"method": "GET"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_test_refund(order: Order):
|
||||||
|
return {
|
||||||
|
'refund_from_received_amount': {'value': '13.30', 'currency': 'EUR'},
|
||||||
|
'amount': {'total': '13.37', 'currency': 'EUR'},
|
||||||
|
'sale_id': '1G495778AR8401726',
|
||||||
|
'update_time': '2018-07-24T07:50:07Z',
|
||||||
|
'total_refunded_amount': {'value': '13.37', 'currency': 'EUR'},
|
||||||
|
'refund_reason_code': 'REFUND',
|
||||||
|
'invoice_number': 'Test',
|
||||||
|
'parent_payment': 'PAY-0UB50445HE155450FLNLNMUY',
|
||||||
|
'state': 'completed',
|
||||||
|
'create_time': '2018-07-24T07:50:07Z',
|
||||||
|
'refund_from_transaction_fee': {'value': '0.07', 'currency': 'EUR'},
|
||||||
|
'id': '93M41501U3542574L',
|
||||||
|
'refund_to_payer': {'value': '13.37', 'currency': 'EUR'},
|
||||||
|
'links': [
|
||||||
|
{'method': 'GET', 'rel': 'self',
|
||||||
|
'href': 'https://api.sandbox.paypal.com/v1/payments/refund/93M41501U3542574L'},
|
||||||
|
{'method': 'GET',
|
||||||
|
'rel': 'parent_payment',
|
||||||
|
'href': 'https://api.sandbox.paypal.com/v1/payments/payment/PAY-0UB50445HE155450FLNLNMUY'},
|
||||||
|
{'method': 'GET', 'rel': 'sale',
|
||||||
|
'href': 'https://api.sandbox.paypal.com/v1/payments/sale/1G495778AR8401726'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_webhook_all_good(env, client, monkeypatch):
|
||||||
|
charge = get_test_charge(env[1])
|
||||||
|
monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
|
||||||
|
monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
|
||||||
|
|
||||||
|
client.post('/dummy/dummy/paypal/webhook/', json.dumps(
|
||||||
|
{
|
||||||
|
"id": "WH-2WR32451HC0233532-67976317FL4543714",
|
||||||
|
"create_time": "2014-10-23T17:23:52Z",
|
||||||
|
"resource_type": "sale",
|
||||||
|
"event_type": "PAYMENT.SALE.COMPLETED",
|
||||||
|
"summary": "A successful sale payment was made for $ 0.48 USD",
|
||||||
|
"resource": {
|
||||||
|
"amount": {
|
||||||
|
"total": "-0.01",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
"id": "36C38912MN9658832",
|
||||||
|
"parent_payment": "PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"update_time": "2014-10-31T15:41:51Z",
|
||||||
|
"state": "completed",
|
||||||
|
"create_time": "2014-10-31T15:41:51Z",
|
||||||
|
"links": [],
|
||||||
|
"sale_id": "9T0916710M1105906"
|
||||||
|
},
|
||||||
|
"links": [],
|
||||||
|
"event_version": "1.0"
|
||||||
|
}
|
||||||
|
), content_type='application_json')
|
||||||
|
|
||||||
|
order = env[1]
|
||||||
|
order.refresh_from_db()
|
||||||
|
assert order.status == Order.STATUS_PAID
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_webhook_mark_paid(env, client, monkeypatch):
|
||||||
|
order = env[1]
|
||||||
|
order.status = Order.STATUS_PENDING
|
||||||
|
order.save()
|
||||||
|
with scopes_disabled():
|
||||||
|
order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
|
||||||
|
|
||||||
|
charge = get_test_charge(env[1])
|
||||||
|
monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
|
||||||
|
monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
|
||||||
|
ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
|
||||||
|
|
||||||
|
client.post('/_paypal/webhook/', json.dumps(
|
||||||
|
{
|
||||||
|
"id": "WH-2WR32451HC0233532-67976317FL4543714",
|
||||||
|
"create_time": "2014-10-23T17:23:52Z",
|
||||||
|
"resource_type": "sale",
|
||||||
|
"event_type": "PAYMENT.SALE.COMPLETED",
|
||||||
|
"summary": "A successful sale payment was made for $ 0.48 USD",
|
||||||
|
"resource": {
|
||||||
|
"amount": {
|
||||||
|
"total": "-0.01",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
"id": "36C38912MN9658832",
|
||||||
|
"parent_payment": "PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"update_time": "2014-10-31T15:41:51Z",
|
||||||
|
"state": "completed",
|
||||||
|
"create_time": "2014-10-31T15:41:51Z",
|
||||||
|
"links": [],
|
||||||
|
"sale_id": "9T0916710M1105906"
|
||||||
|
},
|
||||||
|
"links": [],
|
||||||
|
"event_version": "1.0"
|
||||||
|
}
|
||||||
|
), content_type='application_json')
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
assert order.status == Order.STATUS_PAID
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_webhook_refund1(env, client, monkeypatch):
|
||||||
|
order = env[1]
|
||||||
|
charge = get_test_charge(env[1])
|
||||||
|
charge['state'] = 'refunded'
|
||||||
|
refund = get_test_refund(env[1])
|
||||||
|
|
||||||
|
monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
|
||||||
|
monkeypatch.setattr("paypalrestsdk.Refund.find", lambda *args: refund)
|
||||||
|
monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
|
||||||
|
ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
|
||||||
|
|
||||||
|
client.post('/_paypal/webhook/', json.dumps(
|
||||||
|
{
|
||||||
|
# Sample obtained in a sandbox webhook
|
||||||
|
"id": "WH-9K829080KA1622327-31011919VC6498738",
|
||||||
|
"create_time": "2017-01-15T20:15:36Z",
|
||||||
|
"resource_type": "refund",
|
||||||
|
"event_type": "PAYMENT.SALE.REFUNDED",
|
||||||
|
"summary": "A EUR 255.41 EUR sale payment was refunded",
|
||||||
|
"resource": {
|
||||||
|
"amount": {
|
||||||
|
"total": "255.41",
|
||||||
|
"currency": "EUR"
|
||||||
|
},
|
||||||
|
"id": "75S46770PP192124D",
|
||||||
|
"parent_payment": "PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"update_time": "2017-01-15T20:15:06Z",
|
||||||
|
"create_time": "2017-01-15T20:14:29Z",
|
||||||
|
"state": "completed",
|
||||||
|
"links": [],
|
||||||
|
"refund_to_payer": {
|
||||||
|
"value": "255.41",
|
||||||
|
"currency": "EUR"
|
||||||
|
},
|
||||||
|
"invoice_number": "",
|
||||||
|
"refund_reason_code": "REFUND",
|
||||||
|
"sale_id": "9T0916710M1105906"
|
||||||
|
},
|
||||||
|
"links": [],
|
||||||
|
"event_version": "1.0"
|
||||||
|
}
|
||||||
|
), content_type='application_json')
|
||||||
|
|
||||||
|
order = env[1]
|
||||||
|
order.refresh_from_db()
|
||||||
|
assert order.status == Order.STATUS_PAID
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
r = order.refunds.first()
|
||||||
|
assert r.provider == 'paypal'
|
||||||
|
assert r.amount == order.total
|
||||||
|
assert r.payment == order.payments.first()
|
||||||
|
assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
|
||||||
|
assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_webhook_refund2(env, client, monkeypatch):
|
||||||
|
order = env[1]
|
||||||
|
charge = get_test_charge(env[1])
|
||||||
|
charge['state'] = 'refunded'
|
||||||
|
refund = get_test_refund(env[1])
|
||||||
|
|
||||||
|
monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
|
||||||
|
monkeypatch.setattr("paypalrestsdk.Refund.find", lambda *args: refund)
|
||||||
|
monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
|
||||||
|
ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
|
||||||
|
|
||||||
|
client.post('/_paypal/webhook/', json.dumps(
|
||||||
|
{
|
||||||
|
# Sample obtained in the webhook simulator
|
||||||
|
"id": "WH-2N242548W9943490U-1JU23391CS4765624",
|
||||||
|
"create_time": "2014-10-31T15:42:24Z",
|
||||||
|
"resource_type": "refund",
|
||||||
|
"event_type": "PAYMENT.SALE.REFUNDED",
|
||||||
|
"summary": "A 0.01 USD sale payment was refunded",
|
||||||
|
"resource": {
|
||||||
|
"amount": {
|
||||||
|
"total": "-0.01",
|
||||||
|
"currency": "USD"
|
||||||
|
},
|
||||||
|
"id": "36C38912MN9658832",
|
||||||
|
"parent_payment": "PAY-5YK922393D847794YKER7MUI",
|
||||||
|
"update_time": "2014-10-31T15:41:51Z",
|
||||||
|
"state": "completed",
|
||||||
|
"create_time": "2014-10-31T15:41:51Z",
|
||||||
|
"links": [],
|
||||||
|
"sale_id": "9T0916710M1105906"
|
||||||
|
},
|
||||||
|
"links": [],
|
||||||
|
"event_version": "1.0"
|
||||||
|
}
|
||||||
|
), content_type='application_json')
|
||||||
|
|
||||||
|
order = env[1]
|
||||||
|
order.refresh_from_db()
|
||||||
|
assert order.status == Order.STATUS_PAID
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
r = order.refunds.first()
|
||||||
|
assert r.provider == 'paypal'
|
||||||
|
assert r.amount == order.total
|
||||||
|
assert r.payment == order.payments.first()
|
||||||
|
assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
|
||||||
|
assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
|
||||||
@@ -32,7 +32,7 @@ from paypalhttp.http_response import Result
|
|||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
|
Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
|
||||||
)
|
)
|
||||||
from pretix.plugins.paypal2.models import ReferencedPayPalObject
|
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -744,35 +744,6 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
|
|||||||
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
|
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
|
||||||
self.assertIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
|
self.assertIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
|
||||||
|
|
||||||
def test_hidden_if_item_available_variation_unavailable_by_time(self):
|
|
||||||
with scopes_disabled():
|
|
||||||
q = Quota.objects.create(event=self.event, name='Early-bird', size=10)
|
|
||||||
q2 = Quota.objects.create(event=self.event, name='Late-bird', size=10)
|
|
||||||
item_with_vars = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=12)
|
|
||||||
v = item_with_vars.variations.create(
|
|
||||||
value='Regular', active=True,
|
|
||||||
)
|
|
||||||
item2 = Item.objects.create(event=self.event, name='Late-bird ticket', default_price=12,
|
|
||||||
hidden_if_item_available=item_with_vars)
|
|
||||||
q.items.add(item_with_vars)
|
|
||||||
q.variations.add(v)
|
|
||||||
q2.items.add(item2)
|
|
||||||
|
|
||||||
self.event.settings.hide_sold_out = True
|
|
||||||
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
|
|
||||||
self.assertIn("Early-bird", doc.select("section:nth-of-type(1)")[0].text)
|
|
||||||
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
|
|
||||||
self.assertNotIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
|
|
||||||
|
|
||||||
item_with_vars.available_until = now() - datetime.timedelta(days=3)
|
|
||||||
item_with_vars.available_until_mode = "hide"
|
|
||||||
item_with_vars.save()
|
|
||||||
|
|
||||||
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
|
|
||||||
self.assertNotIn("Early-bird", doc.select("section:nth-of-type(1)")[0].text)
|
|
||||||
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
|
|
||||||
self.assertIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
|
|
||||||
|
|
||||||
def test_bundle_sold_out(self):
|
def test_bundle_sold_out(self):
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user