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