Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-django-eremaea2 for
openSUSE:Factory checked in at 2025-09-23 20:47:10
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-django-eremaea2 (Old)
and /work/SRC/openSUSE:Factory/.python-django-eremaea2.new.27445 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-django-eremaea2"
Tue Sep 23 20:47:10 2025 rev:10 rq:1306762 version:2.1.1
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-django-eremaea2/python-django-eremaea2.changes
2024-10-04 17:11:20.970855590 +0200
+++
/work/SRC/openSUSE:Factory/.python-django-eremaea2.new.27445/python-django-eremaea2.changes
2025-09-23 20:48:17.806379765 +0200
@@ -1,0 +2,6 @@
+Tue Sep 23 08:48:30 UTC 2025 - Matwey Kornilov <[email protected]>
+
+- Version 2.1.1
+ - Rework API structure
+
+-------------------------------------------------------------------
Old:
----
django_eremaea2-2.0.22.tar.gz
New:
----
django_eremaea2-2.1.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-django-eremaea2.spec ++++++
--- /var/tmp/diff_new_pack.jE7xui/_old 2025-09-23 20:48:18.494408850 +0200
+++ /var/tmp/diff_new_pack.jE7xui/_new 2025-09-23 20:48:18.494408850 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-django-eremaea2
#
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2025 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -20,7 +20,7 @@
%define skip_python36 1
%{?sle15_python_module_pythons}
Name: python-django-eremaea2
-Version: 2.0.22
+Version: 2.1.1
Release: 0
Summary: A simple Django application to store and show webcam snapshots
License: BSD-2-Clause
@@ -30,7 +30,9 @@
BuildArch: noarch
BuildRequires: %{python_module click}
BuildRequires: %{python_module devel}
+BuildRequires: %{python_module django-filter}
BuildRequires: %{python_module djangorestframework >= 3.7.0}
+BuildRequires: %{python_module drf-spectacular}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module pytest-django}
# https://github.com/matwey/django-eremaea2/issues/15
@@ -44,12 +46,14 @@
BuildRequires: python-rpm-macros
Requires: eremaea = %{version}
Requires: python-click
+Requires: python-django-filter
Requires: python-djangorestframework >= 3.7.0
+Requires: python-drf-spectacular
Requires: python-python-magic
Requires: python-requests
Requires: python-requests-toolbelt
Requires(post): update-alternatives
-Requires(postun):update-alternatives
+Requires(postun): update-alternatives
%python_subpackages
%description
++++++ django_eremaea2-2.0.22.tar.gz -> django_eremaea2-2.1.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/PKG-INFO
new/django_eremaea2-2.1.1/PKG-INFO
--- old/django_eremaea2-2.0.22/PKG-INFO 2024-10-03 15:53:29.807284600 +0200
+++ new/django_eremaea2-2.1.1/PKG-INFO 2025-09-23 10:41:26.318097000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: django-eremaea2
-Version: 2.0.22
+Version: 2.1.1
Summary: A simple Django application to store and show webcam snapshots
Author-email: "Matwey V. Kornilov" <[email protected]>
License: BSD-2-Clause
@@ -20,7 +20,9 @@
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click
+Requires-Dist: django-filter
Requires-Dist: djangorestframework
+Requires-Dist: drf_spectacular
Requires-Dist: python-magic
Requires-Dist: requests
Requires-Dist: requests_toolbelt
@@ -30,6 +32,7 @@
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-django; extra == "dev"
Requires-Dist: requests-mock; extra == "dev"
+Dynamic: license-file
# django-eremaea2
@@ -73,8 +76,7 @@
To perform actual cleanup a POST request to the endpoint
```http://example.com/eremaea/retention_policies/{name}/purge``` is required.
The ```Collection``` model has two parameters: a lookup field ```name``` and
```default_retention_policy```.
The ```Snapshot``` model has the following field: associated ```collection```
and ```retention_policy```, ```file``` object, and auto-now ```date```.
-New images are uploaded by POST request to the endpoint
```http://example.com/eremaea/snapshots/?collection=collection_name&retention_policy=retention_policy_name```.
-The latest query parameter is optional one.
+New images are uploaded by POST request to the endpoint
```http://example.com/eremaea/snapshots/{collection}/``` with optional
```retention_policy={retention_policy_name}``` query string parameter.
The other ways to configure the application are Django fixtures, Django admin
interface, [django-rest-framework] web browsable interface, and REST API itself.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/README.md
new/django_eremaea2-2.1.1/README.md
--- old/django_eremaea2-2.0.22/README.md 2024-10-03 15:53:20.000000000
+0200
+++ new/django_eremaea2-2.1.1/README.md 2025-09-23 10:41:21.000000000 +0200
@@ -40,8 +40,7 @@
To perform actual cleanup a POST request to the endpoint
```http://example.com/eremaea/retention_policies/{name}/purge``` is required.
The ```Collection``` model has two parameters: a lookup field ```name``` and
```default_retention_policy```.
The ```Snapshot``` model has the following field: associated ```collection```
and ```retention_policy```, ```file``` object, and auto-now ```date```.
-New images are uploaded by POST request to the endpoint
```http://example.com/eremaea/snapshots/?collection=collection_name&retention_policy=retention_policy_name```.
-The latest query parameter is optional one.
+New images are uploaded by POST request to the endpoint
```http://example.com/eremaea/snapshots/{collection}/``` with optional
```retention_policy={retention_policy_name}``` query string parameter.
The other ways to configure the application are Django fixtures, Django admin
interface, [django-rest-framework] web browsable interface, and REST API itself.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/pyproject.toml
new/django_eremaea2-2.1.1/pyproject.toml
--- old/django_eremaea2-2.0.22/pyproject.toml 2024-10-03 15:53:20.000000000
+0200
+++ new/django_eremaea2-2.1.1/pyproject.toml 2025-09-23 10:41:21.000000000
+0200
@@ -4,7 +4,7 @@
[project]
name = "django-eremaea2"
-version = "2.0.22"
+version = "2.1.1"
authors = [
{name = "Matwey V. Kornilov", email = "[email protected]"},
]
@@ -26,7 +26,9 @@
requires-python = ">= 3.7"
dependencies = [
"click",
+ "django-filter",
"djangorestframework",
+ "drf_spectacular",
"python-magic",
"requests",
"requests_toolbelt",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_eremaea2-2.0.22/src/django_eremaea2.egg-info/PKG-INFO
new/django_eremaea2-2.1.1/src/django_eremaea2.egg-info/PKG-INFO
--- old/django_eremaea2-2.0.22/src/django_eremaea2.egg-info/PKG-INFO
2024-10-03 15:53:29.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/django_eremaea2.egg-info/PKG-INFO
2025-09-23 10:41:26.000000000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: django-eremaea2
-Version: 2.0.22
+Version: 2.1.1
Summary: A simple Django application to store and show webcam snapshots
Author-email: "Matwey V. Kornilov" <[email protected]>
License: BSD-2-Clause
@@ -20,7 +20,9 @@
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click
+Requires-Dist: django-filter
Requires-Dist: djangorestframework
+Requires-Dist: drf_spectacular
Requires-Dist: python-magic
Requires-Dist: requests
Requires-Dist: requests_toolbelt
@@ -30,6 +32,7 @@
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-django; extra == "dev"
Requires-Dist: requests-mock; extra == "dev"
+Dynamic: license-file
# django-eremaea2
@@ -73,8 +76,7 @@
To perform actual cleanup a POST request to the endpoint
```http://example.com/eremaea/retention_policies/{name}/purge``` is required.
The ```Collection``` model has two parameters: a lookup field ```name``` and
```default_retention_policy```.
The ```Snapshot``` model has the following field: associated ```collection```
and ```retention_policy```, ```file``` object, and auto-now ```date```.
-New images are uploaded by POST request to the endpoint
```http://example.com/eremaea/snapshots/?collection=collection_name&retention_policy=retention_policy_name```.
-The latest query parameter is optional one.
+New images are uploaded by POST request to the endpoint
```http://example.com/eremaea/snapshots/{collection}/``` with optional
```retention_policy={retention_policy_name}``` query string parameter.
The other ways to configure the application are Django fixtures, Django admin
interface, [django-rest-framework] web browsable interface, and REST API itself.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_eremaea2-2.0.22/src/django_eremaea2.egg-info/SOURCES.txt
new/django_eremaea2-2.1.1/src/django_eremaea2.egg-info/SOURCES.txt
--- old/django_eremaea2-2.0.22/src/django_eremaea2.egg-info/SOURCES.txt
2024-10-03 15:53:29.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/django_eremaea2.egg-info/SOURCES.txt
2025-09-23 10:41:26.000000000 +0200
@@ -23,6 +23,8 @@
src/eremaea/ctl/commandline.py
src/eremaea/ctl/file.py
src/eremaea/migrations/0001_squashed_0002_drop_index_together.py
+src/eremaea/migrations/0003_initial_retention_policies.py
+src/eremaea/migrations/0004_use_slug_for_names.py
src/eremaea/migrations/__init__.py
tests/__init__.py
tests/settings.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_eremaea2-2.0.22/src/django_eremaea2.egg-info/requires.txt
new/django_eremaea2-2.1.1/src/django_eremaea2.egg-info/requires.txt
--- old/django_eremaea2-2.0.22/src/django_eremaea2.egg-info/requires.txt
2024-10-03 15:53:29.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/django_eremaea2.egg-info/requires.txt
2025-09-23 10:41:26.000000000 +0200
@@ -1,5 +1,7 @@
click
+django-filter
djangorestframework
+drf_spectacular
python-magic
requests
requests_toolbelt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/src/eremaea/ctl/client.py
new/django_eremaea2-2.1.1/src/eremaea/ctl/client.py
--- old/django_eremaea2-2.0.22/src/eremaea/ctl/client.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/eremaea/ctl/client.py 2025-09-23
10:41:21.000000000 +0200
@@ -9,12 +9,12 @@
self._session.headers.update({'Authorization': 'Token
{}'.format(token)})
def upload(self, file, collection, retention_policy = None):
- url = self.api + '/snapshots/'
+ url = self.api + '/snapshots/{}/'.format(collection)
headers = {
'Content-Disposition': 'attachment;
filename=\"{}\"'.format(file.name),
'Content-Type': file.mimetype
}
- params = {'collection': collection}
+ params = {}
if retention_policy:
params['retention_policy'] = retention_policy
r = self._session.post(url, params=params, headers=headers,
data=file.content)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_eremaea2-2.0.22/src/eremaea/migrations/0003_initial_retention_policies.py
new/django_eremaea2-2.1.1/src/eremaea/migrations/0003_initial_retention_policies.py
---
old/django_eremaea2-2.0.22/src/eremaea/migrations/0003_initial_retention_policies.py
1970-01-01 01:00:00.000000000 +0100
+++
new/django_eremaea2-2.1.1/src/eremaea/migrations/0003_initial_retention_policies.py
2025-09-23 10:41:21.000000000 +0200
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.4 on 2023-08-11 08:25
+
+from datetime import timedelta
+from django.db import migrations
+
+
+def populate_retention_policies(apps, schema_editor):
+ INITIAL_RETENTION_POLICIES = [
+ ("annual", 365),
+ ("monthly", 30),
+ ("weekly", 7),
+ ("daily", 1),
+ ]
+
+ RetentionPolicy = apps.get_model("eremaea", "RetentionPolicy")
+
+ for (name, duration) in INITIAL_RETENTION_POLICIES:
+ RetentionPolicy.objects.get_or_create(name = name, duration =
timedelta(days = duration))
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('eremaea', '0002_drop_index_together'),
+ ]
+
+ operations = [
+ migrations.RunPython(populate_retention_policies),
+ ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django_eremaea2-2.0.22/src/eremaea/migrations/0004_use_slug_for_names.py
new/django_eremaea2-2.1.1/src/eremaea/migrations/0004_use_slug_for_names.py
---
old/django_eremaea2-2.0.22/src/eremaea/migrations/0004_use_slug_for_names.py
1970-01-01 01:00:00.000000000 +0100
+++ new/django_eremaea2-2.1.1/src/eremaea/migrations/0004_use_slug_for_names.py
2025-09-23 10:41:21.000000000 +0200
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.3 on 2025-08-12 09:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('eremaea', '0003_initial_retention_policies'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='collection',
+ name='name',
+ field=models.SlugField(max_length=256, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='retentionpolicy',
+ name='name',
+ field=models.SlugField(max_length=256, unique=True),
+ ),
+ ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/src/eremaea/models.py
new/django_eremaea2-2.1.1/src/eremaea/models.py
--- old/django_eremaea2-2.0.22/src/eremaea/models.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/eremaea/models.py 2025-09-23
10:41:21.000000000 +0200
@@ -69,39 +69,17 @@
if storage.exists(path):
storage.delete(path)
- def get_next(self):
- try:
- return self.get_next_by_date(collection =
self.collection)
- except Snapshot.DoesNotExist:
- pass
- def get_previous(self):
- try:
- return self.get_previous_by_date(collection =
self.collection)
- except Snapshot.DoesNotExist:
- pass
-
class Meta:
ordering = ['-date']
get_latest_by = 'date'
class Collection(models.Model):
- name = models.CharField(max_length=256, blank=False, unique=True,
db_index=True)
+ name = models.SlugField(max_length=256, unique=True, db_index=True)
default_retention_policy = models.ForeignKey('RetentionPolicy',
on_delete=models.PROTECT)
- def get_latest(self):
- try:
- return Snapshot.objects.filter(collection =
self).latest()
- except Snapshot.DoesNotExist:
- pass
- def get_earliest(self):
- try:
- return Snapshot.objects.filter(collection =
self).earliest()
- except Snapshot.DoesNotExist:
- pass
-
class RetentionPolicy(models.Model):
- name = models.CharField(max_length=256, blank=False, null=False,
unique=True, db_index=True)
- duration = models.DurationField(blank=False, null=False)
+ name = models.SlugField(max_length=256, unique=True, db_index=True)
+ duration = models.DurationField()
class Meta:
db_table = 'retention_policy'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/src/eremaea/serializers.py
new/django_eremaea2-2.1.1/src/eremaea/serializers.py
--- old/django_eremaea2-2.0.22/src/eremaea/serializers.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/eremaea/serializers.py 2025-09-23
10:41:21.000000000 +0200
@@ -1,29 +1,69 @@
from eremaea import models
+from django.shortcuts import get_object_or_404
from rest_framework import serializers
+from rest_framework.reverse import reverse
+
+
+class SnapshotHyperlinkRelated(serializers.HyperlinkedRelatedField):
+ view_name = 'snapshot-detail'
+
+ def get_url(self, obj, view_name, request, format):
+ url_kwargs = {
+ 'collection': obj.collection.name,
+ 'pk': obj.pk,
+ }
+
+ return reverse(view_name, kwargs=url_kwargs, request=request,
format=format)
+
+ def get_object(self, view_name, view_args, view_kwargs):
+ return self.get_queryset().get(collection__name =
view_kwargs['collection'], pk = view_kwargs['pk'])
+
+class SnapshotHyperlinkIdentity(SnapshotHyperlinkRelated):
+ def __init__(self, **kwargs):
+ kwargs['read_only'] = True
+ kwargs['source'] = '*'
+ super(SnapshotHyperlinkIdentity, self).__init__(**kwargs)
+
+class CurrentCollectionDefault:
+ requires_context = True
+
+ def __call__(self, serializer_field):
+ view = serializer_field.context['view']
+ collection_name = view.kwargs['collection']
+ collection = get_object_or_404(models.Collection, name =
collection_name)
+ return collection
class RetentionPolicySerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(lookup_field='name',
view_name='retention_policy-detail')
class Meta:
model = models.RetentionPolicy
- fields = ('url', 'name', 'duration')
+ fields = '__all__'
+
+class CreateSnapshotSerializer(serializers.HyperlinkedModelSerializer):
+ url = SnapshotHyperlinkIdentity()
+ collection =
serializers.SlugRelatedField(queryset=models.Collection.objects.all(),
slug_field='name', allow_null=False,
default=serializers.CreateOnlyDefault(CurrentCollectionDefault()))
+ retention_policy =
serializers.SlugRelatedField(queryset=models.RetentionPolicy.objects.all(),
slug_field='name', allow_null=True)
+
+ class Meta:
+ model = models.Snapshot
+ fields = '__all__'
+
+class SnapshotSerializer(CreateSnapshotSerializer):
+ class Meta(CreateSnapshotSerializer.Meta):
+ read_only_fields = ['file']
-class SnapshotSerializer(serializers.HyperlinkedModelSerializer):
- collection =
serializers.HyperlinkedRelatedField(view_name='collection-detail',
read_only=True, lookup_field='name')
- retention_policy =
serializers.HyperlinkedRelatedField(view_name='retention_policy-detail',
queryset=models.RetentionPolicy.objects.all(), lookup_field='name')
- next = serializers.HyperlinkedRelatedField(view_name='snapshot-detail',
read_only=True, source="get_next")
- prev = serializers.HyperlinkedRelatedField(view_name='snapshot-detail',
read_only=True, source="get_previous")
+class ListSnapshotSerializer(SnapshotSerializer):
+ collection = None
class Meta:
model = models.Snapshot
- fields = ('url', 'collection', 'file', 'date',
'retention_policy', 'next', 'prev')
+ exclude = ['collection',]
class CollectionSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(lookup_field='name',
view_name='collection-detail')
- default_retention_policy =
serializers.HyperlinkedRelatedField(view_name='retention_policy-detail',
queryset=models.RetentionPolicy.objects.all(), lookup_field='name')
- begin =
serializers.HyperlinkedRelatedField(view_name='snapshot-detail',
read_only=True, source="get_earliest")
- end = serializers.HyperlinkedRelatedField(view_name='snapshot-detail',
read_only=True, source="get_latest")
+ default_retention_policy =
serializers.SlugRelatedField(queryset=models.RetentionPolicy.objects.all(),
slug_field='name')
class Meta:
model = models.Collection
- fields = ('url', 'name', 'default_retention_policy', 'begin',
'end')
+ fields = '__all__'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/src/eremaea/urls.py
new/django_eremaea2-2.1.1/src/eremaea/urls.py
--- old/django_eremaea2-2.0.22/src/eremaea/urls.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/eremaea/urls.py 2025-09-23
10:41:21.000000000 +0200
@@ -4,7 +4,7 @@
router = DefaultRouter()
router.register(r'collections', views.CollectionViewSet)
-router.register(r'snapshots', views.SnapshotViewSet)
+router.register(r'snapshots/(?P<collection>[-\w]+)', views.SnapshotViewSet)
router.register(r'retention_policies', views.RetentionPolicyViewSet,
basename='retention_policy')
if django.VERSION[0] > 1:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/src/eremaea/views.py
new/django_eremaea2-2.1.1/src/eremaea/views.py
--- old/django_eremaea2-2.0.22/src/eremaea/views.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/src/eremaea/views.py 2025-09-23
10:41:21.000000000 +0200
@@ -1,11 +1,129 @@
-from django.shortcuts import get_object_or_404
+import datetime
+from django.conf import settings
from django.db.models.deletion import ProtectedError
-from django.utils.cache import patch_cache_control
+from django.shortcuts import get_object_or_404
+from django.utils import timezone
+from django.utils.cache import patch_cache_control, patch_response_headers
+from django.utils.http import http_date
+from django_filters import rest_framework as filters
from eremaea import models, serializers
from rest_framework import status, viewsets
-from rest_framework.decorators import action
+from rest_framework.decorators import action, parser_classes
+from rest_framework.pagination import Cursor, CursorPagination, _positive_int
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response
+from rest_framework.settings import api_settings
+from rest_framework.utils.urls import replace_query_param
+from drf_spectacular.utils import extend_schema
+from drf_spectacular.types import OpenApiTypes
+
+
+class CollectionFilter(filters.FilterSet):
+ default_retention_policy =
filters.ModelChoiceFilter(queryset=models.RetentionPolicy.objects.all(),
to_field_name="name")
+
+ class Meta:
+ model = models.Collection
+ fields = ['default_retention_policy']
+
+class SnapshotPagination(CursorPagination):
+ ordering = '-date'
+ page_size_query_param = 'page_size'
+ cursor_separator = '.'
+ # Cursor text is always in UTC for consistency. However, it must be
+ # converted to Django format accoring to actual settings.USE_TZ
+ time_origin = datetime.datetime(2000, 1, 1,
tzinfo=datetime.timezone.utc)
+
+ @staticmethod
+ def _datetime_to_django(datetime):
+ if not settings.USE_TZ and timezone.is_aware(datetime):
+ datetime = timezone.make_naive(datetime,
timezone.get_default_timezone())
+ return datetime
+
+ @staticmethod
+ def _datetime_from_django(datetime):
+ if timezone.is_naive(datetime):
+ datetime = timezone.make_aware(datetime,
timezone.get_default_timezone())
+ return datetime
+
+ def decode_cursor(self, request):
+ encoded = request.query_params.get(self.cursor_query_param)
+ if encoded is None:
+ return None
+
+ try:
+ position, offset, reverse =
encoded.split(self.cursor_separator)
+
+ if not position:
+ position = None
+ else:
+ position =
self._datetime_to_django(self.time_origin + datetime.datetime.resolution *
_positive_int(position))
+ offset = _positive_int(offset,
cutoff=self.offset_cutoff)
+ reverse = bool(int(reverse))
+ except (TypeError, ValueError):
+ raise NotFound(self.invalid_cursor_message)
+
+ return Cursor(offset=offset, reverse=reverse, position=position)
+
+ def encode_cursor(self, cursor):
+ if cursor.position is not None:
+ position =
str(int((self._datetime_from_django(cursor.position) - self.time_origin) /
datetime.datetime.resolution))
+ else:
+ position = ''
+ offset = str(cursor.offset)
+ reverse = str(int(cursor.reverse))
+ encoded = self.cursor_separator.join([position, offset,
reverse])
+
+ return replace_query_param(self.base_url,
self.cursor_query_param, encoded)
+
+ def _get_position_from_instance(self, instance, ordering):
+ field_name = ordering[0].lstrip('-')
+ if isinstance(instance, dict):
+ attr = instance[field_name]
+ else:
+ attr = getattr(instance, field_name)
+
+ assert isinstance(attr, datetime.datetime), (
+ 'Invalid ordering value type. Expected
datetime.datetime type, but got {type}'.format(type=type(attr).__name__)
+ )
+
+ return attr
+
+ def get_paginated_response(self, data):
+ response = super(SnapshotPagination,
self).get_paginated_response(data)
+
+ date_now = timezone.now()
+ date = self.page[0].date.timestamp() if self.page else
date_now.timestamp()
+ response['Date'] = http_date(date)
+
+ reverse = self.cursor.reverse if self.cursor else False
+ current_position = self.cursor.position if self.cursor else None
+ is_expires = (current_position is not None and current_position
< date_now and not reverse) or (reverse and self.has_previous)
+
+ if self.page and is_expires:
+ last_instance = self.page[-1]
+ retention_policy = last_instance.retention_policy
+ response['Expires'] =
http_date(last_instance.date.timestamp() +
retention_policy.duration.total_seconds())
+ else:
+ patch_cache_control(response, no_cache=True, max_age=0)
+
+ return response
+
+class
SnapshotContentNegotiation(api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS):
+ def select_parser(self, request, parsers):
+ viewset = request.parser_context['view']
+ if viewset.action == 'create':
+ return FileUploadParser()
+
+ return super(SnapshotContentNegotiation,
self).select_parser(request, parsers)
+
+class SnapshotFilter(filters.FilterSet):
+ retention_policy =
filters.ModelChoiceFilter(queryset=models.RetentionPolicy.objects.all(),
to_field_name="name")
+ date = filters.IsoDateTimeFromToRangeFilter()
+
+ class Meta:
+ model = models.Snapshot
+ fields = ['retention_policy', 'date']
+
class RetentionPolicyViewSet(viewsets.ModelViewSet):
queryset = models.RetentionPolicy.objects.all()
@@ -17,54 +135,97 @@
return super(RetentionPolicyViewSet,
self).destroy(request, name)
except ProtectedError as e:
return Response(status=status.HTTP_400_BAD_REQUEST)
+
+ @extend_schema(request=None)
@action(methods=['post'], detail=True)
def purge(self, request, name):
- retention_policy = get_object_or_404(models.RetentionPolicy,
name = name)
+ retention_policy = self.get_object()
retention_policy.purge()
return Response(status=status.HTTP_201_CREATED)
class CollectionViewSet(viewsets.ModelViewSet):
- queryset =
models.Collection.objects.select_related("default_retention_policy")
+ queryset =
models.Collection.objects.select_related('default_retention_policy')
serializer_class = serializers.CollectionSerializer
lookup_field = 'name'
-
- def retrieve(self, request, name=None):
- response = super(CollectionViewSet, self).retrieve(request,
name)
- link = []
- if 'begin' in response.data:
- link.append("{0};
rel=begin".format(response.data['begin']))
- if 'end' in response.data:
- link.append("{0}; rel=end".format(response.data['end']))
- if link:
- response['Link'] = ", ".join(link)
- patch_cache_control(response, max_age=0, must_revalidate=True)
- return response
+ filter_backends = [filters.DjangoFilterBackend]
+ filterset_class = CollectionFilter
class SnapshotViewSet(viewsets.ModelViewSet):
- queryset = models.Snapshot.objects.select_related("collection",
"retention_policy")
+ queryset = models.Snapshot.objects.select_related('collection',
'retention_policy')
serializer_class = serializers.SnapshotSerializer
- parser_classes = (FileUploadParser,)
+ create_serializer_class = serializers.CreateSnapshotSerializer
+ list_serializer_class = serializers.ListSnapshotSerializer
+ pagination_class = SnapshotPagination
+ content_negotiation_class = SnapshotContentNegotiation
+ filter_backends = [filters.DjangoFilterBackend]
+ filterset_class = SnapshotFilter
+
+ def get_queryset(self):
+ collection_name = self.kwargs['collection']
+ queryset = super(SnapshotViewSet, self).get_queryset()
+
+ # For list action we need to distinguish
+ # empty response from non-existing collection
+ if self.action == 'list':
+ collection = get_object_or_404(models.Collection, name
= collection_name)
+ return queryset.filter(collection = collection)
+
+ return queryset.filter(collection__name = collection_name)
+
+ def _get_create_serializer(self, request):
+ file = getattr(request, 'data', {}).get('file')
+ retention_policy = getattr(request, 'query_params',
{}).get('retention_policy')
+
+ data = {
+ 'file': file,
+ 'retention_policy': retention_policy,
+ }
+
+ return super(SnapshotViewSet, self).get_serializer(data = data)
+
+ def get_serializer(self, *args, **kwargs):
+ if self.action == 'create':
+ return self._get_create_serializer(self.request)
+
+ return super(SnapshotViewSet, self).get_serializer(*args,
**kwargs)
+
+ def get_serializer_class(self):
+ if self.action == 'create':
+ return self.create_serializer_class
+ elif self.action == 'list':
+ return self.list_serializer_class
+
+ return super(SnapshotViewSet, self).get_serializer_class()
+
+ @extend_schema(request={'image/*': OpenApiTypes.BINARY})
+ def create(self, request, collection=None):
+ return super(SnapshotViewSet, self).create(request, collection)
+
+ def retrieve(self, request, pk=None, collection=None):
+ response = super(SnapshotViewSet, self).retrieve(request, pk =
pk, collection = collection)
+ instance = response.data.serializer.instance
+ retention_policy = instance.retention_policy
+
+ response['Link'] = '{0};
rel=alternate'.format(response.data['file'])
+ response['Date'] = http_date(instance.date.timestamp())
+ patch_response_headers(response,
cache_timeout=retention_policy.duration.total_seconds())
+
+ return response
+
+ def list(self, request, collection=None):
+ response = super(SnapshotViewSet, self).list(request,
collection = collection)
+
+ # Paginated response handled in
SnapshotPagination.get_paginated_response()
+ if not hasattr(response.data, 'serializer'):
+ return response
+
+ query_set = response.data.serializer.instance
+ instance = query_set.first()
+
+ date_now = timezone.now()
+ date = instance.date.timestamp() if instance is not None else
date_now.timestamp()
+ response['Date'] = http_date(date)
+
+ patch_cache_control(response, no_cache=True, max_age=0)
- def create(self, request):
- if 'collection' not in request.query_params:
- return Response(status=status.HTTP_400_BAD_REQUEST,
data={'detail':'collection is not specified'})
- if 'file' not in request.data:
- return Response(status=status.HTTP_400_BAD_REQUEST,
data={'detail':'file is not supplied'})
- kwargs = {}
- kwargs['file'] = request.data['file']
- kwargs['collection'] = models.Collection.objects.get(name =
request.query_params['collection'])
- if 'retention_policy' in request.query_params:
- kwargs['retention_policy'] =
models.RetentionPolicy.objects.get(name =
request.query_params['retention_policy'])
- models.Snapshot.objects.create(**kwargs)
- return Response(status=status.HTTP_201_CREATED)
- def retrieve(self, request, pk=None):
- response = super(SnapshotViewSet, self).retrieve(request, pk)
- link = []
- if response.data['next'] is not None:
- link.append("{0};
rel=next".format(response.data['next']))
- if response.data['prev'] is not None:
- link.append("{0};
rel=prev".format(response.data['prev']))
- link.append("{0}; rel=alternate".format(response.data['file']))
- if link:
- response['Link'] = ", ".join(link)
return response
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/tests/ctl/test_client.py
new/django_eremaea2-2.1.1/tests/ctl/test_client.py
--- old/django_eremaea2-2.0.22/tests/ctl/test_client.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/tests/ctl/test_client.py 2025-09-23
10:41:21.000000000 +0200
@@ -1,4 +1,5 @@
-from datetime import datetime, timedelta
+from datetime import timedelta
+from django.utils import timezone
from django.core.files.base import ContentFile
from django.contrib.auth.models import User
from django.test import LiveServerTestCase
@@ -36,7 +37,7 @@
file.name = "file.jpg"
retention_policy =
models.RetentionPolicy.objects.create(name="hourly",
duration=timedelta(hours=1))
collection = models.Collection.objects.create(name="mycol",
default_retention_policy=retention_policy)
- models.Snapshot.objects.create(collection = collection, date =
datetime.now() - timedelta(minutes=90), file=file)
+ models.Snapshot.objects.create(collection = collection, date =
timezone.now() - timedelta(minutes=90), file=file)
self.assertTrue(self.client.purge("hourly"))
snapshots = models.Snapshot.objects.all()
self.assertEqual(len(snapshots), 0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/tests/settings.py
new/django_eremaea2-2.1.1/tests/settings.py
--- old/django_eremaea2-2.0.22/tests/settings.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/tests/settings.py 2025-09-23 10:41:21.000000000
+0200
@@ -9,9 +9,15 @@
'django.contrib.auth',
'rest_framework',
'rest_framework.authtoken',
+ 'drf_spectacular',
+ 'django_filters',
'eremaea',
]
+MIDDLEWARE = [
+ 'django.middleware.http.ConditionalGetMiddleware',
+]
+
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
@@ -26,6 +32,7 @@
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
),
+ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
DATABASES = {
'default': {
@@ -48,5 +55,5 @@
MIDDLEWARE_CLASSES = []
ROOT_URLCONF = 'tests.urls'
TIME_ZONE = 'UTC'
-
-DEFAULT_AUTO_FIELD='django.db.models.AutoField'
+USE_TZ = True
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/tests/test_collection.py
new/django_eremaea2-2.1.1/tests/test_collection.py
--- old/django_eremaea2-2.0.22/tests/test_collection.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/tests/test_collection.py 2025-09-23
10:41:21.000000000 +0200
@@ -2,9 +2,11 @@
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
+from django.core.files.base import ContentFile
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
+from rest_framework.utils.urls import replace_query_param
from eremaea import models
from datetime import timedelta
from urllib.parse import urlparse
@@ -12,51 +14,79 @@
class CollectionTest(TestCase):
def setUp(self):
self.client = APIClient()
- self.retention =
models.RetentionPolicy.objects.create(name="daily", duration=timedelta(days=1))
- def tearDown(self):
- pass
-
+
def assertEqualUrl(self, x, y):
path_x = urlparse(x).path
path_y = urlparse(y).path
return self.assertEqual(path_x, path_y)
- def test_collection_create1(self):
+
+ def test_collection_create(self):
+ url = reverse('collection-list')
+ retention = 'daily'
+
+ response = self.client.post(url, {'name': 'collection',
'default_retention_policy': retention}, format='json')
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertIn('Location', response)
+ self.assertIn(reverse('collection-detail', args =
('collection',)), response['Location'])
+ collection = models.Collection.objects.get(name='collection')
+ self.assertEqual(collection.default_retention_policy,
models.RetentionPolicy.objects.get(name='daily'))
+
+ def test_collection_create_duplicate(self):
url = reverse('collection-list')
- retention_url = reverse('retention_policy-detail',
args=('daily',))
- response = self.client.post(url, {'name': 'name1',
'default_retention_policy': retention_url}, format='json')
+ retention = 'daily'
+
+ response = self.client.post(url, {'name': 'collection',
'default_retention_policy': retention}, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- response = self.client.post(url, {'name': 'name1',
'default_retention_policy': retention_url}, format='json')
+
+ response = self.client.post(url, {'name': 'collection',
'default_retention_policy': retention}, format='json')
+
self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
- group1 = models.Collection.objects.get(name="name1")
- self.assertEqual(group1.default_retention_policy,
self.retention)
- url = reverse('collection-detail', args=['name1'])
- response = self.client.get(url, format='json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- def test_collection_get1(self):
- collection =
models.Collection.objects.create(name="collection",
default_retention_policy=self.retention)
- snapshots = []
- for i in range(0,3):
-
snapshots.append(models.Snapshot.objects.create(collection = collection))
- url = reverse('collection-detail', args=('collection',))
- response = self.client.get(url)
- self.assertEqualUrl(response.data['begin'],
reverse("snapshot-detail", args=(snapshots[0].id,)))
- self.assertEqualUrl(response.data['end'],
reverse("snapshot-detail", args=(snapshots[2].id,)))
- def test_collection_head1(self):
- collection =
models.Collection.objects.create(name="collection",
default_retention_policy=self.retention)
- snapshots = []
- for i in range(0,3):
-
snapshots.append(models.Snapshot.objects.create(collection = collection))
+
+ def test_collection_update(self):
+ retention_policy =
models.RetentionPolicy.objects.get(name='daily')
+ retention_alternative = 'weekly'
+ collection =
models.Collection.objects.create(name='collection',
default_retention_policy=retention_policy)
+
url = reverse('collection-detail', args=('collection',))
- response = self.client.head(url)
+ response = self.client.put(url, {
+ 'name': 'alternative',
+ 'default_retention_policy': retention_alternative
+ }, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqualUrl(response.data['begin'],
reverse("snapshot-detail", args=(snapshots[0].id,)))
- self.assertEqualUrl(response.data['end'],
reverse("snapshot-detail", args=(snapshots[2].id,)))
- def test_collection_delete1(self):
- group1 = models.Collection.objects.create(name="delete1",
default_retention_policy=self.retention)
- url = reverse('collection-detail', args=['delete1'])
+
+ collection.refresh_from_db()
+ self.assertEqual(collection.name, 'alternative')
+ self.assertEqual(collection.default_retention_policy,
models.RetentionPolicy.objects.get(name='weekly'))
+
+ def test_collection_delete(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+
+ retention_policy =
models.RetentionPolicy.objects.get(name='daily')
+ collection =
models.Collection.objects.create(name='collection',
default_retention_policy=retention_policy)
+ snapshot = models.Snapshot.objects.create(collection =
collection, file = file)
+ storage = snapshot.file.storage
+ filepath = snapshot.file.name
+
+ url = reverse('collection-detail', args=(collection.name,))
response = self.client.delete(url)
self.assertEqual(response.status_code,
status.HTTP_204_NO_CONTENT)
+ self.assertRaises(models.Collection.DoesNotExist,
models.Collection.objects.get, name='collection')
+ self.assertRaises(models.Snapshot.DoesNotExist,
models.Snapshot.objects.get, pk=snapshot.id)
+ # Known issue:
+ # self.assertFalse(storage.exists(filepath))
+
response = self.client.delete(url)
self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
- self.assertRaises(models.Collection.DoesNotExist,
models.Collection.objects.get, name="delete1")
+ def test_collection_filter_by_retention_policy(self):
+ daily = models.RetentionPolicy.objects.get(name='daily')
+ weekly = models.RetentionPolicy.objects.get(name='weekly')
+ collection =
models.Collection.objects.create(name='collection',
default_retention_policy=daily)
+ alternative =
models.Collection.objects.create(name='alternative',
default_retention_policy=weekly)
+
+ url = reverse('collection-list')
+ url = replace_query_param(url, 'default_retention_policy',
'daily')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/tests/test_retention.py
new/django_eremaea2-2.1.1/tests/test_retention.py
--- old/django_eremaea2-2.0.22/tests/test_retention.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/tests/test_retention.py 2025-09-23
10:41:21.000000000 +0200
@@ -7,66 +7,124 @@
from rest_framework import status
from rest_framework.test import APIClient
from eremaea import models
-from datetime import datetime,timedelta
+from datetime import timedelta
+from django.utils import timezone
class RetentionPolicyTest(TestCase):
def setUp(self):
self.client = APIClient()
- def test_retention_create1(self):
- url = reverse('retention_policy-list')
- response = self.client.post(url, {'name': 'daily', 'duration':
'01 00'}, format='json')
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- response = self.client.post(url, {'name': 'daily', 'duration':
'02 00'}, format='json')
- self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
- group1 = models.RetentionPolicy.objects.get(name="daily")
- self.assertEqual(group1.duration, timedelta(days=1))
+ def test_retention_get_daily(self):
url = reverse('retention_policy-detail', args=['daily'])
response = self.client.get(url, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['duration'], '1 00:00:00')
+
+ def test_retention_get_weekly(self):
+ url = reverse('retention_policy-detail', args=['weekly'])
+ response = self.client.get(url, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['duration'], '7 00:00:00')
+
+ def test_retention_get_monthly(self):
+ url = reverse('retention_policy-detail', args=['monthly'])
+ response = self.client.get(url, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['duration'], '30 00:00:00')
+
+ def test_retention_get_annual(self):
+ url = reverse('retention_policy-detail', args=['annual'])
+ response = self.client.get(url, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['duration'], '365 00:00:00')
+
+ def test_retention_create_biweekly(self):
+ url = reverse('retention_policy-list')
+
+ response = self.client.post(url, {
+ 'name': 'biweekly',
+ 'duration': '14 00'
+ }, format='json')
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertIn('Location', response)
+ self.assertIn(reverse('retention_policy-detail', args =
('biweekly',)), response['Location'])
+
+ biweekly = models.RetentionPolicy.objects.get(name='biweekly')
+ self.assertEqual(biweekly.duration, timedelta(days=14))
+
+ def test_retention_create_duplicate(self):
+ url = reverse('retention_policy-list')
+
+ response = self.client.post(url, {
+ 'name': 'weekly',
+ 'duration': '7 00'
+ }, format='json')
+ self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
+
+ def test_retention_update_weekly(self):
+ weekly = models.RetentionPolicy.objects.get(name='weekly')
+ url = reverse('retention_policy-detail', args=['weekly'])
+
+ response = self.client.put(url, {
+ 'name': 'biweekly',
+ 'duration': '14 00'
+ }, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
- def test_retention_delete1(self):
- group1 = models.RetentionPolicy.objects.create(id=123,
name="test", duration=timedelta(days=1))
- url = reverse('retention_policy-detail', args=['test'])
+
+ weekly.refresh_from_db()
+ self.assertEqual(weekly.name, 'biweekly')
+ self.assertEqual(weekly.duration, timedelta(days=14))
+
+ def test_retention_delete_biweekly(self):
+ biweekly =
models.RetentionPolicy.objects.create(name='biweekly',
duration=timedelta(days=14))
+
+ url = reverse('retention_policy-detail', args=['biweekly'])
response = self.client.delete(url)
self.assertEqual(response.status_code,
status.HTTP_204_NO_CONTENT)
+
response = self.client.delete(url)
self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
- self.assertRaises(models.RetentionPolicy.DoesNotExist,
models.RetentionPolicy.objects.get, name="test")
- def test_retention_delete2(self):
+ self.assertRaises(models.RetentionPolicy.DoesNotExist,
models.RetentionPolicy.objects.get, name='biweekly')
+
+ def test_retention_delete_protected_by_collection(self):
# Retention deletion is protected by collection
- retention = models.RetentionPolicy.objects.create(name='daily',
duration=timedelta(days=1))
- collection = models.Collection.objects.create(name='test',
default_retention_policy=retention)
+ retention, created =
models.RetentionPolicy.objects.get_or_create(name='daily',
duration=timedelta(days=1))
+ collection =
models.Collection.objects.create(name='collection',
default_retention_policy=retention)
+
url = reverse('retention_policy-detail', args=['daily'])
response = self.client.delete(url)
self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
- def test_retention_delete3(self):
+
+ def test_retention_delete_protected_by_snapshot(self):
# Retention deletion is protected by snapshot
- retention_daily =
models.RetentionPolicy.objects.create(name='daily', duration=timedelta(days=1))
- retention_hourly =
models.RetentionPolicy.objects.create(name='hourly',
duration=timedelta(hours=1))
- collection = models.Collection.objects.create(name='test',
default_retention_policy=retention_daily)
+ retention_daily, created =
models.RetentionPolicy.objects.get_or_create(name='daily',
duration=timedelta(days=1))
+ retention_hourly, created =
models.RetentionPolicy.objects.get_or_create(name='hourly',
duration=timedelta(hours=1))
+ collection =
models.Collection.objects.create(name='collection',
default_retention_policy=retention_daily)
snapshot = models.Snapshot.objects.create(collection =
collection, retention_policy=retention_hourly)
+
url = reverse('retention_policy-detail', args=['hourly'])
response = self.client.delete(url)
self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
- def test_retention_purge1(self):
- file = ContentFile(b"123")
- file.name = "file.jpg"
- retention_daily =
models.RetentionPolicy.objects.create(name='daily', duration=timedelta(days=1))
- retention_hourly =
models.RetentionPolicy.objects.create(name='hourly',
duration=timedelta(hours=1))
- collection = models.Collection.objects.create(name='test',
default_retention_policy=retention_hourly)
- dates = [datetime.now(), datetime.now() -
timedelta(minutes=30), datetime.now() - timedelta(minutes=90)]
+
+ def test_retention_purge(self):
+ retention_daily, created =
models.RetentionPolicy.objects.get_or_create(name='daily',
duration=timedelta(days=1))
+ retention_hourly, created =
models.RetentionPolicy.objects.get_or_create(name='hourly',
duration=timedelta(hours=1))
+ collection =
models.Collection.objects.create(name='collection',
default_retention_policy=retention_hourly)
+
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ dates = [timezone.now(), timezone.now() -
timedelta(minutes=30), timezone.now() - timedelta(minutes=90)]
snapshots = [models.Snapshot.objects.create(collection =
collection, date = x, file = file) for x in dates]
snapshots.append(models.Snapshot.objects.create(collection =
collection, date = dates[-1], retention_policy = retention_daily, file = file))
storage2, filepath2 = snapshots[2].file.storage,
snapshots[2].file.name
+
url = reverse('retention_policy-purge', args=['hourly'])
response = self.client.post(url)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- snapshots2 = models.Snapshot.objects.all()
- self.assertEqual(len(snapshots2), 3)
- self.assertEqual(snapshots2[0], snapshots[0])
- self.assertEqual(snapshots2[1], snapshots[1])
- self.assertEqual(snapshots2[2], snapshots[3])
+
+ snapshots_expected = snapshots[:2] + snapshots[3:]
+ snapshots_actual = list(models.Snapshot.objects.all())
+ self.assertListEqual(snapshots_actual, snapshots_expected)
self.assertFalse(storage2.exists(filepath2))
def test_retention_purge2(self):
url = reverse('retention_policy-purge', args=['not_exists'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django_eremaea2-2.0.22/tests/test_snapshot.py
new/django_eremaea2-2.1.1/tests/test_snapshot.py
--- old/django_eremaea2-2.0.22/tests/test_snapshot.py 2024-10-03
15:53:20.000000000 +0200
+++ new/django_eremaea2-2.1.1/tests/test_snapshot.py 2025-09-23
10:41:21.000000000 +0200
@@ -1,154 +1,428 @@
+from django.conf import settings
from django.core.files.base import ContentFile
-from django.test import TestCase
+from django.test import TestCase, override_settings
+from django.utils import timezone
from rest_framework import status
from rest_framework.reverse import reverse
from rest_framework.test import APIClient
+from rest_framework.utils.urls import replace_query_param
from mimetypes import guess_all_extensions
from os.path import splitext
from eremaea import models
-from datetime import timedelta
+from datetime import datetime, timedelta
from urllib.parse import urlparse
-class SnapshotTest(TestCase):
+
+class SnapshotTestBase(TestCase):
+ __test__ = False
+
def setUp(self):
self.client = APIClient()
- self.retention =
models.RetentionPolicy.objects.create(name="daily", duration=timedelta(days=1))
- self.collection =
models.Collection.objects.create(name="mycol",
default_retention_policy=self.retention)
+ self.retention =
models.RetentionPolicy.objects.get(name='daily')
+ self.collection =
models.Collection.objects.create(name='collection', default_retention_policy =
self.retention)
+
def tearDown(self):
self.collection.delete()
- self.retention.delete()
def assertEqualUrl(self, x, y):
path_x = urlparse(x).path
path_y = urlparse(y).path
return self.assertEqual(path_x, path_y)
- def test_snapshot_create1(self):
- content = b'123'
- url = reverse('snapshot-list')
+
+ @staticmethod
+ def make_datetime(*args, **kwargs):
+ obj = datetime(*args, **kwargs)
+
+ if settings.USE_TZ:
+ default_timezone = timezone.get_default_timezone()
+ obj = timezone.make_aware(obj, default_timezone)
+
+ return obj
+
+ def test_snapshot_create_in_not_existing_collection(self):
+ content = b'test'
+
+ url = reverse('snapshot-list', kwargs = {'collection':
'not_exists'})
response = self.client.post(url, content,
content_type='image/jpeg', HTTP_CONTENT_DISPOSITION='attachment;
filename=upload.jpg')
- self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
- def test_snapshot_create2(self):
- content = b'123'
- url = reverse('snapshot-list') + '?collection=mycol'
+ self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
+
+ def test_snapshot_create(self):
+ content = b'test'
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
response = self.client.post(url, content,
content_type='image/jpeg', HTTP_CONTENT_DISPOSITION='attachment;
filename=upload.jpg')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
snapshot = models.Snapshot.objects.all()[0]
self.assertEqual(snapshot.retention_policy, self.retention)
self.assertEqual(snapshot.file.read(), content)
- def test_snapshot_create3(self):
- content = b'123'
- retention_hourly =
models.RetentionPolicy.objects.create(name="hourly",
duration=timedelta(hours=1))
- url = reverse('snapshot-list') +
'?collection=mycol&retention_policy=hourly'
+ self.assertIn('Location', response)
+ self.assertIn(reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id
+ }), response['Location'])
+
+ def test_snapshot_create_with_retention_policy(self):
+ content = b'test'
+ retention_policy =
models.RetentionPolicy.objects.get(name='weekly')
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+ url = replace_query_param(url, 'retention_policy',
retention_policy.name)
response = self.client.post(url, content,
content_type='image/jpeg', HTTP_CONTENT_DISPOSITION='attachment;
filename=upload.jpg')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
snapshot = models.Snapshot.objects.all()[0]
- self.assertEqual(snapshot.retention_policy, retention_hourly)
+ self.assertEqual(snapshot.retention_policy, retention_policy)
self.assertEqual(snapshot.file.read(), content)
- def test_snapshot_create4(self):
+
+ def test_snapshot_create_with_not_existing_retention_policy(self):
+ content = b'test'
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+ url = replace_query_param(url, 'retention_policy', 'not_exists')
+ response = self.client.post(url, content,
content_type='image/jpeg', HTTP_CONTENT_DISPOSITION='attachment;
filename=upload.jpg')
+ self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
+
+ def test_snapshot_create_with_empty_payload(self):
content = b''
- url = reverse('snapshot-list') + '?collection=mycol'
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
response = self.client.post(url, content,
content_type='image/jpeg', HTTP_CONTENT_DISPOSITION='attachment;
filename=upload.jpg')
self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
- def test_snapshot_create5(self):
- url = reverse('snapshot-list') + '?collection=mycol'
+
+ def test_snapshot_create_guess_by_content_type(self):
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+
response = self.client.post(url, {}, content_type='image/jpeg',
HTTP_CONTENT_DISPOSITION='attachment; filename=upload.png')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
snapshot = models.Snapshot.objects.all()[0]
self.assertIn(splitext(snapshot.file.name)[1],
guess_all_extensions('image/jpeg'))
- def test_snapshot_create6(self):
- url = reverse('snapshot-list') + '?collection=mycol'
+
+ def test_snapshot_create_guess_by_content_type_ext(self):
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+
response = self.client.post(url, {}, content_type='image/jpeg',
HTTP_CONTENT_DISPOSITION='attachment; filename=upload.jpg')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
snapshot = models.Snapshot.objects.all()[0]
- self.assertEqual(splitext(snapshot.file.name)[1], ".jpg")
- def test_snapshot_create7(self):
- url = reverse('snapshot-list') + '?collection=mycol'
+ self.assertEqual(splitext(snapshot.file.name)[1], '.jpg')
+
+ def test_snapshot_create_guess_by_filename(self):
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+
response = self.client.post(url, {},
HTTP_CONTENT_DISPOSITION='attachment; filename=upload.png')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
snapshot = models.Snapshot.objects.all()[0]
- self.assertEqual(splitext(snapshot.file.name)[1], ".png")
- def test_snapshot_create8(self):
+ self.assertEqual(splitext(snapshot.file.name)[1], '.png')
+
+ def test_snapshot_create_guess_by_content(self):
content =
b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0b\x49\x44\x41\x54\x78\x9c\x63\x62\x60\x00\x00\x00\x09\x00\x03\x19\x11\xd9\xe4\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82'
- url = reverse('snapshot-list') + '?collection=mycol'
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
response = self.client.post(url, content,
content_type='application/x-null', HTTP_CONTENT_DISPOSITION='attachment;
filename=upload')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
snapshot = models.Snapshot.objects.all()[0]
- self.assertEqual(splitext(snapshot.file.name)[1], ".png")
- def test_snapshot_get1(self):
- file = ContentFile(b"123")
- file.name = "file.jpg"
+ self.assertEqual(splitext(snapshot.file.name)[1], '.png')
+
+ def test_snapshot_get(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
- url = reverse('snapshot-detail', args=[snapshot.id])
- response = self.client.get(url)
- self.assertIsNone(response.data['next'])
- self.assertIsNone(response.data['prev'])
- def test_snapshot_get2(self):
- file = ContentFile(b"123")
- file.name = "file.jpg"
- snapshot1 = models.Snapshot.objects.create(collection =
self.collection, file = file)
- snapshot2 = models.Snapshot.objects.create(collection =
self.collection, file = file)
- snapshot3 = models.Snapshot.objects.create(collection =
self.collection, file = file)
- url = reverse('snapshot-detail', args=[snapshot1.id])
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
response = self.client.get(url)
- self.assertEqualUrl(response.data['next'],
reverse("snapshot-detail", args=(snapshot2.id,)))
- self.assertIsNone(response.data['prev'])
- url = reverse('snapshot-detail', args=[snapshot2.id])
- response = self.client.get(url)
- self.assertEqualUrl(response.data['next'],
reverse("snapshot-detail", args=(snapshot3.id,)))
- self.assertEqualUrl(response.data['prev'],
reverse("snapshot-detail", args=(snapshot1.id,)))
- url = reverse('snapshot-detail', args=[snapshot3.id])
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response['Link'], '{};
rel=alternate'.format(response.data['file']))
+ self.assertIn('Date', response)
+ self.assertIn('Expires', response)
+
+ def test_snapshot_get_from_not_existing_collection(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': 'not_exists',
+ 'pk': snapshot.id})
response = self.client.get(url)
- self.assertIsNone(response.data['next'])
- self.assertEqualUrl(response.data['prev'],
reverse("snapshot-detail", args=(snapshot2.id,)))
- def test_snapshot_get3(self):
- file = ContentFile(b"123")
- file.name = "file.jpg"
- retention_hourly =
models.RetentionPolicy.objects.create(name="hourly",
duration=timedelta(hours=1))
- collection1 =
models.Collection.objects.create(name="collection1",
default_retention_policy=retention_hourly)
- collection2 =
models.Collection.objects.create(name="collection2",
default_retention_policy=retention_hourly)
- snapshots1 = []
- snapshots2 = []
- for i in range(0,3):
-
snapshots1.append(models.Snapshot.objects.create(collection = collection1, file
= file))
-
snapshots2.append(models.Snapshot.objects.create(collection = collection2, file
= file))
- # first collection
- self.assertEqual(snapshots1[0].get_next(), snapshots1[1])
- self.assertEqual(snapshots1[1].get_next(), snapshots1[2])
- self.assertEqual(snapshots1[1].get_previous(), snapshots1[0])
- self.assertEqual(snapshots1[2].get_previous(), snapshots1[1])
- self.assertEqual(collection1.get_earliest(), snapshots1[0])
- self.assertEqual(collection1.get_latest(), snapshots1[2])
- # second collection
- self.assertEqual(snapshots2[0].get_next(), snapshots2[1])
- self.assertEqual(snapshots2[1].get_next(), snapshots2[2])
- self.assertEqual(snapshots2[1].get_previous(), snapshots2[0])
- self.assertEqual(snapshots2[2].get_previous(), snapshots2[1])
- self.assertEqual(collection2.get_earliest(), snapshots2[0])
- self.assertEqual(collection2.get_latest(), snapshots2[2])
- def test_snapshot_get4(self):
- file = ContentFile(b"123")
- file.name = "file.jpg"
+ self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
+
+ def test_snapshot_get_from_wrong_collection(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
- url = reverse('snapshot-detail', args=[snapshot.id])
+ collection =
models.Collection.objects.create(name='alternative', default_retention_policy =
self.retention)
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': collection.name,
+ 'pk': snapshot.id})
response = self.client.get(url)
+ self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
+
+ def test_snapshot_head(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
+ response = self.client.head(url)
link_hdr = response['Link']
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(link_hdr, '{};
rel=alternate'.format(response.data['file']))
- def test_snapshot_head1(self):
- file = ContentFile(b"123")
- file.name = "file.jpg"
+
+ def test_shapshot_update_retention_policy(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
- url = reverse('snapshot-detail', args=[snapshot.id])
- response = self.client.head(url)
+
+ retention = 'weekly'
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
+ response = self.client.patch(url, {
+ 'retention_policy': retention
+ }, format='json')
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIsNone(response.data['next'])
- self.assertIsNone(response.data['prev'])
- def test_snapshot_delete1(self):
- file = ContentFile(b"123")
- file.name = "file.jpg"
+ snapshot.refresh_from_db()
+ self.assertEqual(snapshot.retention_policy,
models.RetentionPolicy.objects.get(name='weekly'))
+
+ def test_shapshot_update_retention_policy_to_non_existing(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
+ response = self.client.patch(url, {
+ 'retention_policy': 'not_exists'
+ }, format='json')
+
+ self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
+
+ def test_shapshot_update_collection(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
+
+ collection =
models.Collection.objects.create(name='alternative', default_retention_policy =
self.retention)
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
+ response = self.client.patch(url, {
+ 'collection': collection.name
+ }, format='json')
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ snapshot.refresh_from_db()
+ self.assertEqual(snapshot.collection, collection)
+
+ def test_shapshot_update_collection_to_non_existing(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
+ response = self.client.patch(url, {
+ 'collection': 'not_exists'
+ }, format='json')
+
+ self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
+
+ def test_shapshot_update_file(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
+ storage = snapshot.file.storage
+ filepath = snapshot.file.name
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
+ response = self.client.patch(url, {
+ 'file': 'not_exists'
+ }, format='json')
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertTrue(storage.exists(filepath))
+
+ def test_snapshot_delete(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
storage = snapshot.file.storage
filepath = snapshot.file.name
- url = reverse('snapshot-detail', args=[snapshot.id])
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': self.collection.name,
+ 'pk': snapshot.id})
response = self.client.delete(url)
self.assertEqual(response.status_code,
status.HTTP_204_NO_CONTENT)
self.assertFalse(storage.exists(filepath))
+
+ def test_snapshot_delete_from_wrong_collection(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot = models.Snapshot.objects.create(collection =
self.collection, file = file)
+ storage = snapshot.file.storage
+ filepath = snapshot.file.name
+ collection =
models.Collection.objects.create(name='alternative', default_retention_policy =
self.retention)
+
+ url = reverse('snapshot-detail', kwargs = {
+ 'collection': collection.name,
+ 'pk': snapshot.id})
+ response = self.client.delete(url)
+ self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
+
+ snapshot.refresh_from_db()
+ self.assertIsNotNone(snapshot.id)
+
+ def test_snapshot_list(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot1 = models.Snapshot.objects.create(collection =
self.collection, file = file)
+ snapshot2 = models.Snapshot.objects.create(collection =
self.collection, file = file)
+ snapshot3 = models.Snapshot.objects.create(collection =
self.collection, file = file)
+ url = reverse('snapshot-list', kwargs = {
+ 'collection': self.collection.name
+ })
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 3)
+ self.assertIn('Date', response)
+ self.assertNotIn('Expires', response)
+ self.assertIn('no-cache', response["Cache-Control"])
+
+ def test_snapshot_list_from_not_existing_collection(self):
+ url = reverse('snapshot-list', kwargs = {'collection':
'not_exists'})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
+
+ def test_snapshot_list_from_empty_collection(self):
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_snapshot_filter_by_retention_policy(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ daily = models.RetentionPolicy.objects.get(name='daily')
+ weekly = models.RetentionPolicy.objects.get(name='weekly')
+ snapshot1 = models.Snapshot.objects.create(collection =
self.collection, file = file, retention_policy = daily)
+ snapshot2 = models.Snapshot.objects.create(collection =
self.collection, file = file, retention_policy = weekly)
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+ url = replace_query_param(url, 'retention_policy', 'daily')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+ self.assertIn('Date', response)
+ self.assertNotIn('Expires', response)
+ self.assertIn('no-cache', response["Cache-Control"])
+
+ def test_snapshot_filter_by_date_range(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ snapshot1 = models.Snapshot.objects.create(collection =
self.collection,
+ file = file, date = self.make_datetime(2001, 1, 1))
+ snapshot2 = models.Snapshot.objects.create(collection =
self.collection,
+ file = file, date = self.make_datetime(2001, 1, 3))
+ snapshot3 = models.Snapshot.objects.create(collection =
self.collection,
+ file = file, date = self.make_datetime(2001, 1, 5))
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+ url = replace_query_param(url, 'date_after',
'2001-01-02T00:00:00')
+ url = replace_query_param(url, 'date_before',
'2001-01-03T00:00:00')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+ self.assertIn('Date', response)
+ self.assertNotIn('Expires', response)
+ self.assertIn('no-cache', response["Cache-Control"])
+
+ def test_snapshot_list_pagination(self):
+ file = ContentFile(b'123')
+ file.name = 'file.jpg'
+ date1 = self.make_datetime(2001, 1, 1)
+ date2 = self.make_datetime(2001, 1, 2)
+ snapshot1 = models.Snapshot.objects.create(collection =
self.collection, file = file, date = date1)
+ snapshot2 = models.Snapshot.objects.create(collection =
self.collection, file = file, date = date1)
+ snapshot2 = models.Snapshot.objects.create(collection =
self.collection, file = file, date = date2)
+
+ url = reverse('snapshot-list', kwargs = {'collection':
self.collection.name})
+ url = replace_query_param(url, 'page_size', 1)
+
+ # First item
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('next', response.data)
+ self.assertIsNone(response.data['previous'])
+ self.assertEqual(len(response.data['results']), 1)
+ self.assertIn('Date', response)
+ self.assertNotIn('Expires', response)
+ self.assertIn('no-cache', response["Cache-Control"])
+ url = response.data['next']
+
+ # Second item
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('next', response.data)
+ self.assertIn('previous', response.data)
+ self.assertEqual(len(response.data['results']), 1)
+ self.assertIn('Date', response)
+ self.assertIn('Expires', response)
+ self.assertNotIn('Cache-Control', response)
+ url = response.data['next']
+
+ # Third item
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIsNone(response.data['next'])
+ self.assertIn('previous', response.data)
+ self.assertEqual(len(response.data['results']), 1)
+ self.assertIn('Date', response)
+ self.assertIn('Expires', response)
+ self.assertNotIn('Cache-Control', response)
+ url = response.data['previous']
+
+ # Second item in reverse
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('next', response.data)
+ self.assertIn('previous', response.data)
+ self.assertEqual(len(response.data['results']), 1)
+ self.assertIn('Date', response)
+ self.assertIn('Expires', response)
+ self.assertNotIn('Cache-Control', response)
+ url = response.data['previous']
+
+ # First item in reverse
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('next', response.data)
+ self.assertIsNone(response.data['previous'])
+ self.assertEqual(len(response.data['results']), 1)
+ self.assertIn('Date', response)
+ self.assertNotIn('Expires', response)
+ self.assertIn('no-cache', response['Cache-Control'])
+ url = response.data['previous']
+
+
+@override_settings(USE_TZ=False)
+class SnapshotTestNoTZ(SnapshotTestBase):
+ __test__ = True
+
+@override_settings(USE_TZ=True)
+class SnapshotTestWithTZ(SnapshotTestBase):
+ __test__ = True