add filter on session and node list to recover specific objects
authorymh <ymh.work@gmail.com>
Tue, 25 Jul 2017 19:11:26 +0200
changeset 128 34a75bd8d0b9
parent 127 006c5270128c
child 129 d48946d164c6
add filter on session and node list to recover specific objects
src/irinotes/settings.py
src/notes/api/filters.py
src/notes/api/views/core.py
src/notes/api/views/sync.py
src/notes/middlewares.py
src/notes/tests/api/note.py
src/notes/tests/api/session.py
src/notes/tests/api/sync.py
--- a/src/irinotes/settings.py	Fri Jul 28 18:22:46 2017 +0200
+++ b/src/irinotes/settings.py	Tue Jul 25 19:11:26 2017 +0200
@@ -51,6 +51,7 @@
     'django.contrib.staticfiles',
     'django.contrib.sites',
     'django_extensions',
+    'django_filters',
     'irinotes',
     'corsheaders',
     'rest_framework',
@@ -74,6 +75,7 @@
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'notes.middlewares.JWTAuthenticationMiddleware',
     'auditlog.middleware.AuditlogMiddleware'
 ]
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/notes/api/filters.py	Tue Jul 25 19:11:26 2017 +0200
@@ -0,0 +1,24 @@
+'''
+Inspired by https://stackoverflow.com/a/36289332
+'''
+from django_filters.rest_framework import (BaseInFilter, Filter, FilterSet,
+                                           UUIDFilter)
+
+from ..models import Note, Session
+
+class ExtIdFilter(BaseInFilter, UUIDFilter):
+    pass
+
+class CoreFilterSet(FilterSet):
+    ext_id__in = ExtIdFilter(name='ext_id')
+
+class SessionFilterSet(CoreFilterSet):
+    class Meta:
+        model = Session
+        fields = ['ext_id__in']
+
+class NoteFilterSet(CoreFilterSet):
+    class Meta:
+        model = Note
+        fields = ['ext_id__in']
+
--- a/src/notes/api/views/core.py	Fri Jul 28 18:22:46 2017 +0200
+++ b/src/notes/api/views/core.py	Tue Jul 25 19:11:26 2017 +0200
@@ -5,15 +5,16 @@
 import logging
 
 from django.utils import timezone
+from django_filters.rest_framework import DjangoFilterBackend
 from notes.models import Note, Session
 from rest_framework import viewsets
 
+from ..filters import NoteFilterSet, SessionFilterSet
 from ..permissions import NotePermission, SessionPermission
 from ..serializers.core import (CreateNoteSerializer, CreateSessionSerializer,
                                 DetailNoteSerializer, DetailSessionSerializer,
                                 ListNoteSerializer, ListSessionSerializer,
-                                RootDetailNoteSerializer,
-                                RootListNoteSerializer, UpdateNoteSerializer)
+                                RootDetailNoteSerializer, UpdateNoteSerializer)
 
 logger = logging.getLogger(__name__)
 
@@ -33,6 +34,9 @@
 
     permission_classes = (SessionPermission,)
 
+    filter_backends = (DjangoFilterBackend,)
+    filter_class = SessionFilterSet
+
     def get_serializer_class(self):
         return self.serializers.get(self.action, ListSessionSerializer)
 
@@ -77,6 +81,9 @@
     permission_classes = (NotePermission,)
     serializer_class = RootDetailNoteSerializer
 
+    filter_backends = (DjangoFilterBackend,)
+    filter_class = NoteFilterSet
+
     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)
--- a/src/notes/api/views/sync.py	Fri Jul 28 18:22:46 2017 +0200
+++ b/src/notes/api/views/sync.py	Tue Jul 25 19:11:26 2017 +0200
@@ -1,4 +1,5 @@
 import datetime
+import logging
 
 from auditlog.models import LogEntry
 from django.utils import timezone
@@ -7,6 +8,7 @@
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
+logger = logging.getLogger(__name__)
 
 class ListLogsView(APIView):
     """
@@ -17,17 +19,18 @@
     """
     permission_classes = (permissions.IsAuthenticated,)
 
-    def __filter_object(self, queryset, modified_since):
-        log_entries = LogEntry.objects.get_for_objects(queryset)
+    def __filter_object(self, model, user, modified_since):
+        log_entries = LogEntry.objects.get_for_model(model).filter(actor=user)
         if modified_since:
             log_entries = log_entries.filter(timestamp__gte=modified_since)
-        return log_entries
+        return log_entries.order_by('timestamp')
 
