diff options
-rw-r--r-- | apps/notes/admin.py | 13 | ||||
-rw-r--r-- | apps/notes/migrations/0004_auto_20181117_2039.py | 27 | ||||
-rw-r--r-- | apps/notes/migrations/0005_auto_20181119_1145.py | 28 | ||||
-rw-r--r-- | apps/notes/models.py | 12 | ||||
-rw-r--r-- | apps/notes/serializers.py | 4 | ||||
-rw-r--r-- | apps/notes/tests/test_models.py | 36 | ||||
-rw-r--r-- | apps/notes/tests/test_views.py | 58 | ||||
-rw-r--r-- | apps/notes/urls.py | 21 | ||||
-rw-r--r-- | apps/notes/views.py | 59 | ||||
-rw-r--r-- | design/templates/notes/notes_list.html | 9 |
10 files changed, 220 insertions, 47 deletions
diff --git a/apps/notes/admin.py b/apps/notes/admin.py new file mode 100644 index 0000000..dbac05c --- /dev/null +++ b/apps/notes/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from .models import Note, Folder + + +@admin.register(Note) +class NoteAdmin(admin.ModelAdmin): + pass + + +@admin.register(Folder) +class FolderAdmin(admin.ModelAdmin): + pass diff --git a/apps/notes/migrations/0004_auto_20181117_2039.py b/apps/notes/migrations/0004_auto_20181117_2039.py new file mode 100644 index 0000000..6fc6f2d --- /dev/null +++ b/apps/notes/migrations/0004_auto_20181117_2039.py @@ -0,0 +1,27 @@ +# Generated by Django 2.1.2 on 2018-11-18 02:39 + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0003_note_folder'), + ] + + operations = [ + migrations.AddField( + model_name='folder', + name='date_created', + field=models.DateTimeField(blank=True, default=datetime.datetime(2018, 11, 18, 2, 38, 45, 996162, tzinfo=utc), editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='folder', + name='date_updated', + field=models.DateTimeField(blank=True, default=datetime.datetime(2018, 11, 18, 2, 39, 0, 850658, tzinfo=utc), editable=False), + preserve_default=False, + ), + ] diff --git a/apps/notes/migrations/0005_auto_20181119_1145.py b/apps/notes/migrations/0005_auto_20181119_1145.py new file mode 100644 index 0000000..9b6cee7 --- /dev/null +++ b/apps/notes/migrations/0005_auto_20181119_1145.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.2 on 2018-11-19 17:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0004_auto_20181117_2039'), + ] + + operations = [ + migrations.RenameField( + model_name='note', + old_name='body_markdown', + new_name='body', + ), + migrations.AddField( + model_name='note', + name='is_public', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='note', + name='rendered_body', + field=models.TextField(null=True), + ), + ] diff --git a/apps/notes/models.py b/apps/notes/models.py index aeb3bb3..318b079 100644 --- a/apps/notes/models.py +++ b/apps/notes/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils import timezone from django.template.defaultfilters import slugify +from django.urls import reverse from taggit.managers import TaggableManager @@ -10,6 +11,8 @@ from django.conf import settings class Folder(models.Model): created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) name = models.CharField(max_length=250) + date_created = models.DateTimeField(blank=True, editable=False) + date_updated = models.DateTimeField(blank=True, editable=False) def __str__(self): return self.name @@ -20,19 +23,26 @@ class Note(models.Model): date_created = models.DateTimeField(blank=True, editable=False) date_updated = models.DateTimeField(blank=True, editable=False) title = models.CharField(max_length=250) - body_markdown = models.TextField(null=True) + body = models.TextField(null=True) + rendered_body = models.TextField(null=True) url = models.CharField(max_length=250) slug = models.SlugField(unique=True, blank=True) folder = models.ForeignKey(Folder, null=True, on_delete=models.SET_NULL) tags = TaggableManager(blank=True, help_text='Tags') + is_public = models.BooleanField(default=False) def __str__(self): return self.title + def get_absolute_url(self): + return reverse("notes:notes-detail", kwargs={"pk": self.pk}) + def save(self, **kwargs): # On save, update timestamps (users updated through admin.py) if not self.id: self.date_created = timezone.now() self.slug = slugify(self.title)[:50] + if not self.title: + self.title = str(self.date_created) self.date_updated = timezone.now() super(Note, self).save() diff --git a/apps/notes/serializers.py b/apps/notes/serializers.py index d6c7392..0929e79 100644 --- a/apps/notes/serializers.py +++ b/apps/notes/serializers.py @@ -9,10 +9,10 @@ class NoteSerializer(TaggitSerializer, serializers.HyperlinkedModelSerializer): class Meta: model = Note - fields = ('title', 'body_markdown', 'url', 'folder', 'tags', 'date_created') + fields = ('title', 'body', 'url', 'tags') class FolderSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Folder - fields = ('name') + fields = ('name',) diff --git a/apps/notes/tests/test_models.py b/apps/notes/tests/test_models.py index e6cdb6a..ddb731e 100644 --- a/apps/notes/tests/test_models.py +++ b/apps/notes/tests/test_models.py @@ -14,17 +14,31 @@ class FolderModelTest(TestCase): class NoteModelTest(TestCase): - - def test_string_representation(self): - user = mixer.blend(User, username='test') - note = Note( - created_by=user, + def setUp(self): + self.user = mixer.blend(User, username='test') + self.note = Note.objects.create( + created_by=self.user, title="test note", - body_markdown="the body of the note", + body="the body of the note", + url="https://luxagraf.net/", + tags="mine,cool site" + ) + self.note.save() + self.note_no_title = Note.objects.create( + created_by=self.user, + body="the body of the note", url="https://luxagraf.net/", tags="mine,cool site" - ) - self.assertEqual(str(note), "test note") - self.assertEqual(str(note.body_markdown), "the body of the note") - self.assertEqual(str(note.url), "https://luxagraf.net/") - self.assertEqual(str(note.tags), "mine,cool site") + ) + self.note_no_title.save() + + def test_string_representation(self): + self.assertEqual(str(self.note), "test note") + self.assertEqual(str(self.note.body), "the body of the note") + self.assertEqual(str(self.note.url), "https://luxagraf.net/") + self.assertEqual(str(self.note.tags), "mine,cool site") + # titleless note gets date + self.assertEqual(str(self.note_no_title), str(self.note_no_title.date_created)) + + def test_get_absolute_url(self): + self.assertEqual(str(self.note.get_absolute_url()), "/notes/%s/" % (self.note.id)) diff --git a/apps/notes/tests/test_views.py b/apps/notes/tests/test_views.py index 3f21b0b..05cb8db 100644 --- a/apps/notes/tests/test_views.py +++ b/apps/notes/tests/test_views.py @@ -1,9 +1,11 @@ import json from django.test import Client from django.test import RequestFactory, TestCase +from django.urls import reverse from rest_framework.test import force_authenticate from rest_framework.test import APIRequestFactory +from rest_framework.test import APIClient from mixer.backend.django import mixer from accounts.models import User @@ -11,28 +13,32 @@ from notes.models import Note from notes.views import NoteListView, NoteViewSet -class StoriesViewTest(TestCase): +class NotesViewsTest(TestCase): def setUp(self): # Every test needs access to the request factory. self.factory = RequestFactory() # test API with rest framework request factory. self.apifactory = APIRequestFactory() - self.user = mixer.blend(User, username='tpynchon', password="gravity") + self.client = Client() + # and test with rest client + self.apiclient = APIClient() + self.user = User.objects.create(username='testuser', password='password') + self.bad_user = User.objects.create(username='someoneelse', password='password') self.note = Note.objects.create( created_by=self.user, title="test note", - body_markdown="the body of the note", + body="the body of the note", url="https://luxagraf.net/", ) self.note.tags.add("mine,cool site") self.note.save() def test_list_view(self): - request = self.factory.get('/%s/notes/' % (self.user.username)) + request = self.factory.get('/notes/') request.user = self.user response = NoteListView.as_view()(request) self.assertEqual(response.status_code, 200) - response.render() + # bad_user def test_api_list(self): # Make an authenticated request to the view... @@ -45,13 +51,37 @@ class StoriesViewTest(TestCase): self.assertEqual(api_data['title'], 'test note') self.assertEqual(api_data['tags'], ['mine,cool site']) - def test_api_(self): - # Make an authenticated request to the view... - request = self.factory.get('/api/notes/') - force_authenticate(request, user=self.user) - response = NoteViewSet.as_view({'get': 'list'})(request) - self.assertEqual(response.status_code, 200) + def test_note_create(self): + ''' + post some data to create a new note + ''' + data = { + 'title': "test note post", + 'body': "the body of the note", + 'url': "https://luxagraf.net/", + 'tags': [], + } + self.apiclient.force_authenticate(self.user) + url = reverse("notes:notes-list") + response = self.apiclient.post(url, data, format='json') + self.assertEqual(response.status_code, 201) + self.assertEqual(Note.objects.count(), 2) response.render() - api_data = json.loads(response.content.decode('utf8'))[0] - self.assertEqual(api_data['title'], 'test note') - self.assertEqual(api_data['tags'], ['mine,cool site']) + api_data = json.loads(response.content.decode('utf8')) + self.assertEqual(api_data['title'], 'test note post') + self.assertEqual(api_data['body'], 'the body of the note') + self.assertEqual(api_data['tags'], []) + + def test_note_create_bad(self): + # create another user + data = { + 'title': "", + 'body': "the body of the note", + 'url': "https://luxagraf.net/", + 'tags': [], + } + url = reverse("notes:notes-list") + self.apiclient.force_authenticate(self.user) + response = self.apiclient.post(url, data, format='json') + self.assertEqual(response.status_code, 400) + self.assertEqual(Note.objects.count(), 1) diff --git a/apps/notes/urls.py b/apps/notes/urls.py index 76bdeb1..cbb1884 100644 --- a/apps/notes/urls.py +++ b/apps/notes/urls.py @@ -1,18 +1,17 @@ from django.urls import path +from django.conf.urls import include -from . import views +from rest_framework import routers + +from .views import NoteViewSet, FolderViewSet, NoteListView + +router = routers.DefaultRouter() +router.register(r'notes/folder', FolderViewSet, basename="folder") +router.register(r'notes', NoteViewSet, basename="notes") app_name = "notes" urlpatterns = [ - path( - r'create/', - views.NoteCreateView.as_view(), - name="note-create" - ), - path( - r'', - views.NoteListView.as_view(), - name="note" - ), + path(r'', NoteListView.as_view(), name='homepage',), + path(r'', include(router.urls)), ] diff --git a/apps/notes/views.py b/apps/notes/views.py index ddb72ed..e4b8fda 100644 --- a/apps/notes/views.py +++ b/apps/notes/views.py @@ -2,11 +2,16 @@ from django.views.generic import CreateView, ListView, UpdateView, DeleteView from django.views.generic.detail import DetailView from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404, render, redirect +from django.urls import reverse from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.decorators import list_route +from rest_framework import permissions + from .serializers import NoteSerializer, FolderSerializer -from .models import Note -from .forms import NoteForm +from .models import Note, Folder @method_decorator(login_required, name='dispatch') @@ -22,20 +27,58 @@ class NoteListView(ListView): model = Note def get_queryset(self): - return Note.objects.filter(created_by=self.request.user) + if not self.request.user.is_anonymous: + return Note.objects.filter(created_by=self.request.user) + def get_template_names(self): + if not self.request.user.is_anonymous: + return ['notes/notes_list.html'] + else: + return ['sell.html'] -class NoteCreateView(LoggedInCreateViewWithUser): - model = Note - form_class = NoteForm - template_name = "notes/create.html" + +class IsOwnerOrDeny(permissions.BasePermission): + """ + Custom permission to only allow owners to post to their endpoint + """ + + def has_object_permission(self, request, view, obj): + # Write permissions are only allowed to the owner of the snippet. + return obj.owner == request.user class NoteViewSet(viewsets.ModelViewSet): """ - API endpoint that allows users to be viewed or edited. + API endpoint that allows notes to be viewed or edited. """ serializer_class = NoteSerializer + permission_classes = (permissions.IsAuthenticated, IsOwnerOrDeny,) def get_queryset(self): return Note.objects.filter(created_by=self.request.user).order_by('-date_created') + + @list_route(methods=['post']) + def perform_create(self, serializer): + serializer.save(created_by=self.request.user) + return super(NoteViewSet, self).perform_create(serializer) + + def get_object(self): + obj = get_object_or_404(self.get_queryset(), pk=self.kwargs["pk"]) + if obj.is_public: + return obj + else: + self.check_object_permissions(self.request, obj) + return obj + + +class FolderViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows folder to be viewed or edited. + """ + serializer_class = FolderSerializer + + def get_queryset(self): + return Folder.objects.filter(created_by=self.request.user).order_by('-date_created') + + def perform_create(self, serializer): + serializer.save(created_by=self.request.user) diff --git a/design/templates/notes/notes_list.html b/design/templates/notes/notes_list.html new file mode 100644 index 0000000..4451588 --- /dev/null +++ b/design/templates/notes/notes_list.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content %} +<main> + <h1> Notes</h1> + <ul>{% for obj in object_list %} + <li><a href="{{obj.get_absolute_url}}">{{obj}}</a></li> + {% endfor %}</ul> +</main> +{% endblock %} |