mirror of
https://github.com/pretix/pretix.git
synced 2026-08-06 10:07:50 +00:00
Add last_modified property to orders (#907)
This commit is contained in:
@@ -49,7 +49,7 @@ invoice_address object Invoice address
|
||||
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
happens in rare cases.
|
||||
position list of objects List of order positions (see below)
|
||||
positions list of objects List of order positions (see below)
|
||||
fees list of objects List of fees included in the order total (i.e.
|
||||
payment fees)
|
||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||
@@ -68,6 +68,7 @@ downloads list of objects List of ticket
|
||||
download options.
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
└ url string Download URL
|
||||
last_modified datetime Last modification of this object
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -96,6 +97,10 @@ downloads list of objects List of ticket
|
||||
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate``, ``order.payment_fee_tax_value`` and
|
||||
``order.payment_fee_tax_rule`` have finally been removed.
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -174,6 +179,7 @@ Order endpoints
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
X-Page-Generated: 2017-12-01T10:00:00Z
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
@@ -188,6 +194,7 @@ Order endpoints
|
||||
"locale": "en",
|
||||
"datetime": "2017-12-01T10:00:00Z",
|
||||
"expires": "2017-12-10T10:00:00Z",
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-05",
|
||||
"payment_provider": "banktransfer",
|
||||
"fees": [],
|
||||
@@ -264,8 +271,11 @@ Order endpoints
|
||||
:query string status: Only return orders in the given order status (see above)
|
||||
:query string email: Only return orders created with the given email address
|
||||
:query string locale: Only return orders with the given customer locale
|
||||
:query datetime modified_since: Only return orders that have changed since the given date
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:resheader X-Page-Generated: The server time at the beginning of the operation. If you're using this API to fetch
|
||||
differences, this is the value you want to use as ``modified_since`` in your next call.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
@@ -298,6 +308,7 @@ Order endpoints
|
||||
"locale": "en",
|
||||
"datetime": "2017-12-01T10:00:00Z",
|
||||
"expires": "2017-12-10T10:00:00Z",
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"payment_date": "2017-12-05",
|
||||
"payment_provider": "banktransfer",
|
||||
"fees": [],
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
@@ -123,18 +121,6 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||
|
||||
|
||||
class PaymentFeeLegacyField(serializers.Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.attr = kwargs.pop('attribute')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_representation(self, instance: Order):
|
||||
return str(
|
||||
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
|
||||
Decimal('0.00'))
|
||||
)
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAddressSerializer()
|
||||
positions = OrderPositionSerializer(many=True)
|
||||
@@ -145,7 +131,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
model = Order
|
||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention')
|
||||
'checkin_attention', 'last_modified')
|
||||
|
||||
|
||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytz
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.http import FileResponse
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import serializers, status, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
@@ -38,6 +38,7 @@ class OrderFilter(FilterSet):
|
||||
email = django_filters.CharFilter(name='email', lookup_expr='iexact')
|
||||
code = django_filters.CharFilter(name='code', lookup_expr='iexact')
|
||||
status = django_filters.CharFilter(name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(name='last_modified', lookup_expr='gte')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -71,6 +72,18 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return prov
|
||||
raise NotFound('Unknown output provider.')
|
||||
|
||||
def list(self, request, **kwargs):
|
||||
date = serializers.DateTimeField().to_representation(now())
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data, headers={'X-Page-Generated': date})
|
||||
|
||||
@detail_route(url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
def download(self, request, output, **kwargs):
|
||||
provider = self._get_output_provider(output)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-05-13 16:41
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0090_auto_20180509_0917'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='last_modified',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||
),
|
||||
]
|
||||
@@ -168,3 +168,11 @@ class Checkin(models.Model):
|
||||
return "<Checkin: pos {} on list '{}' at {}>".format(
|
||||
self.position, self.list, self.datetime
|
||||
)
|
||||
|
||||
def save(self, **kwargs):
|
||||
self.position.order.touch()
|
||||
super().save(**kwargs)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
self.position.order.touch()
|
||||
super().delete(**kwargs)
|
||||
|
||||
@@ -180,6 +180,9 @@ class Order(LoggedModel):
|
||||
verbose_name=_("Meta information"),
|
||||
null=True, blank=True
|
||||
)
|
||||
last_modified = models.DateTimeField(
|
||||
auto_now=True, db_index=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order")
|
||||
@@ -208,12 +211,17 @@ class Order(LoggedModel):
|
||||
def changable(self):
|
||||
return self.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
def save(self, **kwargs):
|
||||
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
|
||||
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
|
||||
if not self.code:
|
||||
self.assign_code()
|
||||
if not self.datetime:
|
||||
self.datetime = now()
|
||||
super().save(*args, **kwargs)
|
||||
super().save(**kwargs)
|
||||
|
||||
def touch(self):
|
||||
self.save(update_fields=['last_modified'])
|
||||
|
||||
@cached_property
|
||||
def tax_total(self):
|
||||
@@ -547,8 +555,15 @@ class QuestionAnswer(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
if self.orderposition and self.cartposition:
|
||||
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
||||
if self.orderposition:
|
||||
self.orderposition.order.touch()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
if self.orderposition:
|
||||
self.orderposition.order.touch()
|
||||
super().delete(**kwargs)
|
||||
|
||||
|
||||
class AbstractPosition(models.Model):
|
||||
"""
|
||||
@@ -751,8 +766,13 @@ class OrderFee(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
if self.tax_rate is None:
|
||||
self._calculate_tax()
|
||||
self.order.touch()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
self.order.touch()
|
||||
super().delete(**kwargs)
|
||||
|
||||
|
||||
class OrderPosition(AbstractPosition):
|
||||
"""
|
||||
@@ -861,6 +881,7 @@ class OrderPosition(AbstractPosition):
|
||||
def save(self, *args, **kwargs):
|
||||
if self.tax_rate is None:
|
||||
self._calculate_tax()
|
||||
self.order.touch()
|
||||
if self.pk is None:
|
||||
while OrderPosition.objects.filter(secret=self.secret).exists():
|
||||
self.secret = generate_position_secret()
|
||||
@@ -945,6 +966,11 @@ class InvoiceAddress(models.Model):
|
||||
blank=True
|
||||
)
|
||||
|
||||
def save(self, **kwargs):
|
||||
if self.order:
|
||||
self.order.touch()
|
||||
super().save(**kwargs)
|
||||
|
||||
|
||||
def cachedticket_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
||||
|
||||
@@ -1143,6 +1143,7 @@ class OrderChangeManager:
|
||||
self._recalculate_total_and_payment_fee()
|
||||
self._reissue_invoice()
|
||||
self._clear_tickets_cache()
|
||||
self.order.touch()
|
||||
self._check_paid_to_free()
|
||||
|
||||
if self.notify:
|
||||
|
||||
@@ -144,6 +144,7 @@ def test_order_list(token_client, organizer, event, order, item, taxrule, questi
|
||||
res["positions"][0]["id"] = order.positions.first().pk
|
||||
res["positions"][0]["item"] = item.pk
|
||||
res["positions"][0]["answers"][0]["question"] = question.pk
|
||||
res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z')
|
||||
res["fees"][0]["tax_rule"] = taxrule.pk
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/'.format(organizer.slug, event.slug))
|
||||
@@ -172,6 +173,19 @@ def test_order_list(token_client, organizer, event, order, item, taxrule, questi
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?locale=de'.format(organizer.slug, event.slug))
|
||||
assert [] == resp.data['results']
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?modified_since={}'.format(
|
||||
organizer.slug, event.slug, (order.last_modified - datetime.timedelta(hours=1)).isoformat().replace('+00:00', 'Z')
|
||||
))
|
||||
assert [res] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?modified_since={}'.format(
|
||||
organizer.slug, event.slug, order.last_modified.isoformat().replace('+00:00', 'Z')
|
||||
))
|
||||
assert [res] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?modified_since={}'.format(
|
||||
organizer.slug, event.slug, (order.last_modified + datetime.timedelta(hours=1)).isoformat().replace('+00:00', 'Z')
|
||||
))
|
||||
assert [] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_detail(token_client, organizer, event, order, item, taxrule, question):
|
||||
@@ -180,6 +194,7 @@ def test_order_detail(token_client, organizer, event, order, item, taxrule, ques
|
||||
res["positions"][0]["item"] = item.pk
|
||||
res["fees"][0]["tax_rule"] = taxrule.pk
|
||||
res["positions"][0]["answers"][0]["question"] = question.pk
|
||||
res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z')
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/'.format(organizer.slug, event.slug,
|
||||
order.code))
|
||||
assert resp.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user