Compare commits

...

6 Commits

Author SHA1 Message Date
Phin Wolkwitz
9a43c9c437 Add location tests 2026-04-14 15:15:37 +02:00
Phin Wolkwitz
19fb5e7600 Fix migration, update API documentation 2026-04-14 14:50:33 +02:00
Phin Wolkwitz
90d4fa7c9a Add location for program time slots 2026-04-14 14:21:10 +02:00
Phin Wolkwitz
d1171b9cf0 [wip] Add location 2026-04-14 14:21:10 +02:00
Richard Schreiber
efd887b439 API: fix PDF-download name (Z#23231496) 2026-04-14 14:13:14 +02:00
pajowu
8690d65e99 Do not show payment text of canceled and failed payments on invoice (Z#23231070) (#6075) 2026-04-14 13:02:12 +02:00
10 changed files with 130 additions and 13 deletions

View File

@@ -16,6 +16,7 @@ Field Type Description
id integer Internal ID of the program time
start datetime The start date time for this program time slot.
end datetime The end date time for this program time slot.
location multi-lingual string The program time slot's location (or ``null``)
===================================== ========================== =======================================================
.. versionchanged:: TODO
@@ -54,17 +55,20 @@ Endpoints
{
"id": 2,
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
"end": "2025-08-15T00:00:00Z",
"location": null
},
{
"id": 3,
"start": "2025-08-12T22:00:00Z",
"end": "2025-08-13T22:00:00Z"
"end": "2025-08-13T22:00:00Z",
"location": null
},
{
"id": 14,
"start": "2025-08-15T22:00:00Z",
"end": "2025-08-17T22:00:00Z"
"end": "2025-08-17T22:00:00Z",
"location": null
}
]
}
@@ -99,7 +103,8 @@ Endpoints
{
"id": 1,
"start": "2025-08-15T22:00:00Z",
"end": "2025-10-27T23:00:00Z"
"end": "2025-10-27T23:00:00Z",
"location": null
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -125,7 +130,8 @@ Endpoints
{
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
"end": "2025-08-15T22:00:00Z",
"location": null
}
**Example response**:
@@ -139,7 +145,8 @@ Endpoints
{
"id": 17,
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
"end": "2025-08-15T22:00:00Z",
"location": null
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for

View File

@@ -191,7 +191,7 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('start', 'end')
fields = ('start', 'end', 'location')
class ItemBundleSerializer(serializers.ModelSerializer):
@@ -222,7 +222,7 @@ class ItemBundleSerializer(serializers.ModelSerializer):
class ItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('id', 'start', 'end')
fields = ('id', 'start', 'end', 'location')
def validate(self, data):
data = super().validate(data)

View File

@@ -1308,7 +1308,7 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
ftype, ignored = mimetypes.guess_type(answer.file.name)
return FileResponse(
answer.file,
filename='{}-{}-{}-{}"'.format(
filename='{}-{}-{}-{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
@@ -2000,7 +2000,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
return FileResponse(
invoice.file.file,
filename='{}.pdf"'.format(invoice.number),
filename='{}.pdf'.format(invoice.number),
as_attachment=True,
content_type='application/pdf'
)

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.27 on 2026-01-21 12:06
from django.db import migrations, models
import i18nfield.fields
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_pluggable_permissions"),
]
operations = [
migrations.AddField(
model_name="itemprogramtime",
name="location",
field=i18nfield.fields.I18nTextField(max_length=200, null=True),
)
]

View File

@@ -2306,10 +2306,17 @@ class ItemProgramTime(models.Model):
:type start: datetime
:param end: The date and time this program time ends
:type end: datetime
:param location: venue
:type location: str
"""
item = models.ForeignKey('Item', related_name='program_times', on_delete=models.CASCADE)
start = models.DateTimeField(verbose_name=_("Start"))
end = models.DateTimeField(verbose_name=_("End"))
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
def clean(self):
if hasattr(self, 'item') and self.item and self.item.event.has_subevents:

View File

@@ -58,6 +58,7 @@ from pretix.base.invoicing.transmission import (
from pretix.base.models import (
ExchangeRate, Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
)
from pretix.base.models.orders import OrderPayment
from pretix.base.models.tax import EU_CURRENCIES
from pretix.base.services.tasks import (
TransactionAwareProfiledEventTask, TransactionAwareTask,
@@ -102,7 +103,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
if lp and lp.payment_provider:
if lp and lp.payment_provider and lp.state not in (OrderPayment.PAYMENT_STATE_FAILED, OrderPayment.PAYMENT_STATE_CANCELED):
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:

View File

@@ -1354,6 +1354,10 @@ class ItemProgramTimeForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['end'].widget.attrs['data-date-after'] = '#id_{prefix}-start_0'.format(prefix=self.prefix)
self.fields['location'].widget.attrs['rows'] = '1'
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center, Heidelberg, Germany'
)
class Meta:
model = ItemProgramTime
@@ -1361,6 +1365,7 @@ class ItemProgramTimeForm(I18nModelForm):
fields = [
'start',
'end',
'location'
]
field_classes = {
'start': forms.SplitDateTimeField,

View File

@@ -34,6 +34,7 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.start layout="control" %}
{% bootstrap_field form.end layout="control" %}
{% bootstrap_field form.location layout="control" %}
</div>
</div>
{% endfor %}
@@ -59,6 +60,7 @@
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.start layout="control" %}
{% bootstrap_field formset.empty_form.end layout="control" %}
{% bootstrap_field formset.empty_form.location layout="control" %}
</div>
</div>
{% endescapescript %}

View File

@@ -153,7 +153,7 @@ def get_private_icals(event, positions):
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
location = None
location = ", ".join(l.strip() for l in str(pt.location).splitlines() if l.strip())
dtstart = pt.start.astimezone(tz)
dtend = pt.end.astimezone(tz)
uid = 'pretix-{}-{}-{}-{}@{}'.format(

View File

@@ -530,6 +530,7 @@ def test_item_detail_program_times(token_client, organizer, event, team, item, c
res["program_times"] = [{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": None
}]
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
@@ -1972,32 +1973,54 @@ def program_time2(item, category):
end=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc))
@pytest.fixture
def program_time3(item, category):
return item.program_times.create(start=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 31, 0, 0, 0, tzinfo=timezone.utc),
location='Testlocation')
TEST_PROGRAM_TIMES_RES = {
0: {
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": None,
},
1: {
"start": "2017-12-29T00:00:00Z",
"end": "2017-12-30T00:00:00Z",
"location": None,
},
2: {
"start": "2017-12-30T00:00:00Z",
"end": "2017-12-31T00:00:00Z",
"location": {"en": "Testlocation"},
}
}
@pytest.mark.django_db
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2):
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2, program_time3):
res = dict(TEST_PROGRAM_TIMES_RES)
res[0]["id"] = program_time.pk
res[1]["id"] = program_time2.pk
res[2]["id"] = program_time3.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res[0]['start'] == resp.data['results'][0]['start']
assert res[0]['end'] == resp.data['results'][0]['end']
assert res[0]['id'] == resp.data['results'][0]['id']
assert res[0] == resp.data['results'][0]
assert res[1]['start'] == resp.data['results'][1]['start']
assert res[1]['end'] == resp.data['results'][1]['end']
assert res[1]['id'] == resp.data['results'][1]['id']
assert res[1] == resp.data['results'][1]
assert res[2]['start'] == resp.data['results'][2]['start']
assert res[2]['end'] == resp.data['results'][2]['end']
assert res[2]['location'] == resp.data['results'][2]['location']
assert res[2]['id'] == resp.data['results'][2]['id']
assert res[2] == resp.data['results'][2]
@pytest.mark.django_db
@@ -2039,6 +2062,59 @@ def test_program_times_create(token_client, organizer, event, item):
assert resp.content.decode() == '{"non_field_errors":["The program end must not be before the program start."]}'
@pytest.mark.django_db
def test_program_times_create_location(token_client, organizer, event, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": {
"en": "Testlocation",
"de": "Testort"
}
},
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert "Testlocation" == program_time.location.localize("en")
assert "Testort" == program_time.location.localize("de")
@pytest.mark.django_db
def test_program_times_create_without_location(token_client, organizer, event, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z"
},
format='json'
)
assert resp.status_code == 201
assert resp.data['location'] is None
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert str(program_time.location) == ""
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": None
},
format='json'
)
assert resp.status_code == 201
assert resp.data['location'] is None
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert str(program_time.location) == ""
@pytest.mark.django_db
def test_program_times_update(token_client, organizer, event, item, program_time):
resp = token_client.patch(