mirror of
https://github.com/pretix/pretix.git
synced 2026-10-03 19:34:41 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e211d41c9a | ||
|
|
7d0e38b97a | ||
|
|
b6857967a5 | ||
|
|
3a1db55e8b | ||
|
|
57da5cbae2 |
@@ -24,7 +24,7 @@ jobs:
|
|||||||
name: Packaging
|
name: Packaging
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.13"]
|
python-version: ["3.11"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ jobs:
|
|||||||
name: Check gettext syntax
|
name: Check gettext syntax
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
@@ -49,10 +49,10 @@ jobs:
|
|||||||
name: Spellcheck
|
name: Spellcheck
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
@@ -44,10 +44,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
@@ -64,10 +64,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install licenseheaders
|
run: pip3 install licenseheaders
|
||||||
- name: Run licenseheaders
|
- name: Run licenseheaders
|
||||||
|
|||||||
@@ -23,15 +23,13 @@ jobs:
|
|||||||
name: Tests
|
name: Tests
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.11", "3.13", "3.14"]
|
python-version: ["3.10", "3.11", "3.13"]
|
||||||
database: [sqlite, postgres]
|
database: [sqlite, postgres]
|
||||||
exclude:
|
exclude:
|
||||||
- database: sqlite
|
- database: sqlite
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
- database: sqlite
|
- database: sqlite
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- database: sqlite
|
|
||||||
python-version: "3.12"
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
@@ -83,4 +81,4 @@ jobs:
|
|||||||
file: src/coverage.xml
|
file: src/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
if: matrix.database == 'postgres' && matrix.python-version == '3.13'
|
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||||
|
|||||||
+6
-7
@@ -3,7 +3,7 @@ name = "pretix"
|
|||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Reinventing presales, one ticket at a time"
|
description = "Reinventing presales, one ticket at a time"
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.10"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
keywords = ["tickets", "web", "shop", "ecommerce"]
|
keywords = ["tickets", "web", "shop", "ecommerce"]
|
||||||
authors = [
|
authors = [
|
||||||
@@ -19,11 +19,10 @@ classifiers = [
|
|||||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Framework :: Django :: 4.2",
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Programming Language :: Python :: 3.14",
|
|
||||||
"Framework :: Django :: 5.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -37,7 +36,7 @@ dependencies = [
|
|||||||
"css-inline==0.20.*",
|
"css-inline==0.20.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dnspython==2.*",
|
"dnspython==2.*",
|
||||||
"Django[argon2]==5.2.*",
|
"Django[argon2]==4.2.*,>=4.2.26",
|
||||||
"django-bootstrap3==26.1",
|
"django-bootstrap3==26.1",
|
||||||
"django-compressor==4.6.0",
|
"django-compressor==4.6.0",
|
||||||
"django-countries==8.2.*",
|
"django-countries==8.2.*",
|
||||||
@@ -60,7 +59,7 @@ dependencies = [
|
|||||||
"dnspython==2.8.*",
|
"dnspython==2.8.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==5.*",
|
"geoip2==5.*",
|
||||||
"importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.6.*",
|
"kombu==5.6.*",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import pycountry
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.gis.geoip2 import GeoIP2
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.core.validators import (
|
from django.core.validators import (
|
||||||
@@ -101,7 +102,6 @@ from pretix.helpers.countries import (
|
|||||||
from pretix.helpers.escapejson import escapejson_attr
|
from pretix.helpers.escapejson import escapejson_attr
|
||||||
from pretix.helpers.http import get_client_ip
|
from pretix.helpers.http import get_client_ip
|
||||||
from pretix.helpers.i18n import get_format_without_seconds
|
from pretix.helpers.i18n import get_format_without_seconds
|
||||||
from pretix.helpers.security import get_geoip
|
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -393,7 +393,7 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
|||||||
|
|
||||||
def guess_country_from_request(request, event):
|
def guess_country_from_request(request, event):
|
||||||
if settings.HAS_GEOIP:
|
if settings.HAS_GEOIP:
|
||||||
g = get_geoip()
|
g = GeoIP2()
|
||||||
try:
|
try:
|
||||||
res = g.country(get_client_ip(request))
|
res = g.country(get_client_ip(request))
|
||||||
if res['country_code'] and len(res['country_code']) == 2:
|
if res['country_code'] and len(res['country_code']) == 2:
|
||||||
|
|||||||
@@ -36,9 +36,8 @@ from django.core.management.commands.makemigrations import Command as Parent
|
|||||||
|
|
||||||
from ._migrations import monkeypatch_migrations
|
from ._migrations import monkeypatch_migrations
|
||||||
|
|
||||||
|
monkeypatch_migrations()
|
||||||
|
|
||||||
|
|
||||||
class Command(Parent):
|
class Command(Parent):
|
||||||
|
pass
|
||||||
def handle(self, *args, **kwargs):
|
|
||||||
monkeypatch_migrations()
|
|
||||||
return super().handle(*args, **kwargs)
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Command(BaseCommand):
|
|||||||
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
|
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
|
||||||
return
|
return
|
||||||
|
|
||||||
for receiver in periodic_task._live_receivers(self)[0]:
|
for receiver in periodic_task._live_receivers(self):
|
||||||
name = f'{receiver.__module__}.{receiver.__name__}'
|
name = f'{receiver.__module__}.{receiver.__name__}'
|
||||||
if options['list_tasks']:
|
if options['list_tasks']:
|
||||||
print(name)
|
print(name)
|
||||||
|
|||||||
@@ -41,20 +41,16 @@ class Migration(migrations.Migration):
|
|||||||
name='datetime',
|
name='datetime',
|
||||||
field=models.DateTimeField(),
|
field=models.DateTimeField(),
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AlterIndexTogether(
|
||||||
'logentry',
|
name='logentry',
|
||||||
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"),
|
index_together={('datetime', 'id')},
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AlterIndexTogether(
|
||||||
'order',
|
name='order',
|
||||||
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
|
index_together={('datetime', 'id'), ('last_modified', 'id')},
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.AlterIndexTogether(
|
||||||
'order',
|
name='transaction',
|
||||||
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
|
index_together={('datetime', 'id')},
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
'transaction',
|
|
||||||
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b20405_idx"),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -61,10 +61,7 @@ class Migration(migrations.Migration):
|
|||||||
options={
|
options={
|
||||||
'ordering': ('identifier', 'type', 'organizer'),
|
'ordering': ('identifier', 'type', 'organizer'),
|
||||||
'unique_together': {('identifier', 'type', 'organizer')},
|
'unique_together': {('identifier', 'type', 'organizer')},
|
||||||
'indexes': [
|
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
|
||||||
models.Index(fields=('identifier', 'type', 'organizer'), name='reusable_medium_organizer_index'),
|
|
||||||
models.Index(fields=('updated', 'id'), name="pretixbase__updated_093277_idx")
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,6 +9,31 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="logentry",
|
||||||
|
new_name="pretixbase__datetim_b1fe5a_idx",
|
||||||
|
old_fields=("datetime", "id"),
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="order",
|
||||||
|
new_name="pretixbase__datetim_66aff0_idx",
|
||||||
|
old_fields=("datetime", "id"),
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="order",
|
||||||
|
new_name="pretixbase__last_mo_4ebf8b_idx",
|
||||||
|
old_fields=("last_modified", "id"),
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="reusablemedium",
|
||||||
|
new_name="pretixbase__updated_093277_idx",
|
||||||
|
old_fields=("updated", "id"),
|
||||||
|
),
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name="transaction",
|
||||||
|
new_name="pretixbase__datetim_b20405_idx",
|
||||||
|
old_fields=("datetime", "id"),
|
||||||
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="attendeeprofile",
|
model_name="attendeeprofile",
|
||||||
name="id",
|
name="id",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Generated by Django 4.2.10 on 2024-04-02 15:16
|
# Generated by Django 4.2.10 on 2024-04-02 15:16
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveIndex(
|
migrations.AlterIndexTogether(
|
||||||
"reusablemedium",
|
name="reusablemedium",
|
||||||
'reusable_medium_organizer_index',
|
index_together=set(),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class LogEntry(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('-datetime', '-id')
|
ordering = ('-datetime', '-id')
|
||||||
indexes = [models.Index(fields=["datetime", "id"], name="pretixbase__datetim_b1fe5a_idx")]
|
indexes = [models.Index(fields=["datetime", "id"])]
|
||||||
|
|
||||||
def display(self):
|
def display(self):
|
||||||
from pretix.base.logentrytype_registry import log_entry_types
|
from pretix.base.logentrytype_registry import log_entry_types
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class ReusableMedium(LoggedModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("identifier", "type", "organizer"),)
|
unique_together = (("identifier", "type", "organizer"),)
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=("updated", "id"), name="pretixbase__updated_093277_idx"),
|
models.Index(fields=("updated", "id")),
|
||||||
]
|
]
|
||||||
ordering = "identifier", "type", "organizer"
|
ordering = "identifier", "type", "organizer"
|
||||||
|
|
||||||
|
|||||||
@@ -336,8 +336,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
verbose_name_plural = _("Orders")
|
verbose_name_plural = _("Orders")
|
||||||
ordering = ("-datetime", "-pk")
|
ordering = ("-datetime", "-pk")
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
|
models.Index(fields=["datetime", "id"]),
|
||||||
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
|
models.Index(fields=["last_modified", "id"]),
|
||||||
]
|
]
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
|
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
|
||||||
@@ -3080,7 +3080,7 @@ class Transaction(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = 'datetime', 'pk'
|
ordering = 'datetime', 'pk'
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['datetime', 'id'], name="pretixbase__datetim_b20405_idx")
|
models.Index(fields=['datetime', 'id'])
|
||||||
]
|
]
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -4148,14 +4148,6 @@ def validate_event_settings(event, settings_dict):
|
|||||||
)
|
)
|
||||||
]}
|
]}
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
settings_dict.get('invoice_address_from_vat_id') and
|
|
||||||
settings_dict.get('invoice_address_from_country') and
|
|
||||||
settings_dict.get('invoice_address_from_country') not in VAT_ID_COUNTRIES
|
|
||||||
):
|
|
||||||
raise ValidationError({
|
|
||||||
'invoice_address_from_vat_id': _('VAT-ID is not supported for "{}".').format(settings_dict.get('invoice_address_from_country'))
|
|
||||||
})
|
|
||||||
|
|
||||||
payment_term_last = settings_dict.get('payment_term_last')
|
payment_term_last = settings_dict.get('payment_term_last')
|
||||||
if payment_term_last and event.presale_end:
|
if payment_term_last and event.presale_end:
|
||||||
|
|||||||
+19
-60
@@ -32,7 +32,6 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import logging
|
|
||||||
import warnings
|
import warnings
|
||||||
from typing import Any, Callable, Generic, List, Tuple, TypeVar
|
from typing import Any, Callable, Generic, List, Tuple, TypeVar
|
||||||
|
|
||||||
@@ -49,8 +48,6 @@ from .plugins import (
|
|||||||
PLUGIN_LEVEL_ORGANIZER,
|
PLUGIN_LEVEL_ORGANIZER,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app_cache = {}
|
app_cache = {}
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
@@ -63,25 +60,23 @@ def _populate_app_cache():
|
|||||||
|
|
||||||
def get_defining_app(o):
|
def get_defining_app(o):
|
||||||
# If sentry packed this in a wrapper, unpack that
|
# If sentry packed this in a wrapper, unpack that
|
||||||
module = getattr(o, "__module__", None)
|
if "sentry" in o.__module__:
|
||||||
if module and "sentry" in module:
|
|
||||||
o = o.__wrapped__
|
o = o.__wrapped__
|
||||||
|
|
||||||
if hasattr(o, "__mocked_app"):
|
if hasattr(o, "__mocked_app"):
|
||||||
return o.__mocked_app
|
return o.__mocked_app
|
||||||
|
|
||||||
# Find the Django application this belongs to
|
# Find the Django application this belongs to
|
||||||
searchpath = module or getattr(o.__class__, "__module__", None) or ""
|
searchpath = o.__module__
|
||||||
|
|
||||||
# Core modules are always active
|
# Core modules are always active
|
||||||
if searchpath and any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
|
if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
|
||||||
return 'CORE'
|
return 'CORE'
|
||||||
|
|
||||||
if not app_cache:
|
if not app_cache:
|
||||||
_populate_app_cache()
|
_populate_app_cache()
|
||||||
|
|
||||||
app = None
|
while True:
|
||||||
while searchpath:
|
|
||||||
app = app_cache.get(searchpath)
|
app = app_cache.get(searchpath)
|
||||||
if "." not in searchpath or app:
|
if "." not in searchpath or app:
|
||||||
break
|
break
|
||||||
@@ -162,7 +157,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
|||||||
if not app_cache:
|
if not app_cache:
|
||||||
_populate_app_cache()
|
_populate_app_cache()
|
||||||
|
|
||||||
for receiver in self._live_receivers(sender)[0]:
|
for receiver in self._sorted_receivers(sender):
|
||||||
if self._is_receiver_active(sender, receiver):
|
if self._is_receiver_active(sender, receiver):
|
||||||
response = receiver(signal=self, sender=sender, **named)
|
response = receiver(signal=self, sender=sender, **named)
|
||||||
responses.append((receiver, response))
|
responses.append((receiver, response))
|
||||||
@@ -184,7 +179,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
|||||||
if not app_cache:
|
if not app_cache:
|
||||||
_populate_app_cache()
|
_populate_app_cache()
|
||||||
|
|
||||||
for receiver in self._live_receivers(sender)[0]:
|
for receiver in self._sorted_receivers(sender):
|
||||||
if self._is_receiver_active(sender, receiver):
|
if self._is_receiver_active(sender, receiver):
|
||||||
named[chain_kwarg_name] = response
|
named[chain_kwarg_name] = response
|
||||||
response = receiver(signal=self, sender=sender, **named)
|
response = receiver(signal=self, sender=sender, **named)
|
||||||
@@ -209,7 +204,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
|||||||
if not app_cache:
|
if not app_cache:
|
||||||
_populate_app_cache()
|
_populate_app_cache()
|
||||||
|
|
||||||
for receiver in self._live_receivers(sender)[0]:
|
for receiver in self._sorted_receivers(sender):
|
||||||
if self._is_receiver_active(sender, receiver):
|
if self._is_receiver_active(sender, receiver):
|
||||||
try:
|
try:
|
||||||
response = receiver(signal=self, sender=sender, **named)
|
response = receiver(signal=self, sender=sender, **named)
|
||||||
@@ -219,35 +214,17 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
|||||||
responses.append((receiver, response))
|
responses.append((receiver, response))
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
def asend(self, sender: T, **named):
|
def _sorted_receivers(self, sender):
|
||||||
raise NotImplementedError() # NOQA
|
orig_list = self._live_receivers(sender)
|
||||||
|
|
||||||
def asend_robust(self, sender: T, **named):
|
|
||||||
raise NotImplementedError() # NOQA
|
|
||||||
|
|
||||||
def _live_receivers(self, sender):
|
|
||||||
orig_list, orig_async_list = super()._live_receivers(sender)
|
|
||||||
|
|
||||||
if orig_async_list:
|
|
||||||
logger.error('Async receivers are not supported.')
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _getattr_fallback_to_class(obj, key):
|
|
||||||
return getattr(obj, key, getattr(obj.__class__, key))
|
|
||||||
|
|
||||||
def _is_core_module(receiver):
|
|
||||||
m = _getattr_fallback_to_class(receiver, "__module__")
|
|
||||||
return any(m.startswith(c) for c in settings.CORE_MODULES)
|
|
||||||
|
|
||||||
sorted_list = sorted(
|
sorted_list = sorted(
|
||||||
orig_list,
|
orig_list,
|
||||||
key=lambda receiver: (
|
key=lambda receiver: (
|
||||||
0 if _is_core_module(receiver) else 1,
|
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
|
||||||
_getattr_fallback_to_class(receiver, "__module__"),
|
receiver.__module__,
|
||||||
_getattr_fallback_to_class(receiver, "__name__"),
|
receiver.__name__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return sorted_list, []
|
return sorted_list
|
||||||
|
|
||||||
|
|
||||||
class EventPluginSignal(PluginSignal[Event]):
|
class EventPluginSignal(PluginSignal[Event]):
|
||||||
@@ -323,41 +300,23 @@ class GlobalSignal(django.dispatch.Signal):
|
|||||||
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
|
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
for receiver in self._live_receivers(sender)[0]:
|
for receiver in self._live_receivers(sender):
|
||||||
named[chain_kwarg_name] = response
|
named[chain_kwarg_name] = response
|
||||||
response = receiver(signal=self, sender=sender, **named)
|
response = receiver(signal=self, sender=sender, **named)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def asend(self, sender: T, **named):
|
|
||||||
raise NotImplementedError() # NOQA
|
|
||||||
|
|
||||||
def asend_robust(self, sender: T, **named):
|
|
||||||
raise NotImplementedError() # NOQA
|
|
||||||
|
|
||||||
def _live_receivers(self, sender):
|
def _live_receivers(self, sender):
|
||||||
# Ensure consistent sorting of receivers
|
# Ensure consistent sorting of receivers
|
||||||
orig_list, orig_async_list = super()._live_receivers(sender)
|
orig_list = super()._live_receivers(sender)
|
||||||
|
|
||||||
if orig_async_list:
|
|
||||||
logger.error('Async receivers are not supported.')
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _getattr_fallback_to_class(obj, key):
|
|
||||||
return getattr(obj, key, getattr(obj.__class__, key))
|
|
||||||
|
|
||||||
def _is_core_module(receiver):
|
|
||||||
m = _getattr_fallback_to_class(receiver, "__module__")
|
|
||||||
return any(m.startswith(c) for c in settings.CORE_MODULES)
|
|
||||||
|
|
||||||
sorted_list = sorted(
|
sorted_list = sorted(
|
||||||
orig_list,
|
orig_list,
|
||||||
key=lambda receiver: (
|
key=lambda receiver: (
|
||||||
0 if _is_core_module(receiver) else 1,
|
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
|
||||||
_getattr_fallback_to_class(receiver, "__module__"),
|
receiver.__module__,
|
||||||
_getattr_fallback_to_class(receiver, "__name__"),
|
receiver.__name__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return sorted_list, []
|
return sorted_list
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedSignal(GlobalSignal):
|
class DeprecatedSignal(GlobalSignal):
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ from pretix.base.forms import (
|
|||||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||||
from pretix.base.models.organizer import TeamQuerySet
|
from pretix.base.models.organizer import TeamQuerySet
|
||||||
from pretix.base.models.tax import TAX_CODE_LISTS, VAT_ID_COUNTRIES
|
from pretix.base.models.tax import TAX_CODE_LISTS
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
from pretix.base.services.placeholders import FormPlaceholderMixin
|
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||||
from pretix.base.settings import (
|
from pretix.base.settings import (
|
||||||
@@ -531,13 +531,6 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
|
|
||||||
class EventSettingsValidationMixin:
|
class EventSettingsValidationMixin:
|
||||||
|
|
||||||
def clean_invoice_address_from_vat_id(self):
|
|
||||||
value = self.cleaned_data.get('invoice_address_from_vat_id')
|
|
||||||
country = self.cleaned_data.get('invoice_address_from_country')
|
|
||||||
if value and country and country not in VAT_ID_COUNTRIES:
|
|
||||||
return None
|
|
||||||
return value
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
settings_dict = self.obj.settings.freeze()
|
settings_dict = self.obj.settings.freeze()
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ def get_global_navigation(request):
|
|||||||
'icon': 'dashboard',
|
'icon': 'dashboard',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if request.user.is_in_any_teams or request.user.is_staff:
|
if request.user.is_in_any_teams:
|
||||||
nav += [
|
nav += [
|
||||||
{
|
{
|
||||||
'label': _('Events'),
|
'label': _('Events'),
|
||||||
|
|||||||
@@ -8,7 +8,45 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
{% bootstrap_field form.name layout='horizontal' %}
|
{% bootstrap_field form.name layout='horizontal' %}
|
||||||
{% bootstrap_field form.devicetype layout='horizontal' %}
|
|
||||||
|
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}">
|
||||||
|
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div>
|
||||||
|
<div class="big-radio radio">
|
||||||
|
<label>
|
||||||
|
<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>
|
||||||
|
<div class="help-block">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="big-radio radio">
|
||||||
|
<label>
|
||||||
|
<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>
|
||||||
|
<div class="help-block">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if form.devicetype.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{% for error in form.devicetype.errors %}
|
||||||
|
<p>{{ error|escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group submit-group">
|
<div class="form-group submit-group">
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
{% trans "Continue" %}
|
{% trans "Continue" %}
|
||||||
|
|||||||
@@ -28,11 +28,6 @@
|
|||||||
{% trans "iOS (iTunes)" %}
|
{% trans "iOS (iTunes)" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="https://m.google.com/authenticator">
|
|
||||||
{% trans "Blackberry (Link via Google)" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -641,7 +641,7 @@ def user_index(request):
|
|||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'widgets': rearrange(widgets),
|
'widgets': rearrange(widgets),
|
||||||
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists() or request.user.is_staff,
|
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists(),
|
||||||
'upcoming': widgets_for_event_qs(
|
'upcoming': widgets_for_event_qs(
|
||||||
request,
|
request,
|
||||||
annotated_event_query(request, lazy=True).filter(
|
annotated_event_query(request, lazy=True).filter(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import time
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import login as auth_login
|
from django.contrib.auth import login as auth_login
|
||||||
from django.contrib.gis import geoip2
|
from django.contrib.gis.geoip2 import GeoIP2
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -63,20 +63,14 @@ def get_user_agent_hash(request):
|
|||||||
_geoip = None
|
_geoip = None
|
||||||
|
|
||||||
|
|
||||||
def get_geoip() -> geoip2.GeoIP2:
|
def _get_country(request):
|
||||||
# See https://code.djangoproject.com/ticket/36988#ticket
|
|
||||||
global _geoip
|
global _geoip
|
||||||
|
|
||||||
geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
|
|
||||||
|
|
||||||
if not _geoip:
|
if not _geoip:
|
||||||
_geoip = geoip2.GeoIP2()
|
_geoip = GeoIP2()
|
||||||
return _geoip
|
|
||||||
|
|
||||||
|
|
||||||
def _get_country(request):
|
|
||||||
try:
|
try:
|
||||||
res = get_geoip().country(get_client_ip(request))
|
res = _geoip.country(get_client_ip(request))
|
||||||
except AddressNotFoundError:
|
except AddressNotFoundError:
|
||||||
return None
|
return None
|
||||||
return res['country_code']
|
return res['country_code']
|
||||||
|
|||||||
@@ -32,8 +32,11 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.encoding import force_str
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@@ -165,6 +168,46 @@ class QuestionsForm(BaseQuestionsForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddOnRadioSelect(forms.RadioSelect):
|
||||||
|
option_template_name = 'pretixpresale/forms/addon_choice_option.html'
|
||||||
|
|
||||||
|
def optgroups(self, name, value, attrs=None):
|
||||||
|
attrs = attrs or {}
|
||||||
|
groups = []
|
||||||
|
has_selected = False
|
||||||
|
for index, (option_value, option_label, option_desc) in enumerate(chain(self.choices)):
|
||||||
|
if option_value is None:
|
||||||
|
option_value = ''
|
||||||
|
if isinstance(option_label, (list, tuple)):
|
||||||
|
raise TypeError('Choice groups are not supported here')
|
||||||
|
group_name = None
|
||||||
|
subgroup = []
|
||||||
|
groups.append((group_name, subgroup, index))
|
||||||
|
|
||||||
|
selected = (
|
||||||
|
force_str(option_value) in value and
|
||||||
|
(has_selected is False or self.allow_multiple_selected)
|
||||||
|
)
|
||||||
|
if selected is True and has_selected is False:
|
||||||
|
has_selected = True
|
||||||
|
attrs['description'] = option_desc
|
||||||
|
subgroup.append(self.create_option(
|
||||||
|
name, option_value, option_label, selected, index,
|
||||||
|
subindex=None, attrs=attrs,
|
||||||
|
))
|
||||||
|
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
class AddOnVariationField(forms.ChoiceField):
|
||||||
|
def valid_value(self, value):
|
||||||
|
text_value = force_str(value)
|
||||||
|
for k, v, d in self.choices:
|
||||||
|
if value == k or text_value == force_str(k):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class MembershipForm(forms.Form):
|
class MembershipForm(forms.Form):
|
||||||
required_css_class = 'required'
|
required_css_class = 'required'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{% load rich_text %}
|
||||||
|
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% include "django/forms/widgets/input.html" %} {{ widget.label }}</label> {% if widget.attrs.description %}<span class="fa fa-info-circle toggle-variation-description" aria-hidden="true"></span>
|
||||||
|
<div class="variation-description addon-variation-description">{{ widget.attrs.description|rich_text }}</div>{% endif %}
|
||||||
@@ -576,7 +576,7 @@ def filter_subevents_with_plugins(subevents, sales_channel=None):
|
|||||||
if not app_cache:
|
if not app_cache:
|
||||||
_populate_app_cache()
|
_populate_app_cache()
|
||||||
|
|
||||||
for receiver in filter_subevents._live_receivers(None)[0]:
|
for receiver in filter_subevents._live_receivers(None):
|
||||||
app = get_defining_app(receiver)
|
app = get_defining_app(receiver)
|
||||||
event_state = {}
|
event_state = {}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ from django.views.decorators.cache import cache_page
|
|||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.views.decorators.http import condition
|
from django.views.decorators.http import condition
|
||||||
from django.views.i18n import (
|
from django.views.i18n import (
|
||||||
JavaScriptCatalog, builtin_template_path, get_formats,
|
JavaScriptCatalog, get_formats, js_catalog_template,
|
||||||
)
|
)
|
||||||
from lxml import html
|
from lxml import html
|
||||||
|
|
||||||
@@ -170,8 +170,7 @@ def generate_widget_js(version, lang):
|
|||||||
'September', 'October', 'November', 'December'
|
'September', 'October', 'November', 'December'
|
||||||
)
|
)
|
||||||
catalog = dict((k, v) for k, v in catalog.items() if k.startswith('widget\u0004') or k in str_wl)
|
catalog = dict((k, v) for k, v in catalog.items() if k.startswith('widget\u0004') or k in str_wl)
|
||||||
with builtin_template_path("i18n_catalog.js").open(encoding="utf-8") as fh:
|
template = Engine().from_string(js_catalog_template)
|
||||||
template = Engine().from_string(fh.read())
|
|
||||||
context = Context({
|
context = Context({
|
||||||
'catalog_str': indent(json.dumps(
|
'catalog_str': indent(json.dumps(
|
||||||
catalog, sort_keys=True, indent=2)) if catalog else None,
|
catalog, sort_keys=True, indent=2)) if catalog else None,
|
||||||
|
|||||||
@@ -534,7 +534,6 @@ X_FRAME_OPTIONS = 'DENY'
|
|||||||
|
|
||||||
# URL settings
|
# URL settings
|
||||||
ROOT_URLCONF = 'pretix.multidomain.maindomain_urlconf'
|
ROOT_URLCONF = 'pretix.multidomain.maindomain_urlconf'
|
||||||
FORMS_URLFIELD_ASSUME_HTTPS = True # transitional for django 6.0
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'pretix.wsgi.application'
|
WSGI_APPLICATION = 'pretix.wsgi.application'
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -23,7 +23,9 @@ filterwarnings =
|
|||||||
error
|
error
|
||||||
ignore:.*invalid escape sequence.*:
|
ignore:.*invalid escape sequence.*:
|
||||||
ignore:The 'warn' method is deprecated:DeprecationWarning
|
ignore:The 'warn' method is deprecated:DeprecationWarning
|
||||||
ignore::django.utils.deprecation.RemovedInDjango60Warning:
|
ignore::django.utils.deprecation.RemovedInDjango51Warning:django.core.files.storage
|
||||||
|
ignore:.*index_together.*:django.utils.deprecation.RemovedInDjango51Warning:
|
||||||
|
ignore:.*get_storage_class.*:django.utils.deprecation.RemovedInDjango51Warning:compressor
|
||||||
ignore:.*This signal will soon be only available for plugins that declare to be organizer-level.*:DeprecationWarning:
|
ignore:.*This signal will soon be only available for plugins that declare to be organizer-level.*:DeprecationWarning:
|
||||||
ignore::DeprecationWarning:mt940
|
ignore::DeprecationWarning:mt940
|
||||||
ignore::DeprecationWarning:cbor2
|
ignore::DeprecationWarning:cbor2
|
||||||
|
|||||||
@@ -19,13 +19,14 @@
|
|||||||
# 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/>.
|
||||||
#
|
#
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
from pytz import UTC
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Device, Event, Item, Organizer, Quota, SeatingPlan,
|
Device, Event, Item, Organizer, Quota, SeatingPlan,
|
||||||
@@ -52,7 +53,7 @@ def organizer():
|
|||||||
def event(organizer):
|
def event(organizer):
|
||||||
e = Event.objects.create(
|
e = Event.objects.create(
|
||||||
organizer=organizer, name='Dummy', slug='dummy',
|
organizer=organizer, name='Dummy', slug='dummy',
|
||||||
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=timezone.utc),
|
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC),
|
||||||
presale_end=now() + timedelta(days=300),
|
presale_end=now() + timedelta(days=300),
|
||||||
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf',
|
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf',
|
||||||
is_public=True, live=True
|
is_public=True, live=True
|
||||||
@@ -108,8 +109,8 @@ def customer(event, membership_type):
|
|||||||
def membership(event, membership_type, customer):
|
def membership(event, membership_type, customer):
|
||||||
return customer.memberships.create(
|
return customer.memberships.create(
|
||||||
membership_type=membership_type,
|
membership_type=membership_type,
|
||||||
date_start=datetime(2017, 1, 1, 0, 0, tzinfo=timezone.utc),
|
date_start=datetime(2017, 1, 1, 0, 0, tzinfo=UTC),
|
||||||
date_end=datetime(2099, 1, 1, 0, 0, tzinfo=timezone.utc),
|
date_end=datetime(2099, 1, 1, 0, 0, tzinfo=UTC),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user