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
authorymh <ymh.work@gmail.com>
Wed, 19 Jul 2017 15:57:13 +0200
changeset 119 8ff8e2aee0f9
parent 118 2cb8d11aa9ca
child 120 892980a3af09
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
src/irinotes/settings.py
src/notes/api/permissions/__init__.py
src/notes/api/permissions/core.py
src/notes/api/serializers/core.py
src/notes/api/urls.py
src/notes/api/views/__init__.py
src/notes/api/views/core.py
src/notes/migrations/0001_initial.py
src/notes/models/base.py
src/notes/tests/api/note.py
src/notes/tests/api/session.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
--- 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"]
--- 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
--- 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):
 
--- 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.
--- 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']
--- 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
--- 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')),
--- 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'))
 
--- 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="<i>example note 1,1</i>",
+            raw="<i>example note 1.1</i>",
+            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'))
--- 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'])
+