From 01b17b21504a05926d77eb063ae4ad7873772d96 Mon Sep 17 00:00:00 2001 From: "Matwey V. Kornilov" Date: Sun, 9 Aug 2020 15:07:15 +0300 Subject: [PATCH 1/3] Use oid as a primary key for Object --- akb/migrations/0001_initial.py | 19 ++++++++++------ akb/migrations/0002_object_simbadid.py | 18 --------------- akb/migrations/0003_tag_priority.py | 26 ---------------------- akb/migrations/0004_tag_priority_unique.py | 18 --------------- akb/migrations/0005_tag_description.py | 18 --------------- akb/models.py | 7 +----- akb/serializers.py | 2 +- tests/object.py | 4 ++-- 8 files changed, 16 insertions(+), 96 deletions(-) delete mode 100644 akb/migrations/0002_object_simbadid.py delete mode 100644 akb/migrations/0003_tag_priority.py delete mode 100644 akb/migrations/0004_tag_priority_unique.py delete mode 100644 akb/migrations/0005_tag_description.py diff --git a/akb/migrations/0001_initial.py b/akb/migrations/0001_initial.py index 7b7decb..6a6d731 100644 --- a/akb/migrations/0001_initial.py +++ b/akb/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.12 on 2020-07-07 14:12 +# Generated by Django 2.2.12 on 2020-08-09 12:03 from django.db import migrations, models @@ -14,9 +14,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Object', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('oid', models.BigIntegerField(unique=True)), + ('oid', models.BigIntegerField(primary_key=True, serialize=False)), ('description', models.TextField(blank=True)), + ('simbadid', models.CharField(blank=True, max_length=256)), ], ), migrations.CreateModel( @@ -24,19 +24,24 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.SlugField(max_length=256, unique=True)), + ('priority', models.IntegerField()), + ('description', models.TextField(blank=True)), ], + options={ + 'ordering': ['priority'], + }, ), migrations.AddIndex( model_name='tag', index=models.Index(fields=['name'], name='akb_tag_name_32e7bd_idx'), ), + migrations.AddIndex( + model_name='tag', + index=models.Index(fields=['priority'], name='akb_tag_priorit_361b7a_idx'), + ), migrations.AddField( model_name='object', name='tags', field=models.ManyToManyField(related_name='tagged_objects', to='akb.Tag'), ), - migrations.AddIndex( - model_name='object', - index=models.Index(fields=['oid'], name='akb_object_oid_c77fb7_idx'), - ), ] diff --git a/akb/migrations/0002_object_simbadid.py b/akb/migrations/0002_object_simbadid.py deleted file mode 100644 index 9def89a..0000000 --- a/akb/migrations/0002_object_simbadid.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.12 on 2020-07-08 08:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('akb', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='object', - name='simbadid', - field=models.CharField(blank=True, max_length=256), - ), - ] diff --git a/akb/migrations/0003_tag_priority.py b/akb/migrations/0003_tag_priority.py deleted file mode 100644 index 524ebd4..0000000 --- a/akb/migrations/0003_tag_priority.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 2.2.12 on 2020-07-20 07:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('akb', '0002_object_simbadid'), - ] - - operations = [ - migrations.AlterModelOptions( - name='tag', - options={'ordering': ['priority']}, - ), - migrations.AddField( - model_name='tag', - name='priority', - field=models.IntegerField(default=0), - ), - migrations.AddIndex( - model_name='tag', - index=models.Index(fields=['priority'], name='akb_tag_priorit_361b7a_idx'), - ), - ] diff --git a/akb/migrations/0004_tag_priority_unique.py b/akb/migrations/0004_tag_priority_unique.py deleted file mode 100644 index e62edc4..0000000 --- a/akb/migrations/0004_tag_priority_unique.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.12 on 2020-07-20 11:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('akb', '0003_tag_priority'), - ] - - operations = [ - migrations.AlterField( - model_name='tag', - name='priority', - field=models.IntegerField(unique=False), - ), - ] diff --git a/akb/migrations/0005_tag_description.py b/akb/migrations/0005_tag_description.py deleted file mode 100644 index 6036a7a..0000000 --- a/akb/migrations/0005_tag_description.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.12 on 2020-07-31 12:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('akb', '0004_tag_priority_unique'), - ] - - operations = [ - migrations.AddField( - model_name='tag', - name='description', - field=models.TextField(blank=True), - ), - ] diff --git a/akb/models.py b/akb/models.py index a9b69e5..8768f16 100644 --- a/akb/models.py +++ b/akb/models.py @@ -13,12 +13,7 @@ class Meta: ordering = ['priority'] class Object(models.Model): - oid = models.BigIntegerField(unique = True, blank = False) + oid = models.BigIntegerField(primary_key = True, blank = False) description = models.TextField(blank = True) tags = models.ManyToManyField("Tag", related_name="tagged_objects") simbadid = models.CharField(max_length = 256, blank = True) - - class Meta: - indexes = [ - models.Index(fields=['oid']), - ] diff --git a/akb/serializers.py b/akb/serializers.py index 7e93445..0791b2f 100644 --- a/akb/serializers.py +++ b/akb/serializers.py @@ -12,7 +12,7 @@ class ObjectSerializer(serializers.ModelSerializer): class Meta: model = models.Object - fields = ('id', 'oid', 'description', 'simbadid', 'tags') + fields = ('oid', 'description', 'simbadid', 'tags') class UserSerializer(serializers.ModelSerializer): class Meta: diff --git a/tests/object.py b/tests/object.py index 517b059..1c13026 100644 --- a/tests/object.py +++ b/tests/object.py @@ -19,7 +19,7 @@ def test_object_create1(self): 'oid': 695211400132640, 'description': 'text'}, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - pk = response.json()['id'] + pk = response.json()['oid'] o = Object.objects.get(pk=pk) self.assertEqual(o.oid, 695211400132640) self.assertEqual(o.description, 'text') @@ -32,7 +32,7 @@ def test_object_create2(self): 'description': 'text', 'tags': ["thetag", "othertag"]}, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - pk = response.json()['id'] + pk = response.json()['oid'] o = Object.objects.get(pk=pk) self.assertEqual(o.oid, 695211400132640) self.assertEqual(o.description, 'text') From 5f69bb8d51603c32e1611b1b21dd608755993777 Mon Sep 17 00:00:00 2001 From: "Matwey V. Kornilov" Date: Sun, 9 Aug 2020 19:23:14 +0300 Subject: [PATCH 2/3] Introduce changed_at and changed_by fields to Object --- akb/migrations/0001_initial.py | 11 ++++++++- akb/models.py | 3 +++ akb/serializers.py | 7 +++++- akb/views.py | 3 ++- tests/object.py | 41 +++++++++++++++++++++++++++++++++- 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/akb/migrations/0001_initial.py b/akb/migrations/0001_initial.py index 6a6d731..789375d 100644 --- a/akb/migrations/0001_initial.py +++ b/akb/migrations/0001_initial.py @@ -1,6 +1,8 @@ -# Generated by Django 2.2.12 on 2020-08-09 12:03 +# Generated by Django 2.2.12 on 2020-08-09 18:10 +from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -8,6 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -17,6 +20,7 @@ class Migration(migrations.Migration): ('oid', models.BigIntegerField(primary_key=True, serialize=False)), ('description', models.TextField(blank=True)), ('simbadid', models.CharField(blank=True, max_length=256)), + ('changed_at', models.DateTimeField(auto_now=True)), ], ), migrations.CreateModel( @@ -39,6 +43,11 @@ class Migration(migrations.Migration): model_name='tag', index=models.Index(fields=['priority'], name='akb_tag_priorit_361b7a_idx'), ), + migrations.AddField( + model_name='object', + name='changed_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), migrations.AddField( model_name='object', name='tags', diff --git a/akb/models.py b/akb/models.py index 8768f16..7f6af76 100644 --- a/akb/models.py +++ b/akb/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.contrib.auth.models import User class Tag(models.Model): name = models.SlugField(max_length=256, blank = False, unique = True) @@ -17,3 +18,5 @@ class Object(models.Model): description = models.TextField(blank = True) tags = models.ManyToManyField("Tag", related_name="tagged_objects") simbadid = models.CharField(max_length = 256, blank = True) + changed_at = models.DateTimeField(auto_now = True) + changed_by = models.ForeignKey(User, on_delete=models.PROTECT, blank = False) diff --git a/akb/serializers.py b/akb/serializers.py index 0791b2f..f29b7e1 100644 --- a/akb/serializers.py +++ b/akb/serializers.py @@ -9,10 +9,15 @@ class Meta: class ObjectSerializer(serializers.ModelSerializer): tags = serializers.SlugRelatedField(many=True, slug_field='name', queryset=models.Tag.objects, default=[]) + changed_by = serializers.SlugRelatedField(many=False, slug_field='username', allow_null=False, read_only=True) + + def save(self): + self.validated_data['changed_by'] = self.context['request'].user + super(ObjectSerializer, self).save() class Meta: model = models.Object - fields = ('oid', 'description', 'simbadid', 'tags') + fields = ('oid', 'description', 'simbadid', 'tags', 'changed_by', 'changed_at') class UserSerializer(serializers.ModelSerializer): class Meta: diff --git a/akb/views.py b/akb/views.py index 1db917a..9a67d75 100644 --- a/akb/views.py +++ b/akb/views.py @@ -3,7 +3,7 @@ from rest_framework.settings import api_settings from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly class TagViewSet(viewsets.ModelViewSet): queryset = models.Tag.objects.all() @@ -13,6 +13,7 @@ class TagViewSet(viewsets.ModelViewSet): class ObjectViewSet(viewsets.ModelViewSet): queryset = models.Object.objects.prefetch_related('tags').all() serializer_class = serializers.ObjectSerializer + permission_classes = [IsAuthenticatedOrReadOnly] lookup_field = 'oid' @api_view(['GET']) diff --git a/tests/object.py b/tests/object.py index 1c13026..3e963b7 100644 --- a/tests/object.py +++ b/tests/object.py @@ -23,6 +23,7 @@ def test_object_create1(self): o = Object.objects.get(pk=pk) self.assertEqual(o.oid, 695211400132640) self.assertEqual(o.description, 'text') + self.assertEqual(o.changed_by, self.user) def test_object_create2(self): tags = [Tag.objects.create(name="thetag", priority=1), Tag.objects.create(name="othertag", priority=2)] @@ -39,10 +40,48 @@ def test_object_create2(self): self.assertListEqual(list(o.tags.all()), tags) self.assertListEqual(list(tags[0].tagged_objects.all()), [o,]) self.assertListEqual(list(tags[1].tagged_objects.all()), [o,]) + self.assertEqual(o.changed_by, self.user) + + def test_object_create3(self): + wrong_user = User.objects.get_or_create(username='alice') + url = reverse('object-list') + response = self.client.post(url, { + 'oid': 695211400132640, + 'changed_by': 'alice', + 'description': 'text'}, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + pk = response.json()['oid'] + o = Object.objects.get(pk=pk) + self.assertEqual(o.oid, 695211400132640) + self.assertEqual(o.description, 'text') + self.assertEqual(o.changed_by, self.user) + + def test_object_create4(self): + url = reverse('object-list') + response = self.client.post(url, { + 'oid': 695211400132640, + 'description': 'text'}, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + o = Object.objects.get(pk=695211400132640) + self.assertEqual(o.description, 'text') + self.assertEqual(o.changed_by, self.user) + + newuser, _ = User.objects.get_or_create(username='bob') + token = Token.objects.create(user=newuser) + newclient = APIClient() + newclient.credentials(HTTP_AUTHORIZATION='Token ' + token.key) + + url = reverse('object-detail', args=[695211400132640]) + response = newclient.put(url, { + 'oid': 695211400132640, + 'description': 'new text'}, format='json') + o = Object.objects.get(pk=695211400132640) + self.assertEqual(o.description, 'new text') + self.assertEqual(o.changed_by, newuser) def test_object_lookup1(self): tags = [Tag.objects.create(name="thetag", priority=1), Tag.objects.create(name="othertag", priority=2)] - o = Object.objects.create(oid = 695211400132640, description = 'description') + o = Object.objects.create(oid = 695211400132640, description = 'description', changed_by=self.user) o.tags.set(tags) o.save() url = reverse('object-detail', args=[695211400132640]) From 3beb176b75bb9199d40d76399f15ca3ae7db8feb Mon Sep 17 00:00:00 2001 From: "Matwey V. Kornilov" Date: Sun, 9 Aug 2020 17:15:17 +0300 Subject: [PATCH 3/3] Add initial implementation for reversion support --- akb/models.py | 11 +++++++++++ akb/settings.py | 2 ++ akb/views.py | 20 ++++++++++++++++++-- requirements.txt | 1 + setup.py | 2 +- tests/object.py | 14 ++++++++++++++ 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/akb/models.py b/akb/models.py index 7f6af76..34ee5ee 100644 --- a/akb/models.py +++ b/akb/models.py @@ -1,11 +1,21 @@ from django.db import models from django.contrib.auth.models import User +import reversion + +class TagManager(models.Manager): + def get_by_natural_key(self, name): + return self.get(name = name) class Tag(models.Model): name = models.SlugField(max_length=256, blank = False, unique = True) priority = models.IntegerField(unique = False, blank = False) description = models.TextField(blank = True) + objects = TagManager() + + def natural_key(self): + return (self.name,) + class Meta: indexes = [ models.Index(fields=['name']), @@ -13,6 +23,7 @@ class Meta: ] ordering = ['priority'] +@reversion.register(use_natural_foreign_keys = True) class Object(models.Model): oid = models.BigIntegerField(primary_key = True, blank = False) description = models.TextField(blank = True) diff --git a/akb/settings.py b/akb/settings.py index aea2a65..8cf7874 100644 --- a/akb/settings.py +++ b/akb/settings.py @@ -41,6 +41,7 @@ 'django.contrib.staticfiles', 'rest_framework', 'rest_framework.authtoken', + 'reversion', 'akb', ] @@ -51,6 +52,7 @@ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'reversion.middleware.RevisionMiddleware', ] # from rest_framework.authtoken.models import Token diff --git a/akb/views.py b/akb/views.py index 9a67d75..1fc8f43 100644 --- a/akb/views.py +++ b/akb/views.py @@ -1,9 +1,12 @@ from akb import models, serializers -from rest_framework import viewsets +from django.utils.encoding import force_str +from json import JSONDecoder +from rest_framework import viewsets, status from rest_framework.settings import api_settings from rest_framework.response import Response -from rest_framework.decorators import api_view, permission_classes +from rest_framework.decorators import api_view, action, permission_classes from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from reversion.models import Version class TagViewSet(viewsets.ModelViewSet): queryset = models.Tag.objects.all() @@ -16,6 +19,19 @@ class ObjectViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticatedOrReadOnly] lookup_field = 'oid' + @action(detail=True, methods=['GET']) + def log(self, request, oid=None): + decoder = JSONDecoder() + + def decode(x): + obj = decoder.decode(force_str(x.serialized_data.encode("utf8")))[0] + return obj['fields'] + + o = self.get_object() + entries = [decode(x) for x in Version.objects.get_for_object(o)] + + return Response(entries, status=status.HTTP_200_OK) + @api_view(['GET']) @permission_classes([IsAuthenticated]) def whoami(request): diff --git a/requirements.txt b/requirements.txt index fa25eb8..375a7be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django djangorestframework +django-reversion > 3.0.7 dj-database-url diff --git a/setup.py b/setup.py index d95c7a0..4af4d3e 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ import os -import sys from setuptools import setup # allow setup.py to be run from any path @@ -29,6 +28,7 @@ 'Django', 'djangorestframework', 'dj-database-url', + 'django-reversion > 3.0.7', ] ) diff --git a/tests/object.py b/tests/object.py index 3e963b7..def0a20 100644 --- a/tests/object.py +++ b/tests/object.py @@ -5,6 +5,7 @@ from rest_framework.authtoken.models import Token from rest_framework.test import APIClient from akb.models import Object, Tag +import reversion class ObjectTest(TestCase): def setUp(self): @@ -88,3 +89,16 @@ def test_object_lookup1(self): response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["oid"], 695211400132640) + + def test_object_log1(self): + tags = [Tag.objects.create(name="thetag", priority=1), Tag.objects.create(name="othertag", priority=2)] + with reversion.create_revision(): + o = Object.objects.create(oid = 695211400132640, description = 'description', changed_by=self.user) + o.tags.set([tags[0]]) + o.save() + with reversion.create_revision(): + o.tags.set(tags) + o.save() + url = reverse('object-log', args=[695211400132640]) + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK)