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
--- 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'])
+