# HG changeset patch # User ymh # Date 1500472633 -7200 # Node ID 8ff8e2aee0f9d38fa838b8af0734ed09db0acaa7 # Parent 2cb8d11aa9ca9d5db1a40da42b4aece7c61bf02b add parameter to filter session and note by updated date. Add pagination on sessions and notes. add read only endpoint at root level to list notes diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/irinotes/settings.py --- a/src/irinotes/settings.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/irinotes/settings.py Wed Jul 19 15:57:13 2017 +0200 @@ -225,7 +225,8 @@ 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), - 'TEST_REQUEST_DEFAULT_FORMAT': 'json' + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + 'PAGE_SIZE': 100 } REST_USE_JWT = True diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/api/permissions/__init__.py --- a/src/notes/api/permissions/__init__.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/api/permissions/__init__.py Wed Jul 19 15:57:13 2017 +0200 @@ -1,7 +1,7 @@ """ Permissions classes fro notes """ -from .core import SessionPermission, NotePermission +from .core import SessionPermission, NotePermission, RootNotePermission from .auth import GroupPermission -__all__ = ["SessionPermission", "NotePermission", "GroupPermission"] +__all__ = ["SessionPermission", "NotePermission", "GroupPermission", "RootNotePermission"] diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/api/permissions/core.py --- a/src/notes/api/permissions/core.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/api/permissions/core.py Wed Jul 19 15:57:13 2017 +0200 @@ -35,3 +35,21 @@ else: return True +class RootNotePermission(IsAuthenticated): + """ + Permissions for notes + """ + + def has_permission(self, request, view): + """ + Return `True` if permission is granted, `False` otherwise. + """ + is_authenticated = super().has_permission(request, view) + return is_authenticated + # if not is_authenticated: + # return False + # session_ext_id = view.kwargs.get('session_ext_id') + # if is_authenticated and session_ext_id: + # return Session.objects.filter(ext_id=session_ext_id, owner=request.user).exists() + # else: + # return True diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/api/serializers/core.py --- a/src/notes/api/serializers/core.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/api/serializers/core.py Wed Jul 19 15:57:13 2017 +0200 @@ -54,6 +54,34 @@ ) read_only_fields = ('ext_id', ) +class RootListNoteSerializer(serializers.ModelSerializer): + session = serializers.SlugRelatedField(read_only=True, slug_field='ext_id') + + class Meta: + model = Note + fields = ( + 'ext_id', 'tc_start', 'tc_end', 'session' + ) + read_only_fields = ('ext_id', ) + +class RootDetailNoteSerializer(serializers.ModelSerializer): + session = serializers.SlugRelatedField(read_only=True, slug_field='ext_id') + + class Meta: + model = Note + fields = ( + 'ext_id', 'version', 'created', 'updated', + 'plain', 'html', 'raw', + 'categorization', 'margin_note', 'tc_start', 'tc_end', + 'session' + ) + read_only_fields = ( + 'ext_id', 'version', 'created', 'updated', + 'plain', 'html', 'raw', + 'categorization', 'margin_note', 'tc_start', 'tc_end', + 'session' + ) + class ListSessionSerializer(serializers.ModelSerializer): diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/api/urls.py --- a/src/notes/api/urls.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/api/urls.py Wed Jul 19 15:57:13 2017 +0200 @@ -1,12 +1,13 @@ from django.conf.urls import url, include from rest_framework_nested import routers -from .views import SessionViewSet, NoteViewSet +from .views import SessionViewSet, NoteViewSet, RootNoteViewSet router = routers.SimpleRouter() router.register(r'sessions', SessionViewSet, base_name='session') +router.register(r'notes', RootNoteViewSet, base_name='note') session_router = routers.NestedSimpleRouter(router, r'sessions', lookup='session') -session_router.register(r'notes', NoteViewSet, base_name='notes') +session_router.register(r'notes', NoteViewSet, base_name='notes-session') # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/api/views/__init__.py --- a/src/notes/api/views/__init__.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/api/views/__init__.py Wed Jul 19 15:57:13 2017 +0200 @@ -1,3 +1,3 @@ -from .core import SessionViewSet, NoteViewSet +from .core import SessionViewSet, NoteViewSet, RootNoteViewSet -__all__ = ['SessionViewSet', 'NoteViewSet'] +__all__ = ['SessionViewSet', 'NoteViewSet', 'RootNoteViewSet'] diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/api/views/core.py --- a/src/notes/api/views/core.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/api/views/core.py Wed Jul 19 15:57:13 2017 +0200 @@ -1,15 +1,19 @@ """ Core viewsets """ +import datetime import logging +from django.utils import timezone from notes.models import Note, Session from rest_framework import viewsets from ..permissions import NotePermission, SessionPermission -from ..serializers.core import (DetailNoteSerializer, UpdateNoteSerializer, DetailSessionSerializer, - CreateNoteSerializer, ListNoteSerializer, ListSessionSerializer, - CreateSessionSerializer) +from ..serializers.core import (CreateNoteSerializer, CreateSessionSerializer, + DetailNoteSerializer, DetailSessionSerializer, + ListNoteSerializer, ListSessionSerializer, + RootDetailNoteSerializer, + RootListNoteSerializer, UpdateNoteSerializer) logger = logging.getLogger(__name__) @@ -33,7 +37,15 @@ return self.serializers.get(self.action, ListSessionSerializer) def get_queryset(self): - return Session.objects.filter(owner=self.request.user) + queryset = Session.objects.filter(owner=self.request.user).order_by('created') + modified_since_str = self.request.query_params.get('modified_since', None) + if modified_since_str is not None: + modified_since = datetime.datetime.fromtimestamp( + float(modified_since_str), + timezone.utc + ) + queryset = queryset.filter(updated__gte=modified_since) + return queryset class NoteViewSet(viewsets.ModelViewSet): @@ -47,6 +59,7 @@ lookup_field = 'ext_id' permission_classes = (NotePermission,) + pagination_class = None def get_serializer_class(self): return self.serializers.get(self.action, ListNoteSerializer) @@ -55,3 +68,28 @@ return Note.objects.filter( session__ext_id=self.kwargs['session_ext_id'], session__owner=self.request.user) + + +class RootNoteViewSet(viewsets.ReadOnlyModelViewSet): + + serializers = { + 'list': RootListNoteSerializer, + 'retrieve': RootDetailNoteSerializer, + } + lookup_field = 'ext_id' + + permission_classes = (NotePermission,) + + def get_serializer_class(self): + return self.serializers.get(self.action, RootListNoteSerializer) + + def get_queryset(self): + queryset = Note.objects.filter(session__owner=self.request.user).order_by('created') + modified_since_str = self.request.query_params.get('modified_since', None) + if modified_since_str is not None: + modified_since = datetime.datetime.fromtimestamp( + float(modified_since_str), + timezone.utc + ) + queryset = queryset.filter(updated__gte=modified_since) + return queryset diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/migrations/0001_initial.py --- a/src/notes/migrations/0001_initial.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/migrations/0001_initial.py Wed Jul 19 15:57:13 2017 +0200 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.2 on 2017-07-07 09:59 +# Generated by Django 1.11.2 on 2017-07-19 13:26 from __future__ import unicode_literals import colorful.fields @@ -78,8 +78,8 @@ name='Note', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Model|created')), - ('updated', models.DateTimeField(auto_now=True, verbose_name='Model|updated')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Model|created')), + ('updated', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Model|updated')), ('ext_id', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='Model|ext_id')), ('version', concurrency.fields.AutoIncVersionField(default=1, help_text='record revision number', verbose_name='Model|version')), ('tc_start', models.DateTimeField(verbose_name='Note|tc_start')), @@ -100,8 +100,8 @@ name='Protocol', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Model|created')), - ('updated', models.DateTimeField(auto_now=True, verbose_name='Model|updated')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Model|created')), + ('updated', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Model|updated')), ('ext_id', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='Model|ext_id')), ('version', concurrency.fields.AutoIncVersionField(default=1, help_text='record revision number', verbose_name='Model|version')), ('title', models.CharField(max_length=255, verbose_name='Protocol|title')), @@ -116,8 +116,8 @@ name='Session', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Model|created')), - ('updated', models.DateTimeField(auto_now=True, verbose_name='Model|updated')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Model|created')), + ('updated', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Model|updated')), ('ext_id', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='Model|ext_id')), ('version', concurrency.fields.AutoIncVersionField(default=1, help_text='record revision number', verbose_name='Model|version')), ('date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Session|date')), diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/models/base.py --- a/src/notes/models/base.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/models/base.py Wed Jul 19 15:57:13 2017 +0200 @@ -15,8 +15,8 @@ class Model(models.Model): objects = ModelManager() - created = models.DateTimeField(auto_now_add=True, verbose_name=_('Model|created')) - updated = models.DateTimeField(auto_now=True, verbose_name=_('Model|updated')) + created = models.DateTimeField(auto_now_add=True, verbose_name=_('Model|created'), db_index=True) + updated = models.DateTimeField(auto_now=True, verbose_name=_('Model|updated'), db_index=True) ext_id = models.UUIDField(unique=True, default=uuid.uuid4, verbose_name=_('Model|ext_id')) version = AutoIncVersionField(verbose_name=_('Model|version')) diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/tests/api/note.py --- a/src/notes/tests/api/note.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/tests/api/note.py Wed Jul 19 15:57:13 2017 +0200 @@ -1,6 +1,7 @@ """ Tests the core api for sessions """ +import datetime import logging from uuid import uuid4 @@ -16,6 +17,9 @@ class NoteApiTests(APITransactionTestCase): + ''' + Test Note api + ''' def setUp(self): User = get_user_model() @@ -67,6 +71,18 @@ categorization="[]" ) + self.note2 = Note.objects.create( + tc_start=timezone.now(), + tc_end=timezone.now(), + session=self.session1, + plain="example note 1.1", + html="example note 1,1", + raw="example note 1.1", + margin_note="margin note 1.1", + categorization="[]" + ) + + Note.objects.create( tc_start=timezone.now(), tc_end=timezone.now(), @@ -79,7 +95,7 @@ ) def test_create_note_no_user(self): - url = reverse('notes:notes-list', + url = reverse('notes:notes-session-list', kwargs={'session_ext_id': self.session1.ext_id}) response = self.client.post(url, { 'tc_start': timezone.now(), @@ -94,7 +110,7 @@ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_create_note(self): - url = reverse('notes:notes-list', + url = reverse('notes:notes-session-list', kwargs={'session_ext_id': self.session1.ext_id}) self.client.login(username='test_user1', password='top_secret') response = self.client.post(url, { @@ -115,7 +131,7 @@ self.assertTrue(note) def test_create_note_with_ext_id(self): - url = reverse('notes:notes-list', + url = reverse('notes:notes-session-list', kwargs={'session_ext_id': self.session1.ext_id}) self.client.login(username='test_user1', password='top_secret') ext_id = str(uuid4()) @@ -138,7 +154,7 @@ self.assertTrue(note) def test_update_note(self): - url = reverse('notes:notes-detail', + url = reverse('notes:notes-session-detail', kwargs={'session_ext_id': self.session1.ext_id, 'ext_id': self.note1.ext_id}) self.client.login(username='test_user1', password='top_secret') response = self.client.put(url, { @@ -160,7 +176,7 @@ #TODO: Fail if a tc_start, tc_end, session, ext_id updated, created are provided on update ? # def test_update_note_tc_start(self): - # url = reverse('notes:notes-detail', + # url = reverse('notes:notes-session-detail', # kwargs={'session_ext_id': self.session1.ext_id, 'ext_id': self.note1.ext_id}) # self.client.login(username='test_user1', password='top_secret') # response = self.client.put(url, { @@ -174,3 +190,53 @@ # }, format='json') # self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_root_note_list(self): + url = reverse('notes:note-list') + self.client.login(username='test_user1', password='top_secret') + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + json_resp = response.json() + self.assertIn('results', json_resp) + self.assertEqual(2, json_resp['count']) + self.assertEqual(2, len(json_resp['results'])) + + for note_json in json_resp['results']: + self.assertEqual(str(self.session1.ext_id), note_json.get('session')) + + + def test_root_note_list_modified(self): + + nexthour = \ + datetime.datetime.utcnow().replace(tzinfo=timezone.utc) +\ + datetime.timedelta(hours=1) + Note.objects.filter(pk=self.note2.id).update(updated=nexthour) + + url = reverse('notes:note-list') + self.client.login(username='test_user1', password='top_secret') + + response = self.client.get( + url, + {'modified_since': (nexthour - datetime.timedelta(minutes=30)).timestamp()}, + format='json' + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + json_resp = response.json() + self.assertIn('results', json_resp) + self.assertEqual(1, json_resp['count']) + + self.assertEqual(len(json_resp['results']), 1, "must have one note") + self.assertEqual(str(self.session1.ext_id), json_resp['results'][0].get('session')) + + + def test_root_note_detail(self): + url = reverse('notes:note-detail', kwargs={'ext_id': self.note2.ext_id}) + self.client.login(username='test_user1', password='top_secret') + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + json_resp = response.json() + self.assertEqual(str(self.session1.ext_id), json_resp.get('session')) + self.assertEqual('example note 1.1', json_resp.get('plain')) diff -r 2cb8d11aa9ca -r 8ff8e2aee0f9 src/notes/tests/api/session.py --- a/src/notes/tests/api/session.py Tue Jul 18 17:59:28 2017 +0200 +++ b/src/notes/tests/api/session.py Wed Jul 19 15:57:13 2017 +0200 @@ -1,6 +1,7 @@ """ Tests the core api for sessions """ +import datetime import logging from uuid import uuid4 @@ -48,13 +49,20 @@ owner=user2 ) - Session.objects.create( + self.session3 = Session.objects.create( title="a new session 3", description="Description 3", protocol="[]", owner=user3 ) + self.session4 = Session.objects.create( + title="a new session 4", + description="Description 4", + protocol="[]", + owner=user3 + ) + Note.objects.create( tc_start=timezone.now(), tc_end=timezone.now(), @@ -90,8 +98,12 @@ response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) json = response.json() - self.assertEqual(len(json), 1, "must have one session") - for session in json: + self.assertIn('results', json, "must have results") + self.assertIn('count', json, "must have count") + self.assertEqual(json['count'], 1, "must have one session") + self.assertEqual(len(json['results']), 1, "must have one session") + + for session in json['results']: self.assertEqual(session['owner'], 'test_user1') @@ -144,7 +156,7 @@ self.assertEqual(response.status_code, status.HTTP_200_OK) def test_list_notes(self): - url = reverse('notes:notes-list', kwargs={'session_ext_id':str(self.session1.ext_id)}) + url = reverse('notes:notes-session-list', kwargs={'session_ext_id':str(self.session1.ext_id)}) self.client.login(username='test_user1', password='top_secret') response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -156,9 +168,68 @@ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_list_notes_bad(self): - url = reverse('notes:notes-list', kwargs={'session_ext_id':str(self.session2.ext_id)}) - logger.debug("URL: %s", url) + url = reverse('notes:notes-session-list', kwargs={'session_ext_id':str(self.session2.ext_id)}) self.client.login(username='test_user1', password='top_secret') response = self.client.get(url, format='json') - logger.debug(response.json()) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filter_modified_since(self): + url = reverse('notes:session-list') + self.client.login(username='test_user3', password='top_secret') + nexthour = \ + datetime.datetime.utcnow().replace(tzinfo=timezone.utc) +\ + datetime.timedelta(hours=1) + Session.objects.filter(pk=self.session4.id).update(updated=nexthour) + + response = self.client.get( + url, + {'modified_since': (nexthour - datetime.timedelta(minutes=30)).timestamp()}, + format='json' + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + json = response.json() + self.assertIn('results', json, "must have results") + self.assertIn('count', json, "must have count") + self.assertEqual(json['count'], 1, "must have one session") + self.assertEqual(len(json['results']), 1, "must have one session") + + self.assertEqual(json['results'][0].get('title'), "a new session 4") + + + def test_filter_modified_since_zero(self): + url = reverse('notes:session-list') + self.client.login(username='test_user3', password='top_secret') + + response = self.client.get(url, {'modified_since': 0}, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + json = response.json() + self.assertIn('results', json, "must have results") + self.assertIn('count', json, "must have count") + self.assertEqual(json['count'], 2, "must have two sessions") + self.assertEqual(len(json['results']), 2, "must have two sessions") + + for session_json in json['results']: + self.assertIn(session_json.get('title'), ['a new session 3', 'a new session 4']) + + + def test_filter_modified_seconds(self): + url = reverse('notes:session-list') + self.client.login(username='test_user3', password='top_secret') + + prevmoment = \ + datetime.datetime.utcnow().replace(tzinfo=timezone.utc) -\ + datetime.timedelta(seconds=5) + + response = self.client.get(url, {'modified_since': prevmoment.timestamp()}, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + json = response.json() + self.assertIn('results', json, "must have results") + self.assertIn('count', json, "must have count") + self.assertEqual(json['count'], 2, "must have two sessions") + self.assertEqual(len(json['results']), 2, "must have two sessions") + for session_json in json['results']: + self.assertIn(session_json.get('title'), ['a new session 3', 'a new session 4']) +