-    def __process_log_entries(self, queryset, modified_since):
+    def __process_log_entries(self, model, user, modified_since):
         '''
         Process log entries
         '''
-        log_entries = self.__filter_object(queryset, modified_since)
+        log_entries = self.__filter_object(model, user, modified_since)
+        logger.debug("LOG ENTRies %r", list(log_entries))
 
         res = {}
         for log_entry in log_entries:
@@ -76,8 +79,8 @@
             )
 
         user = request.user
-        res_sessions = self.__process_log_entries(Session.objects.filter(owner=user), modified_since)
-        res_notes = self.__process_log_entries(Note.objects.filter(session__owner=user), modified_since)
+        res_sessions = self.__process_log_entries(Session, user, modified_since)
+        res_notes = self.__process_log_entries(Note, user, modified_since)
 
         return Response({
             'sessions': res_sessions.values(),
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/notes/middlewares.py	Tue Jul 25 19:11:26 2017 +0200
@@ -0,0 +1,45 @@
+"""
+Taken from https://gist.github.com/AndrewJHart/9bb9eaea2523cd2144cf959f48a14194
+and https://github.com/GetBlimp/django-rest-framework-jwt/issues/45#issuecomment-255383031
+"""
+from django.contrib.auth.middleware import get_user
+from django.contrib.auth.models import AnonymousUser
+from django.utils.functional import SimpleLazyObject
+from rest_framework_jwt.authentication import JSONWebTokenAuthentication
+from rest_framework import exceptions
+
+
+def get_user_jwt(request):
+    """
+    Replacement for django session auth get_user & auth.get_user for
+     JSON Web Token authentication. Inspects the token for the user_id,
+     attempts to get that user from the DB & assigns the user on the
+     request object. Otherwise it defaults to AnonymousUser.
+    This will work with existing decorators like LoginRequired, whereas
+    the standard restframework_jwt auth only works at the view level
+    forcing all authenticated users to appear as AnonymousUser ;)
+    Returns: instance of user object or AnonymousUser object
+    """
+    user = get_user(request)
+    if user.is_authenticated:
+        return user
+
+    jwt_authentication = JSONWebTokenAuthentication()
+    if jwt_authentication.get_jwt_value(request):
+        try:
+            user, _ = jwt_authentication.authenticate(request)
+        except exceptions.AuthenticationFailed:
+            user = None
+
+    return user or AnonymousUser()
+
+
+class JWTAuthenticationMiddleware(object):
+
+    def __init__(self, get_response):
+        self.get_response = get_response
+        # One-time configuration and initialization.
+
+    def __call__(self, request):
+        request.user = SimpleLazyObject(lambda: get_user_jwt(request))
+        return self.get_response(request)
--- a/src/notes/tests/api/note.py	Fri Jul 28 18:22:46 2017 +0200
+++ b/src/notes/tests/api/note.py	Tue Jul 25 19:11:26 2017 +0200
@@ -82,7 +82,6 @@
             categorization="[]"
         )
 
-
         Note.objects.create(
             tc_start=timezone.now(),
             tc_end=timezone.now(),
@@ -174,7 +173,7 @@
         self.assertTrue(note)
         self.assertEqual("example note 1 modified", note.plain)
 
-    #TODO: Fail if a tc_start, tc_end, session, ext_id updated, created are provided on update ?
+    # 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-session-detail',
     #                   kwargs={'session_ext_id': self.session1.ext_id, 'ext_id': self.note1.ext_id})
@@ -203,12 +202,64 @@
         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'))
+            self.assertIn('plain', note_json)
+            self.assertIn('html', note_json)
+            self.assertIn('raw', note_json)
+
+    def test_root_note_list_filter(self):
+        url = reverse('notes:note-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url, {'ext_id__in': ",".join(
+            [str(self.note1.ext_id), str(uuid4())])}, 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(1, len(json_resp['results']))
+
+        note_json = json_resp['results'][0]
+        self.assertEqual(str(self.note1.ext_id), note_json.get('ext_id'))
+        self.assertEqual(str(self.session1.ext_id), note_json.get('session'))
+        self.assertIn('plain', note_json)
+        self.assertIn('html', note_json)
+        self.assertIn('raw', note_json)
+
+    def test_root_note_list_filter_both(self):
+        url = reverse('notes:note-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url, {'ext_id__in': ",".join(
+            [str(self.note1.ext_id), str(self.note2.ext_id)])}, 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.assertIn(note_json.get('ext_id'), [str(self.note1.ext_id), str(self.note2.ext_id)])
             self.assertEqual(str(self.session1.ext_id), note_json.get('session'))
             self.assertIn('plain', note_json)
             self.assertIn('html', note_json)
             self.assertIn('raw', note_json)
 
 
