# HG changeset patch # User ymh # Date 1501002686 -7200 # Node ID 34a75bd8d0b958046b94e9c414d61c0545d0e97c # Parent 006c5270128ce11236f121b3b3602b537218ed43 add filter on session and node list to recover specific objects diff -r 006c5270128c -r 34a75bd8d0b9 src/irinotes/settings.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' ] diff -r 006c5270128c -r 34a75bd8d0b9 src/notes/api/filters.py --- /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'] + diff -r 006c5270128c -r 34a75bd8d0b9 src/notes/api/views/core.py --- 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) diff -r 006c5270128c -r 34a75bd8d0b9 src/notes/api/views/sync.py --- 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(), diff -r 006c5270128c -r 34a75bd8d0b9 src/notes/middlewares.py --- /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) diff -r 006c5270128c -r 34a75bd8d0b9 src/notes/tests/api/note.py --- 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) diff -r 006c5270128c -r 34a75bd8d0b9 src/notes/tests/api/session.py --- 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, { diff -r 006c5270128c -r 34a75bd8d0b9 src/notes/tests/api/sync.py --- 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="example note 1", - raw="example note 1", - margin_note="margin note 1", - categorization="[]" - ) + response = self.client.post(url, { + 'tc_start': timezone.now(), + 'tc_end': timezone.now(), + 'plain': "example note 1", + 'html': "example note 1", + 'raw': "example note 1", + '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="example note 1,1", - raw="example note 1.1", - 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': "example note 2", + 'raw': "example note 2", + '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="example note", - raw="example note", - margin_note="margin note", - categorization="[]" - ) + response = self.client.post(url, { + 'tc_start': timezone.now(), + 'tc_end': timezone.now(), + 'plain': "example note 3", + 'html': "example note 3", + 'raw': "example note 3", + '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': "example note 2 modified", + 'raw': "example note 2 modified", + '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)) +