Implement Last-Modified for a number of API resources

This commit is contained in:
Raphael Michel
2018-05-17 16:09:08 +02:00
parent 118259a96b
commit 26029508c6
12 changed files with 111 additions and 6 deletions

View File

@@ -109,6 +109,41 @@ respective page.
The field ``results`` contains a list of objects representing the first results. For most
objects, every page contains 50 results.
Conditional fetching
--------------------
If you pull object lists from pretix' APIs regularly, we ask you to implement conditional fetching
to avoid unnecessary data traffic. This is not supported on all resources and we currently implement
two different mechanisms for different resources, which is necessary because we can only obtain best
efficiency for resources that do not support deletion operations.
Object-level conditional fetching
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :ref:`rest-orders` resource list contains an HTTP header called ``X-Page-Generated`` containing the
current time on the server in ISO 8601 format. On your next request, you can pass this header
(as is, without any modifications necessary) as the ``modified_since`` query parameter and you will receive
a list containing only objects that have changed in the time since your last request.
List-level conditional fetching
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If modificiation checks are not possible with this granularity, you can instead check for the full list.
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
last modification to any item of that resource. You can then pass this date back in your next request in the
``If-Modified-Since`` header. If the any object has changed in the meantime, you will receive back a full list
(if something it missing, this means the object has been deleted). If nothing happend, we'll send back a
``304 Not Modified`` return code.
This is currently implemented on the following resources:
* :ref:`rest-categories`
* :ref:`rest-items`
* :ref:`rest-questions`
* :ref:`rest-quotas`
* :ref:`rest-subevents`
* :ref:`rest-taxrules`
Errors
------

View File

@@ -1,3 +1,5 @@
.. _`rest-categories`:
Item categories
===============

View File

@@ -1,3 +1,5 @@
.. _rest-items:
Items
=====

View File

@@ -1,5 +1,8 @@
.. spelling:: checkins
.. _rest-orders:
Orders
======

View File

@@ -1,5 +1,7 @@
.. spelling:: checkin
.. _rest-questions:
Questions
=========

View File

@@ -1,3 +1,5 @@
.. _rest-quotas:
Quotas
======

View File

@@ -1,3 +1,5 @@
.. _rest-subevents:
Event series dates / Sub-events
===============================

View File

@@ -1,3 +1,5 @@
.. _rest-taxrules:
Tax rules
=========

View File

@@ -1,3 +1,8 @@
from calendar import timegm
from django.db.models import Max
from django.http import HttpResponse
from django.utils.http import http_date, parse_http_date_safe
from rest_framework.filters import OrderingFilter
@@ -21,3 +26,34 @@ class RichOrderingFilter(OrderingFilter):
return queryset.order_by(*ordering)
return queryset
class ConditionalListView:
def list(self, request, **kwargs):
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
lmd = request.event.logentry_set.filter(
content_type__model=self.queryset.model._meta.model_name,
content_type__app_label=self.queryset.model._meta.app_label,
).aggregate(
m=Max('datetime')
)['m']
if lmd:
lmd_ts = timegm(lmd.utctimetuple())
print(lmd_ts, if_modified_since)
if if_unmodified_since and lmd and lmd_ts > if_unmodified_since:
return HttpResponse(status=412)
if if_modified_since and lmd and lmd_ts <= if_modified_since:
return HttpResponse(status=304)
resp = super().list(request, **kwargs)
if lmd:
resp['Last-Modified'] = http_date(lmd_ts)
return resp

View File

@@ -9,6 +9,7 @@ from pretix.api.serializers.event import (
CloneEventSerializer, EventSerializer, SubEventSerializer,
TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import Event, ItemCategory, TaxRule
from pretix.base.models.event import SubEvent
from pretix.base.models.organizer import TeamAPIToken
@@ -125,7 +126,7 @@ class SubEventFilter(FilterSet):
fields = ['active']
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
serializer_class = SubEventSerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
@@ -137,7 +138,7 @@ class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
)
class TaxRuleViewSet(viewsets.ModelViewSet):
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = TaxRuleSerializer
queryset = TaxRule.objects.none()
write_permission = 'can_change_event_settings'

View File

@@ -13,6 +13,7 @@ from pretix.api.serializers.item import (
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
QuotaSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
@@ -35,7 +36,7 @@ class ItemFilter(FilterSet):
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class ItemViewSet(viewsets.ModelViewSet):
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -203,7 +204,7 @@ class ItemCategoryFilter(FilterSet):
fields = ['is_addon']
class ItemCategoryViewSet(viewsets.ModelViewSet):
class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = ItemCategorySerializer
queryset = ItemCategory.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -257,7 +258,7 @@ class QuestionFilter(FilterSet):
fields = ['ask_during_checkin', 'required', 'identifier']
class QuestionViewSet(viewsets.ModelViewSet):
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = QuestionSerializer
queryset = Question.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -355,7 +356,7 @@ class QuotaFilter(FilterSet):
fields = ['subevent']
class QuotaViewSet(viewsets.ModelViewSet):
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = QuotaSerializer
queryset = Quota.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter,)

View File

@@ -1,3 +1,4 @@
import time
from datetime import datetime, timedelta
from decimal import Decimal
from unittest import mock
@@ -116,6 +117,22 @@ def test_category_list(token_client, organizer, event, team, category):
assert resp.status_code == 200
assert [res] == resp.data['results']
category.log_action('foo')
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/'.format(
organizer.slug, event.slug))
assert resp.status_code == 200
lmd = resp['Last-Modified']
assert lmd
time.sleep(1)
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/'.format(
organizer.slug, event.slug), HTTP_IF_MODIFIED_SINCE=lmd)
assert resp.status_code == 304
time.sleep(1)
category.log_action('foo')
resp = token_client.get('/api/v1/organizers/{}/events/{}/categories/'.format(
organizer.slug, event.slug), HTTP_IF_MODIFIED_SINCE=lmd)
assert resp.status_code == 200
@pytest.mark.django_db
def test_category_detail(token_client, organizer, event, team, category):