+    def test_root_note_list_bad_filter(self):
+        url = reverse('notes:note-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url, {'ext_id__in': ",".join(
+            [str(self.note1.ext_id), "foo"])}, format='json')
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+        json_resp = response.json()
+        self.assertIn('results', json_resp)
+        self.assertEqual(0, json_resp['count'])
+        self.assertEqual(0, len(json_resp['results']))
+
+
     def test_root_note_list_modified(self):
 
         nexthour = \
@@ -221,7 +272,8 @@
 
         response = self.client.get(
             url,
-            {'modified_since': (nexthour - datetime.timedelta(minutes=30)).timestamp()},
+            {'modified_since': (
+                nexthour - datetime.timedelta(minutes=30)).timestamp()},
             format='json'
         )
 
@@ -231,11 +283,12 @@
         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'))
-
+        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})
+        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)
--- a/src/notes/tests/api/session.py	Fri Jul 28 18:22:46 2017 +0200
+++ b/src/notes/tests/api/session.py	Tue Jul 25 19:11:26 2017 +0200
@@ -107,6 +107,32 @@
             self.assertEqual(session['owner'], 'test_user1')
 
 
+    def test_list_session_filter(self):
+        url = reverse('notes:session-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url, {"ext_id__in": ",".join([str(self.session1.ext_id)])})
+        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")
+
+        for session in json['results']:
+            self.assertEqual(session['owner'], 'test_user1')
+
+
+    def test_list_session_filter_bad(self):
+        url = reverse('notes:session-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url, {"ext_id__in": ",".join([str(uuid4())])})
+        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'], 0, "must have no session")
+        self.assertEqual(len(json['results']), 0, "must have no session")
+
     def test_create_session_no_user(self):
         url = reverse('notes:session-list')
         response = self.client.post(url, {
--- a/src/notes/tests/api/sync.py	Fri Jul 28 18:22:46 2017 +0200
+++ b/src/notes/tests/api/sync.py	Tue Jul 25 19:11:26 2017 +0200
@@ -40,60 +40,85 @@
             password='top_secret'
         )
 
-        self.session1 = Session.objects.create(
-            title="a new session 1",
-            description="Description 1",
-            protocol="[]",
-            owner=user1
-        )
+        url = reverse('notes:session-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.post(url, {
+            'title': "a new session 1",
+            'description': "Description 1",
+            'protocol': "[]"
+        }, format='json')
+
+        logger.debug('REPOSNSE %r', response.json())
+
+        self.session1 = Session.objects.get(ext_id=response.json()['ext_id'])
+        self.client.logout()
 
-        self.session2 = Session.objects.create(
-            title="a new session 2",
-            description="Description 2",
-            protocol="[]",
-            owner=user2
-        )
+        self.client.login(username='test_user2', password='top_secret')
+        response = self.client.post(url, {
+            'title': "a new session 2",
+            'description': "Description 2",
+            'protocol': "[]"
+        }, format='json')
+
+        self.session2 = Session.objects.get(ext_id=response.json()['ext_id'])
+        self.client.logout()
 
-        Session.objects.create(
-            title="a new session 3",
-            description="Description 3",
-            protocol="[]",
-            owner=user3
-        )
+        self.client.login(username='test_user3', password='top_secret')
+        response = self.client.post(url, {
+            'title': "a new session 3",
+            'description': "Description 3",
+            'protocol': "[]"
+        }, format='json')
+
+        self.session3 = Session.objects.get(ext_id=response.json()['ext_id'])
+        self.client.logout()
+
+        self.client.login(username='test_user1', password='top_secret')
+
+        url = reverse('notes:notes-session-list',
+                      kwargs={'session_ext_id': self.session1.ext_id})
 
-        self.note1 = Note.objects.create(
-            tc_start=timezone.now(),
-            tc_end=timezone.now(),
-            session=self.session1,
-            plain="example note 1",
-            html="<i>example note 1</i>",
-            raw="<i>example note 1</i>",
-            margin_note="margin note 1",
-            categorization="[]"
-        )
+        response = self.client.post(url, {
+            'tc_start': timezone.now(),
+            'tc_end': timezone.now(),
+            'plain': "example note 1",
+            'html': "<i>example note 1</i>",
+            'raw': "<i>example note 1</i>",
+            'margin_note': "margin note 1",
+            'categorization': "[]"
+        }, format='json')
+
+        self.note1 = Note.objects.get(ext_id=response.json()['ext_id'])
 
-        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="[]"
-        )
+        response = self.client.post(url, {
+            'tc_start': timezone.now(),
+            'tc_end': timezone.now(),
+            'plain': "example note 2",
+            'html': "<i>example note 2</i>",
+            'raw': "<i>example note 2</i>",
+            'margin_note': "margin note 2",
+            'categorization': "[]"
+        }, format='json')
+
+        self.note2 = Note.objects.get(ext_id=response.json()['ext_id'])
+        self.client.logout()
 
+        self.client.login(username='test_user2', password='top_secret')
+        url = reverse('notes:notes-session-list',
+                      kwargs={'session_ext_id': self.session2.ext_id})
 
-        Note.objects.create(
-            tc_start=timezone.now(),
-            tc_end=timezone.now(),
-            session=self.session2,
-            plain="example note 2",
-            html="<i>example note</i>",
-            raw="<i>example note</i>",
-            margin_note="margin note",
-            categorization="[]"
-        )
+        response = self.client.post(url, {
+            'tc_start': timezone.now(),
+            'tc_end': timezone.now(),
+            'plain': "example note 3",
+            'html': "<i>example note 3</i>",
+            'raw': "<i>example note 3</i>",
+            'margin_note': "margin note 3",
+            'categorization': "[]"
+        }, format='json')
+
+        self.note3 = Note.objects.get(ext_id=response.json()['ext_id'])
+        self.client.logout()
 
     def test_not_authenticated(self):
         url = reverse('notes:sync-list')
@@ -181,11 +206,19 @@
 
 
     def test_modified_since_single_update(self):
-        self.note2.plain = "plain text modified"
-        self.note2.save()
+        self.client.login(username='test_user1', password='top_secret')
+        url = reverse('notes:notes-session-detail',
+                      kwargs={'session_ext_id': self.session1.ext_id, 'ext_id': self.note2.ext_id})
+
+        self.client.put(url, {
+            'plain': "example note 2 modified",
+            'html': "<i>example note 2 modified</i>",
+            'raw': "<i>example note 2 modified</i>",
+            'margin_note': "margin note 2 modified",
+            'categorization': "[]"
+        }, format='json')
 
         url = reverse('notes:sync-list')
-        self.client.login(username='test_user1', password='top_secret')
 
         nexthour = \
             datetime.datetime.utcnow().replace(tzinfo=timezone.utc) +\
@@ -213,7 +246,15 @@
         self.assertEqual(LogEntry.Action.UPDATE, sync_def['action'])
 
     def test_note_delete(self):
-        self.note2.delete()
+
+        self.client.login(username='test_user1', password='top_secret')
+        url = reverse('notes:notes-session-detail',
+                      kwargs={'session_ext_id': self.session1.ext_id, 'ext_id': self.note2.ext_id})
+
+        self.client.delete(url)
+        self.client.logout()
+
+
         url = reverse('notes:sync-list')
         self.client.login(username='test_user1', password='top_secret')
         response = self.client.get(url)
@@ -231,3 +272,39 @@
         self.assertEqual('note', sync_def['type'])
         self.assertEqual(0, sync_def['action'])
         self.assertEqual(sync_def['ext_id'],str(self.note1.ext_id))
+
+    def test_note_delete_modified(self):
+
+        self.client.login(username='test_user1', password='top_secret')
+        url = reverse('notes:notes-session-detail',
+                      kwargs={'session_ext_id': self.session1.ext_id, 'ext_id': self.note2.ext_id})
+
+        self.client.delete(url)
+        self.client.logout()
+
+
+        nexthour = \
+            datetime.datetime.utcnow().replace(tzinfo=timezone.utc) +\
+            datetime.timedelta(hours=1)
+
+        LogEntry.objects.filter(
+            content_type=ContentType.objects.get_for_model(Note),
+            object_pk=self.note2.id,
+            action=LogEntry.Action.DELETE
+        ).update(timestamp=nexthour)
+
+        url = reverse('notes:sync-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url, {'modified_since': (nexthour-datetime.timedelta(minutes=30)).timestamp()})
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        json_resp = response.json()
+        self.assertIn('sessions', json_resp)
+        self.assertEqual(0, len(json_resp['sessions']))
+
+        self.assertIn('notes', json_resp)
+        self.assertEqual(1, len(json_resp['notes']))
+        sync_def = json_resp['notes'][0]
+        self.assertEqual('note', sync_def['type'])
+        self.assertEqual(2, sync_def['action'])
+        self.assertEqual(sync_def['ext_id'],str(self.note2.ext_id))
+