# HG changeset patch # User ymh # Date 1497446271 -7200 # Node ID 63be3ce389f7efbd2bcdf5d58e41f6e4d5d8166a # Parent 4d93f4ed95bcef23687e1c31774820b9b4ef5365 improve api diff -r 4d93f4ed95bc -r 63be3ce389f7 src/.keepme diff -r 4d93f4ed95bc -r 63be3ce389f7 src/.pylintrc --- a/src/.pylintrc Wed Jun 14 12:28:09 2017 +0200 +++ b/src/.pylintrc Wed Jun 14 15:17:51 2017 +0200 @@ -136,7 +136,7 @@ function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ +good-names=i,j,k,ex,Run,_,logger # Include a hint for the correct naming format with invalid-name include-naming-hint=no diff -r 4d93f4ed95bc -r 63be3ce389f7 src/README --- a/src/README Wed Jun 14 12:28:09 2017 +0200 +++ b/src/README Wed Jun 14 15:17:51 2017 +0200 @@ -12,7 +12,9 @@ $ cp .env.tmpl .env $ vi .env $ mkvirtualenv irinotes -$ pip install requirements/dev.txt +$ cd requirements +$ pip install dev.txt +$ cd .. $ python manage.py migrate $ python manage.py collectstatic $ python manage.py createsuperuser diff -r 4d93f4ed95bc -r 63be3ce389f7 src/irinotes/settings.py --- a/src/irinotes/settings.py Wed Jun 14 12:28:09 2017 +0200 +++ b/src/irinotes/settings.py Wed Jun 14 15:17:51 2017 +0200 @@ -48,6 +48,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_extensions', + 'rest_framework', 'colorful', 'concurrency', 'notes' @@ -180,10 +182,31 @@ 'level': LOG_LEVEL, 'propagate': True, }, + 'django.db.backends': { + 'handlers': ['file'], + 'level': LOG_LEVEL, + 'propagate': True, + }, 'irinotes': { 'handlers': ['file'], 'level': LOG_LEVEL, 'propagate': True, }, + 'notes': { + 'handlers': ['file'], + 'level': LOG_LEVEL, + 'propagate': True, + }, } } + +# Rest Framework configuration + +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated' + ], + 'TEST_REQUEST_DEFAULT_FORMAT': 'json' +} diff -r 4d93f4ed95bc -r 63be3ce389f7 src/irinotes/urls.py --- a/src/irinotes/urls.py Wed Jun 14 12:28:09 2017 +0200 +++ b/src/irinotes/urls.py Wed Jun 14 15:17:51 2017 +0200 @@ -13,9 +13,10 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -from django.conf.urls import url +from django.conf.urls import url, include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), + url(r'^notes/', include('notes.urls')), ] diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/__init__.py diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/permissions/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/api/permissions/__init__.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,6 @@ +""" +Permissions classes fro notes +""" +from .core import SessionPermission, NotePermission + +__all__ = ["SessionPermission", "NotePermission"] diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/permissions/core.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/api/permissions/core.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,38 @@ +""" +Permissions for core objects +""" +import logging + +from rest_framework.permissions import IsAuthenticated + +from notes.models import Session + +logger = logging.getLogger(__name__) + +class SessionPermission(IsAuthenticated): + """ + Pemissions for sessions + """ + + def has_object_permission(self, request, view, obj): + return request.user == obj.owner + + +class NotePermission(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) + 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 + diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/serializers/__init__.py diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/serializers/core.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/api/serializers/core.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,55 @@ +""" +Serializers for model core classes +""" +from rest_framework import serializers + +from notes.models import Note, Session + + +class DetailNoteSerializer(serializers.ModelSerializer): + class Meta: + model = Note + fields = ( + 'ext_id', 'version', 'created', 'updated', + 'plain', 'html', 'raw', + 'categorization', 'margin_note', 'tc_start', 'tc_end' + ) + read_only_fields = ('ext_id', 'version', 'created', 'updated') + + +class ListNoteSerializer(serializers.ModelSerializer): + class Meta: + model = Note + fields = ( + 'ext_id', 'tc_start', 'tc_end' + ) + read_only_fields = ('ext_id', ) + + +class ListSessionSerializer(serializers.ModelSerializer): + + owner = serializers.SlugRelatedField( + read_only=True, slug_field='username', default=serializers.CurrentUserDefault()) + + class Meta: + model = Session + fields = ( + 'ext_id', 'version', 'created', 'updated', + 'owner', 'title', 'description', 'protocol' + ) + read_only_fields = ('ext_id', 'version', 'created', 'updated', 'owner') + + +class DetailSessionSerializer(serializers.ModelSerializer): + + owner = serializers.SlugRelatedField(read_only=True, slug_field='username') + notes = DetailNoteSerializer(many=True, read_only=True) + + class Meta: + model = Session + fields = ( + 'ext_id', 'version', 'created', 'updated', + 'owner', 'title', 'description', 'protocol', + 'notes' + ) + read_only_fields = ('ext_id', 'version', 'created', 'updated', 'owner') diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/api/urls.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,16 @@ +from django.conf.urls import url, include +from rest_framework_nested import routers +from .views import SessionViewSet, NoteViewSet + +router = routers.SimpleRouter() +router.register(r'sessions', SessionViewSet, base_name='session') + +session_router = routers.NestedSimpleRouter(router, r'sessions', lookup='session') +session_router.register(r'notes', NoteViewSet, base_name='notes') + +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browsable API. +urlpatterns = [ + url(r'^', include(router.urls)), + url(r'^', include(session_router.urls)), +] diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/views/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/api/views/__init__.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,3 @@ +from .core import SessionViewSet, NoteViewSet + +__all__ = ['SessionViewSet', 'NoteViewSet'] diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/api/views/core.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/api/views/core.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,52 @@ +import logging + +from notes.models import Note, Session +from rest_framework import viewsets + +from ..permissions import NotePermission, SessionPermission +from ..serializers.core import (DetailNoteSerializer, DetailSessionSerializer, + ListNoteSerializer, ListSessionSerializer) + +logger = logging.getLogger(__name__) + + +class SessionViewSet(viewsets.ModelViewSet): + """ + API endpoint that allow sessions ro be viewed or edited + """ + serializer_class = ListSessionSerializer + lookup_field = 'ext_id' + + serializers = { + 'list': ListSessionSerializer, + 'retrieve': DetailSessionSerializer, + } + + permission_classes = (SessionPermission,) + + def get_serializer_class(self): + return self.serializers.get(self.action, ListSessionSerializer) + + def get_queryset(self): + return Session.objects.filter(owner=self.request.user) + + +class NoteViewSet(viewsets.ModelViewSet): + + serializers = { + 'list': ListNoteSerializer, + 'retrieve': DetailNoteSerializer, + 'create': DetailNoteSerializer, + 'update': DetailNoteSerializer, + } + lookup_field = 'ext_id' + + permission_classes = (NotePermission,) + + def get_serializer_class(self): + return self.serializers.get(self.action, ListNoteSerializer) + + def get_queryset(self): + return Note.objects.filter( + session__ext_id=self.kwargs['session_ext_id'], + session__owner=self.request.user) diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/locale/en/LC_MESSAGES/django.mo Binary file src/notes/locale/en/LC_MESSAGES/django.mo has changed diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/locale/en/LC_MESSAGES/django.po --- a/src/notes/locale/en/LC_MESSAGES/django.po Wed Jun 14 12:28:09 2017 +0200 +++ b/src/notes/locale/en/LC_MESSAGES/django.po Wed Jun 14 15:17:51 2017 +0200 @@ -137,15 +137,15 @@ msgstr "session" #: models/core.py:33 -msgid "Note|text_plain" +msgid "Note|plain" msgstr "text plain" #: models/core.py:34 -msgid "Note|text_html" +msgid "Note|html" msgstr "text html" #: models/core.py:35 -msgid "Note|text_raw" +msgid "Note|raw" msgstr "text raw" #: models/core.py:36 diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/locale/fr/LC_MESSAGES/django.mo Binary file src/notes/locale/fr/LC_MESSAGES/django.mo has changed diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/locale/fr/LC_MESSAGES/django.po --- a/src/notes/locale/fr/LC_MESSAGES/django.po Wed Jun 14 12:28:09 2017 +0200 +++ b/src/notes/locale/fr/LC_MESSAGES/django.po Wed Jun 14 15:17:51 2017 +0200 @@ -138,15 +138,15 @@ msgstr "session" #: models/core.py:33 -msgid "Note|text_plain" +msgid "Note|plain" msgstr "texte seul" #: models/core.py:34 -msgid "Note|text_html" +msgid "Note|html" msgstr "texte html" #: models/core.py:35 -msgid "Note|text_raw" +msgid "Note|raw" msgstr "texte brut" #: models/core.py:36 diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/migrations/0001_initial.py --- a/src/notes/migrations/0001_initial.py Wed Jun 14 12:28:09 2017 +0200 +++ b/src/notes/migrations/0001_initial.py Wed Jun 14 15:17:51 2017 +0200 @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.2 on 2017-06-08 15:10 +# Generated by Django 1.11.2 on 2017-06-13 11:53 from __future__ import unicode_literals +import colorful.fields import concurrency.fields from django.conf import settings import django.contrib.auth.models @@ -47,6 +48,20 @@ ], ), migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Category|title')), + ('color', colorful.fields.RGBColorField(verbose_name='Category|color')), + ('need_comment', models.BooleanField(default=False, verbose_name='Category|need_comment')), + ('description', models.TextField(blank=True, null=True, verbose_name='Category|description')), + ], + options={ + 'verbose_name': 'Category', + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( name='GroupProfile', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), @@ -66,11 +81,11 @@ ('updated', models.DateTimeField(auto_now=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()), - ('tc_end', models.DateTimeField()), - ('text_plain', models.TextField(blank=True, null=True, verbose_name='Note|text_plain')), - ('text_html', models.TextField(blank=True, null=True, verbose_name='Note|text_html')), - ('text_raw', models.TextField(blank=True, null=True, verbose_name='Note|text_raw')), + ('tc_start', models.DateTimeField(verbose_name='Note|tc_start')), + ('tc_end', models.DateTimeField(verbose_name='Note|tc_end')), + ('plain', models.TextField(blank=True, null=True, verbose_name='Note|plain')), + ('html', models.TextField(blank=True, null=True, verbose_name='Note|html')), + ('raw', models.TextField(blank=True, null=True, verbose_name='Note|raw')), ('margin_note', models.TextField(blank=True, null=True, verbose_name='Note|margin_note')), ('categorization', models.TextField(blank=True, null=True, verbose_name='Note|categorization')), ], @@ -81,6 +96,22 @@ }, ), migrations.CreateModel( + 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')), + ('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')), + ('group_profile', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='notes.GroupProfile')), + ], + options={ + 'verbose_name': 'Protocol', + 'verbose_name_plural': 'Protocols', + }, + ), + migrations.CreateModel( name='Session', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), @@ -112,6 +143,11 @@ migrations.AddField( model_name='note', name='session', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='notes.Session', verbose_name='Note|session'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='notes.Session', verbose_name='Note|session'), + ), + migrations.AddField( + model_name='category', + name='protocol', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='notes.Protocol', verbose_name='Category|protocol'), ), ] diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/models/category.py --- a/src/notes/models/category.py Wed Jun 14 12:28:09 2017 +0200 +++ b/src/notes/models/category.py Wed Jun 14 15:17:51 2017 +0200 @@ -11,24 +11,33 @@ class Protocol(Model): title = models.CharField(max_length=255, verbose_name=_('Protocol|title')) - group_profile = models.OneToOneField(GroupProfile, on_delete=models.CASCADE) + group_profile = models.OneToOneField( + GroupProfile, on_delete=models.CASCADE) + class Meta: verbose_name = _('Protocol') verbose_name_plural = _('Protocols') - class Category(models.Model): title = models.CharField(max_length=255, verbose_name=_('Category|title')) color = RGBColorField(verbose_name=_('Category|color')) - need_comment = models.BooleanField(default=False, verbose_name=_('Category|need_comment')) - description = models.TextField(null=True, blank=True, verbose_name=_('Category|description')) + need_comment = models.BooleanField( + default=False, + verbose_name=_('Category|need_comment') + ) + description = models.TextField( + null=True, + blank=True, + verbose_name=_('Category|description') + ) protocol = models.ForeignKey( Protocol, verbose_name=_('Category|protocol'), related_name='categories', on_delete=models.CASCADE ) + class Meta: verbose_name = _('Category') verbose_name_plural = _('Categories') diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/models/core.py --- a/src/notes/models/core.py Wed Jun 14 12:28:09 2017 +0200 +++ b/src/notes/models/core.py Wed Jun 14 15:17:51 2017 +0200 @@ -17,9 +17,22 @@ settings.AUTH_USER_MODEL, on_delete=models.CASCADE, ) - title = models.TextField(null=True, blank=True, verbose_name=_('Session|title')) - description = models.TextField(null=True, blank=True, verbose_name=_('Session|description')) - protocol = models.TextField(null=True, blank=True, verbose_name=_('Session|protocol')) + title = models.TextField( + null=True, + blank=True, + verbose_name=_('Session|title') + ) + description = models.TextField( + null=True, + blank=True, + verbose_name=_('Session|description') + ) + protocol = models.TextField( + null=True, + blank=True, + verbose_name=_('Session|protocol') + ) + class Note(Model): class Meta: @@ -29,10 +42,34 @@ tc_start = models.DateTimeField(verbose_name=_('Note|tc_start')) tc_end = models.DateTimeField(verbose_name=_('Note|tc_end')) - session = models.ForeignKey(Session, on_delete=models.CASCADE, verbose_name=_('Note|session')) - text_plain = models.TextField(null=True, blank=True, verbose_name=_('Note|text_plain')) - text_html = models.TextField(null=True, blank=True, verbose_name=_('Note|text_html')) - text_raw = models.TextField(null=True, blank=True, verbose_name=_('Note|text_raw')) - margin_note = models.TextField(null=True, blank=True, verbose_name=_('Note|margin_note')) - categorization = models.TextField(null=True, blank=True, verbose_name=_('Note|categorization')) - + session = models.ForeignKey( + Session, + on_delete=models.CASCADE, + related_name='notes', + verbose_name=_('Note|session') + ) + plain = models.TextField( + null=True, + blank=True, + verbose_name=_('Note|plain') + ) + html = models.TextField( + null=True, + blank=True, + verbose_name=_('Note|html') + ) + raw = models.TextField( + null=True, + blank=True, + verbose_name=_('Note|raw') + ) + margin_note = models.TextField( + null=True, + blank=True, + verbose_name=_('Note|margin_note') + ) + categorization = models.TextField( + null=True, + blank=True, + verbose_name=_('Note|categorization') + ) diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/tests.py --- a/src/notes/tests.py Wed Jun 14 12:28:09 2017 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/tests/__init__.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,1 @@ +from .api import SessionApiTests diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/tests/api/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/tests/api/__init__.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,3 @@ +from .session import SessionApiTests + +__all__ = 'SessionApiTests' diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/tests/api/session.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/tests/api/session.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,146 @@ +""" +Tests the core api for sessions +""" +import logging + +from django.contrib.auth import get_user_model +from django.urls import reverse +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APITransactionTestCase + +from notes.models import Session, Note + +logger = logging.getLogger(__name__) + +class SessionApiTests(APITransactionTestCase): + + def setUp(self): + User = get_user_model() + user1 = User.objects.create_user( + username='test_user1', + email='test_user@emial.com', + password='top_secret' + ) + user2 = User.objects.create_user( + username='test_user2', + email='test_user@emial.com', + password='top_secret' + ) + user3 = User.objects.create_user( + username='test_user3', + email='test_user@emial.com', + password='top_secret' + ) + + self.session1 = Session.objects.create( + title="a new session 1", + description="Description 1", + protocol="[]", + owner=user1 + ) + + self.session2 = Session.objects.create( + title="a new session 2", + description="Description 2", + protocol="[]", + owner=user2 + ) + + Session.objects.create( + title="a new session 3", + description="Description 3", + protocol="[]", + owner=user3 + ) + + 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="[]" + ) + + 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="[]" + ) + + + def test_list_session_no_user(self): + url = reverse('notes_api:session-list') + response = self.client.post(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + + def test_list_session(self): + url = reverse('notes_api:session-list') + self.client.login(username='test_user1', password='top_secret') + 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.assertEqual(session['owner'], 'test_user1') + + + def test_create_session_no_user(self): + url = reverse('notes_api:session-list') + response = self.client.post(url, { + 'title': "a new session", + 'description': "description of the session", + 'protocol': "[]" + }, format='json') + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + + def test_create_session(self): + url = reverse('notes_api:session-list') + self.client.login(username='test_user1', password='top_secret') + response = self.client.post(url, { + 'title': "a new session", + 'description': "description of the session", + 'protocol': "[]" + }, format='json') + + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + json = response.json() + self.assertIn('ext_id', json) + + def test_detail_session(self): + url = reverse('notes_api:session-detail', kwargs={'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) + + def test_list_notes(self): + url = reverse('notes_api:notes-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) + + def test_detail_session_bad(self): + url = reverse('notes_api:session-detail', kwargs={'ext_id':str(self.session2.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_404_NOT_FOUND) + + def test_list_notes_bad(self): + url = reverse('notes_api:notes-list', kwargs={'session_ext_id':str(self.session2.ext_id)}) + logger.debug("URL: %s", url) + 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) diff -r 4d93f4ed95bc -r 63be3ce389f7 src/notes/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/notes/urls.py Wed Jun 14 15:17:51 2017 +0200 @@ -0,0 +1,8 @@ +from django.conf.urls import url, include + +from .api import urls as api_urls + +urlpatterns = [ + url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r'^api/', include(api_urls, namespace='notes_api')), +] diff -r 4d93f4ed95bc -r 63be3ce389f7 src/requirements/base.txt --- a/src/requirements/base.txt Wed Jun 14 12:28:09 2017 +0200 +++ b/src/requirements/base.txt Wed Jun 14 15:17:51 2017 +0200 @@ -2,9 +2,11 @@ Django==1.11.2 django-colorful==1.2 django-concurrency==1.3.2 +django-filter==1.0.4 django-guardian==1.4.8 djangorestframework==3.6.3 irinotes==0.0.1 +Markdown==2.6.8 python-decouple==3.0 pytz==2017.2 six==1.10.0 diff -r 4d93f4ed95bc -r 63be3ce389f7 src/setup.py --- a/src/setup.py Wed Jun 14 12:28:09 2017 +0200 +++ b/src/setup.py Wed Jun 14 15:17:51 2017 +0200 @@ -143,7 +143,9 @@ "djangorestframework >= 3.6", "django-guardian >= 1.4", "django-colorful", - "django-concurrency" + "django-concurrency", + "django-filter", + "markdown" ], )