--- 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
--- 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
--- 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'
+}
--- 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')),
]
--- /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"]
--- /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
+
--- /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')
--- /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)),
+]
--- /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']
--- /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)
Binary file src/notes/locale/en/LC_MESSAGES/django.mo has changed
--- 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
Binary file src/notes/locale/fr/LC_MESSAGES/django.mo has changed
--- 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
--- 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'),
),
]
--- 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')
--- 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')
+ )
--- 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.
--- /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
--- /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'
--- /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="<i>example note 1</i>",
+ raw="<i>example note 1</i>",
+ 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="<i>example note</i>",
+ raw="<i>example note</i>",
+ 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)
--- /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')),
+]
--- 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
--- 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"
],
)