From 0c2a092e8d8ad33a1c306ee9efca0da96eb56415 Mon Sep 17 00:00:00 2001 From: luxagraf Date: Sat, 24 Nov 2018 22:29:02 -0600 Subject: way to much for a single commit --- apps/accounts/migrations/0001_initial.py | 2 +- apps/accounts/models.py | 2 + apps/notes/admin.py | 6 +- apps/notes/forms.py | 4 +- apps/notes/migrations/0001_initial.py | 57 +++++++++++++---- apps/notes/migrations/0002_auto_20181111_1825.py | 31 ---------- apps/notes/migrations/0003_note_folder.py | 19 ------ apps/notes/migrations/0004_auto_20181117_2039.py | 27 -------- apps/notes/migrations/0005_auto_20181119_1145.py | 28 --------- apps/notes/models.py | 59 ++++++++++++------ apps/notes/serializers.py | 10 +-- apps/notes/tests/test_models.py | 24 ++++---- apps/notes/tests/test_views.py | 32 +++++++--- apps/notes/urls.py | 20 +++--- apps/notes/views.py | 68 ++++++++++++++++----- apps/utils/util.py | 23 +++++++ config/base_urls.py | 32 +++++++--- config/requirements.txt | 2 + config/settings.py | 24 ++++++++ design/sass/_forms.scss | 34 +++++++++++ design/sass/_global.scss | 7 ++- design/sass/_header.scss | 1 + design/sass/_mixins.scss | 1 + design/sass/_notes.scss | 71 +++++++++++++++++++++ design/sass/screenv1.scss | 2 + design/templates/base.html | 29 ++++----- design/templates/notes/create.html | 18 ------ design/templates/notes/notes_create.html | 52 ++++++++++++++++ design/templates/notes/notes_detail.html | 78 ++++++++++++++++++++++++ design/templates/notes/notes_list.html | 2 +- design/templates/pages/page.html | 16 +++++ minify.py | 5 +- scripts/util.js | 76 +++++++++++++++++++++-- 33 files changed, 614 insertions(+), 248 deletions(-) delete mode 100644 apps/notes/migrations/0002_auto_20181111_1825.py delete mode 100644 apps/notes/migrations/0003_note_folder.py delete mode 100644 apps/notes/migrations/0004_auto_20181117_2039.py delete mode 100644 apps/notes/migrations/0005_auto_20181119_1145.py create mode 100644 design/sass/_notes.scss delete mode 100644 design/templates/notes/create.html create mode 100644 design/templates/notes/notes_create.html create mode 100644 design/templates/notes/notes_detail.html diff --git a/apps/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py index 0e2775e..36aa6ba 100644 --- a/apps/accounts/migrations/0001_initial.py +++ b/apps/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.2 on 2018-11-02 19:01 +# Generated by Django 2.1.2 on 2018-11-24 04:41 from django.conf import settings import django.contrib.auth.models diff --git a/apps/accounts/models.py b/apps/accounts/models.py index a930081..5ed7634 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -15,6 +15,8 @@ class UserProfile(models.Model): website = models.CharField(max_length=300, null=True, blank=True, default='') location = models.CharField(max_length=300, null=True, blank=True, default='') bio = models.TextField(null=True, blank=True, default='') + #default_note_public = models.BooleanField(default=False) + #default_note_folder = models.ForeignKey('notes.Folder', null=True, on_delete=models.SET_NULL) def __str__(self): return self.user.username diff --git a/apps/notes/admin.py b/apps/notes/admin.py index dbac05c..3958d55 100644 --- a/apps/notes/admin.py +++ b/apps/notes/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Note, Folder +from .models import Note, Notebook @admin.register(Note) @@ -8,6 +8,6 @@ class NoteAdmin(admin.ModelAdmin): pass -@admin.register(Folder) -class FolderAdmin(admin.ModelAdmin): +@admin.register(Notebook) +class NotebookAdmin(admin.ModelAdmin): pass diff --git a/apps/notes/forms.py b/apps/notes/forms.py index 6a27ec9..a7a4f8d 100644 --- a/apps/notes/forms.py +++ b/apps/notes/forms.py @@ -7,9 +7,9 @@ from .models import Note class NoteForm(forms.ModelForm): class Meta: model = Note - fields = ['title', 'body_markdown', 'url', 'tags'] + fields = ['title', 'body_text', 'body_html', 'body_qjson', 'notebook', 'url', 'tags'] labels = { - "body_markdown": _("Note"), + "body": _("Note"), } def __init__(self, *args, **kwargs): diff --git a/apps/notes/migrations/0001_initial.py b/apps/notes/migrations/0001_initial.py index 3f04eb7..f8e2fff 100644 --- a/apps/notes/migrations/0001_initial.py +++ b/apps/notes/migrations/0001_initial.py @@ -1,9 +1,11 @@ -# Generated by Django 2.1.2 on 2018-11-11 18:01 +# Generated by Django 2.1.2 on 2018-11-24 13:55 from django.conf import settings +import django.contrib.postgres.fields.jsonb from django.db import migrations, models import django.db.models.deletion import taggit.managers +import uuid class Migration(migrations.Migration): @@ -11,28 +13,61 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('taggit', '0002_auto_20150616_2121'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Folder', + name='Note', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=250)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('unique_id', models.UUIDField(default=uuid.uuid4, editable=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=250)), + ('body_text', models.TextField(null=True)), + ('body_html', models.TextField(blank=True, null=True)), + ('body_qjson', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ('url', models.CharField(blank=True, max_length=250, null=True)), + ('slug', models.SlugField(blank=True)), + ('is_public', models.BooleanField(default=False)), ], ), migrations.CreateModel( - name='Note', + name='Notebook', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=250)), - ('body_markdown', models.TextField(null=True)), - ('url', models.CharField(max_length=250)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('tags', taggit.managers.TaggableManager(blank=True, help_text='Tags', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), + ('unique_id', models.UUIDField(default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=250)), + ('url', models.CharField(blank=True, max_length=250, null=True)), + ('slug', models.SlugField(blank=True)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), + migrations.AddField( + model_name='note', + name='notebook', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.Notebook'), + ), + migrations.AddField( + model_name='note', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='note', + name='tags', + field=taggit.managers.TaggableManager(blank=True, help_text='Tags', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AlterUniqueTogether( + name='notebook', + unique_together={('owner', 'slug')}, + ), + migrations.AlterUniqueTogether( + name='note', + unique_together={('owner', 'slug')}, + ), ] diff --git a/apps/notes/migrations/0002_auto_20181111_1825.py b/apps/notes/migrations/0002_auto_20181111_1825.py deleted file mode 100644 index a7bc1c7..0000000 --- a/apps/notes/migrations/0002_auto_20181111_1825.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-11 18:25 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('notes', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='note', - name='date_created', - field=models.DateTimeField(blank=True, default=datetime.datetime(2018, 11, 11, 18, 25, 29, 645561), editable=False), - preserve_default=False, - ), - migrations.AddField( - model_name='note', - name='date_updated', - field=models.DateTimeField(blank=True, default=datetime.datetime(2018, 11, 11, 18, 25, 42, 867666), editable=False), - preserve_default=False, - ), - migrations.AddField( - model_name='note', - name='slug', - field=models.SlugField(blank=True, unique=True), - ), - ] diff --git a/apps/notes/migrations/0003_note_folder.py b/apps/notes/migrations/0003_note_folder.py deleted file mode 100644 index d548429..0000000 --- a/apps/notes/migrations/0003_note_folder.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-14 15:41 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('notes', '0002_auto_20181111_1825'), - ] - - operations = [ - migrations.AddField( - model_name='note', - name='folder', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.Folder'), - ), - ] diff --git a/apps/notes/migrations/0004_auto_20181117_2039.py b/apps/notes/migrations/0004_auto_20181117_2039.py deleted file mode 100644 index 6fc6f2d..0000000 --- a/apps/notes/migrations/0004_auto_20181117_2039.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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 deleted file mode 100644 index 9b6cee7..0000000 --- a/apps/notes/migrations/0005_auto_20181119_1145.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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 318b079..83766a9 100644 --- a/apps/notes/models.py +++ b/apps/notes/models.py @@ -1,48 +1,67 @@ +import uuid from django.db import models from django.utils import timezone +from django.utils.functional import cached_property from django.template.defaultfilters import slugify from django.urls import reverse +from django.contrib.postgres.fields import JSONField from taggit.managers import TaggableManager from django.conf import settings +from utils.util import unique_slug_generator -class Folder(models.Model): - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + +class Notebook(models.Model): + unique_id = models.UUIDField(default=uuid.uuid4, editable=False) + owner = 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) + url = models.CharField(max_length=250, null=True, blank=True) + slug = models.SlugField(blank=True) + date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False) + date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False) + + class Meta: + unique_together = ("owner", "slug") def __str__(self): return self.name + def save(self, **kwargs): + if self._state.adding: + self.slug = unique_slug_generator(self) + super(Notebook, self).save() + class Note(models.Model): - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - date_created = models.DateTimeField(blank=True, editable=False) - date_updated = models.DateTimeField(blank=True, editable=False) + unique_id = models.UUIDField(default=uuid.uuid4, editable=False) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False) + date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False) title = models.CharField(max_length=250) - 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) + body_text = models.TextField(null=True) + body_html = models.TextField(null=True, blank=True) + body_qjson = JSONField(null=True, blank=True) + url = models.CharField(max_length=250, null=True, blank=True) + slug = models.SlugField(blank=True) + notebook = models.ForeignKey(Notebook, null=True, blank=True, on_delete=models.SET_NULL) tags = TaggableManager(blank=True, help_text='Tags') is_public = models.BooleanField(default=False) + class Meta: + unique_together = ("owner", "slug") + def __str__(self): return self.title + @cached_property def get_absolute_url(self): - return reverse("notes:notes-detail", kwargs={"pk": self.pk}) + return reverse("notes:note-detail", kwargs={"user": self.owner.username, "slug": self.slug}) 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() + if not self.title: + self.title = str(self.body_text)[:50] + if self._state.adding: + self.slug = unique_slug_generator(self) super(Note, self).save() diff --git a/apps/notes/serializers.py b/apps/notes/serializers.py index 0929e79..2fbcc1f 100644 --- a/apps/notes/serializers.py +++ b/apps/notes/serializers.py @@ -1,18 +1,18 @@ from rest_framework import serializers from taggit_serializer.serializers import TagListSerializerField, TaggitSerializer -from .models import Note, Folder +from .models import Note, Notebook -class NoteSerializer(TaggitSerializer, serializers.HyperlinkedModelSerializer): +class NoteSerializer(TaggitSerializer, serializers.ModelSerializer): tags = TagListSerializerField() class Meta: model = Note - fields = ('title', 'body', 'url', 'tags') + fields = ('title', 'body_text', 'body_qjson', 'body_html', 'url', 'notebook', 'tags') -class FolderSerializer(serializers.HyperlinkedModelSerializer): +class NotebookSerializer(serializers.HyperlinkedModelSerializer): class Meta: - model = Folder + model = Notebook fields = ('name',) diff --git a/apps/notes/tests/test_models.py b/apps/notes/tests/test_models.py index ddb731e..cf21aca 100644 --- a/apps/notes/tests/test_models.py +++ b/apps/notes/tests/test_models.py @@ -1,32 +1,32 @@ from django.test import TestCase from mixer.backend.django import mixer -from notes.models import Note, Folder +from notes.models import Note, Notebook from accounts.models import User -class FolderModelTest(TestCase): +class NotebookModelTest(TestCase): def test_string_representation(self): user = mixer.blend(User, username='tpynchon') - folder = Folder(created_by=user, name="My Folder") - self.assertEqual(str(folder), "My Folder") + notebook = Notebook(owner=user, name="San Miguel Notes") + self.assertEqual(str(notebook), "San Miguel Notes") class NoteModelTest(TestCase): def setUp(self): - self.user = mixer.blend(User, username='test') + self.user = mixer.blend(User, username='tpynchon') self.note = Note.objects.create( - created_by=self.user, + owner=self.user, title="test note", - body="the body of the note", + body_text="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", + owner=self.user, + body_text="the body of the note", url="https://luxagraf.net/", tags="mine,cool site" ) @@ -34,11 +34,11 @@ class NoteModelTest(TestCase): 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.body_text), "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)) + self.assertEqual(str(self.note_no_title), str(self.note_no_title.body_text)[:50]) def test_get_absolute_url(self): - self.assertEqual(str(self.note.get_absolute_url()), "/notes/%s/" % (self.note.id)) + self.assertEqual(str(self.note.get_absolute_url), "/notes/%s/%s" % (self.note.owner.username, self.note.slug)) diff --git a/apps/notes/tests/test_views.py b/apps/notes/tests/test_views.py index 05cb8db..4f9a4ce 100644 --- a/apps/notes/tests/test_views.py +++ b/apps/notes/tests/test_views.py @@ -2,6 +2,7 @@ import json from django.test import Client from django.test import RequestFactory, TestCase from django.urls import reverse +from django.template.defaultfilters import slugify from rest_framework.test import force_authenticate from rest_framework.test import APIRequestFactory @@ -25,9 +26,9 @@ class NotesViewsTest(TestCase): 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, + owner=self.user, title="test note", - body="the body of the note", + body_text="the body of the note", url="https://luxagraf.net/", ) self.note.tags.add("mine,cool site") @@ -40,6 +41,19 @@ class NotesViewsTest(TestCase): self.assertEqual(response.status_code, 200) # bad_user + def test_note_create_view(self): + data = { + 'title': "test note post", + 'body_text': "the body of the note", + 'url': "https://luxagraf.net/", + 'tags': [], + } + self.client.force_login(self.user) + url = reverse("notes:note-create") + response = self.client.post(url, data) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, '/notes/%s/%s' % (self.user.username, slugify(data['title']))) + def test_api_list(self): # Make an authenticated request to the view... request = self.factory.get('/api/notes/') @@ -51,36 +65,36 @@ class NotesViewsTest(TestCase): self.assertEqual(api_data['title'], 'test note') self.assertEqual(api_data['tags'], ['mine,cool site']) - def test_note_create(self): + def test_api_note_create(self): ''' - post some data to create a new note + Post some data to create a new note ''' data = { 'title': "test note post", - 'body': "the body of the note", + 'body_text': "the body of the note", 'url': "https://luxagraf.net/", 'tags': [], } self.apiclient.force_authenticate(self.user) - url = reverse("notes:notes-list") + url = reverse("notes-api-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')) self.assertEqual(api_data['title'], 'test note post') - self.assertEqual(api_data['body'], 'the body of the note') + self.assertEqual(api_data['body_text'], '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", + 'body_text': "the body of the note", 'url': "https://luxagraf.net/", 'tags': [], } - url = reverse("notes:notes-list") + url = reverse("notes-api-list") self.apiclient.force_authenticate(self.user) response = self.apiclient.post(url, data, format='json') self.assertEqual(response.status_code, 400) diff --git a/apps/notes/urls.py b/apps/notes/urls.py index cbb1884..ccfcc9e 100644 --- a/apps/notes/urls.py +++ b/apps/notes/urls.py @@ -1,17 +1,17 @@ from django.urls import path -from django.conf.urls import include -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") +from .views import ( + NoteDetailView, + NoteCreateView, + NoteListView, + NoteListRedirectView, +) app_name = "notes" urlpatterns = [ - path(r'', NoteListView.as_view(), name='homepage',), - path(r'', include(router.urls)), + path(r'create/', NoteCreateView.as_view(), name='note-create',), + path(r'/', NoteDetailView.as_view(), name='note-detail',), + path(r'/', NoteListView.as_view(), name='note-list',), + path(r'', NoteListRedirectView.as_view(), name='note-redirect',), ] diff --git a/apps/notes/views.py b/apps/notes/views.py index e4b8fda..b971390 100644 --- a/apps/notes/views.py +++ b/apps/notes/views.py @@ -1,21 +1,23 @@ from django.views.generic import CreateView, ListView, UpdateView, DeleteView from django.views.generic.detail import DetailView +from django.views.generic.base import View, RedirectView 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 django.urls import reverse, reverse_lazy 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, Folder +from .serializers import NoteSerializer, NotebookSerializer +from .models import Note, Notebook +from .forms import NoteForm @method_decorator(login_required, name='dispatch') -class LoggedInCreateViewWithUser(CreateView): +class LoggedInViewWithUser(View): def get_form_kwargs(self, **kwargs): kwargs = super().get_form_kwargs(**kwargs) @@ -23,20 +25,56 @@ class LoggedInCreateViewWithUser(CreateView): return kwargs -class NoteListView(ListView): +class NoteListView(LoggedInViewWithUser, ListView): model = Note def get_queryset(self): if not self.request.user.is_anonymous: - return Note.objects.filter(created_by=self.request.user) + return Note.objects.filter(owner=self.request.user) def get_template_names(self): + # print("IP Address for debug-toolbar: " + self.request.META['REMOTE_ADDR']) if not self.request.user.is_anonymous: return ['notes/notes_list.html'] else: return ['sell.html'] +class NoteListRedirectView(RedirectView, LoggedInViewWithUser): + + def get_redirect_url(self, *args, **kwargs): + return reverse_lazy("notes:note-list", kwargs={"user": self.request.user.username}) + + +class NoteDetailView(UpdateView, LoggedInViewWithUser): + model = Note + form_class = NoteForm + template_name = 'notes/notes_detail.html' + + def get_queryset(self): + if not self.request.user.is_anonymous: + return Note.objects.filter(owner=self.request.user) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['notes_list'] = Note.objects.filter(owner=self.request.user) + return context + + +class NoteCreateView(CreateView, LoggedInViewWithUser): + model = Note + form_class = NoteForm + template_name = 'notes/notes_create.html' + + def form_valid(self, form): + form.instance.owner = self.request.user + self.object = form.save() + return super(NoteCreateView, self).form_valid(form) + + def get_success_url(self): + return reverse_lazy('notes:note-detail', kwargs={'user': self.request.user.username, 'slug': self.object.slug}) + + class IsOwnerOrDeny(permissions.BasePermission): """ Custom permission to only allow owners to post to their endpoint @@ -52,15 +90,13 @@ class NoteViewSet(viewsets.ModelViewSet): API endpoint that allows notes to be viewed or edited. """ serializer_class = NoteSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrDeny,) + permission_classes = (permissions.IsAuthenticated,) def get_queryset(self): - return Note.objects.filter(created_by=self.request.user).order_by('-date_created') + return Note.objects.filter(owner=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) + serializer.save(owner=self.request.user) def get_object(self): obj = get_object_or_404(self.get_queryset(), pk=self.kwargs["pk"]) @@ -71,14 +107,14 @@ class NoteViewSet(viewsets.ModelViewSet): return obj -class FolderViewSet(viewsets.ModelViewSet): +class NotebookViewSet(viewsets.ModelViewSet): """ - API endpoint that allows folder to be viewed or edited. + API endpoint that allows botebook to be viewed or edited. """ - serializer_class = FolderSerializer + serializer_class = NotebookSerializer def get_queryset(self): - return Folder.objects.filter(created_by=self.request.user).order_by('-date_created') + return Notebook.objects.filter(owner=self.request.user).order_by('-date_created') def perform_create(self, serializer): - serializer.save(created_by=self.request.user) + serializer.save(owner=self.request.user) diff --git a/apps/utils/util.py b/apps/utils/util.py index 0c089ee..899b73f 100644 --- a/apps/utils/util.py +++ b/apps/utils/util.py @@ -1,5 +1,8 @@ import re +import random +import string from django.apps import apps +from django.utils.text import slugify from django.template.loader import render_to_string from bs4 import BeautifulSoup import markdown @@ -82,3 +85,23 @@ def parse_video(s): if soup.find('video'): return True return False + + +def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits): + return ''.join(random.choice(chars) for _ in range(size)) + + +def unique_slug_generator(instance, new_slug=None): + if new_slug is not None: + slug = new_slug + else: + slug = slugify(instance.title) + Klass = instance.__class__ + qs_exists = Klass.objects.filter(slug=slug).exists() + if qs_exists: + new_slug = "{slug}-{randstr}".format( + slug=slug, + randstr=random_string_generator(size=4) + ) + return unique_slug_generator(instance, new_slug=new_slug) + return slug diff --git a/config/base_urls.py b/config/base_urls.py index 84dd56d..953ceae 100644 --- a/config/base_urls.py +++ b/config/base_urls.py @@ -9,25 +9,39 @@ from django_registration.backends.activation.views import RegistrationView from rest_framework import routers from pages.views import PageDetailView -from notes.views import NoteViewSet, FolderViewSet, NoteListView from accounts.forms import UserForm - +from notes.views import ( + NoteViewSet, + NotebookViewSet, + NoteListView, +) router = routers.DefaultRouter() -router.register(r'/notes/folder/', FolderViewSet, basename="folder-api") -router.register(r'/notes/', NoteViewSet, basename="notes-api") - +router.register(r'notes/notebook/', NotebookViewSet, basename="notebook-api") +router.register(r'notes', NoteViewSet, basename="notes-api") +ADMIN_URL = 'https://docs.djangoproject.com/en/dev/ref/contrib/admin/' urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += [ - path('admin/', admin.site.urls), + path('admin/', RedirectView.as_view(url=ADMIN_URL)), + path('magux/', admin.site.urls), path(r'accounts/', include('django_registration.backends.activation.urls')), path(r'accounts/', include('django.contrib.auth.urls')), path(r'register/', RegistrationView.as_view(form_class=UserForm), name='django_registration_register',), path(r'settings/', include('accounts.urls')), path(r'', include('django_registration.backends.activation.urls')), path(r'', include('django.contrib.auth.urls')), - path(r'', include('notes.urls')), - path(r'', PageDetailView.as_view(), name="pages"), - path(r'//', PageDetailView.as_view(), name="pages"), + path(r'', NoteListView.as_view(), name='homepage',), + path(r'notes/', include('notes.urls')), path(r'api/', include(router.urls)), + path(r'', PageDetailView.as_view(), name="pages"), + #path(r'//', PageDetailView.as_view(), name="pages"), path(r'api-auth/', include('rest_framework.urls', namespace='rest_framework')) ] +if settings.DEBUG: + import debug_toolbar + urlpatterns = [ + path('__debug__/', include(debug_toolbar.urls)), + + # For django versions before 2.0: + # url(r'^__debug__/', include(debug_toolbar.urls)), + + ] + urlpatterns diff --git a/config/requirements.txt b/config/requirements.txt index fb66a23..cfe5b7f 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -7,6 +7,7 @@ confusable-homoglyphs==3.2.0 coverage==4.5.1 decorator==4.3.0 Django==2.1.2 +django-debug-toolbar==1.10.1 django-extensions==2.1.3 django-registration==3.0 django-storages==1.7.1 @@ -37,6 +38,7 @@ python-decouple==3.1 pytz==2018.7 requests==2.20.1 six==1.11.0 +sqlparse==0.2.4 text-unidecode==1.2 traitlets==4.3.2 urllib3==1.24.1 diff --git a/config/settings.py b/config/settings.py index 52d2314..b95c18d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -25,6 +25,7 @@ AUTH_PROFILE_MODULE = "accounts.UserProfile" AUTH_USER_MODEL = "accounts.User" DEFAULT_FILE_STORAGE = config('DEFAULT_FILE_STORAGE') +INTERNAL_IPS = config('INTERNAL_IPS') AWS_S3_OBJECT_PARAMETERS = { 'CacheControl': 'max-age=86400', @@ -39,6 +40,9 @@ REGISTRATION_SALT = 'Astra inclinant, sed non obligant' # Django Rest Framework REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ) @@ -60,6 +64,7 @@ THIRD_PARTY_APPS = [ 'taggit_serializer', 'django_extensions', 'rest_framework', + 'debug_toolbar' ] LOCAL_APPS = [ 'utils', @@ -75,6 +80,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -164,5 +170,23 @@ STATIC_ROOT = BASE_DIR+'/static/' MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR+'/media/' +LOGOUT_REDIRECT_URL = "/" + EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" EMAIL_FILE_PATH = os.path.join(BASE_DIR, "sent_emails") + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), + }, + }, +} diff --git a/design/sass/_forms.scss b/design/sass/_forms.scss index cac2442..d6b0931 100644 --- a/design/sass/_forms.scss +++ b/design/sass/_forms.scss @@ -86,6 +86,35 @@ table { border: 1px solid $link_hover_color; } } +.btn-small { + @include fontsize(10); + @include smcaps; +} +.btn-subtle { + padding: 3px 5px; + border: none; //1px solid $body_font_light; + color: $body_font_light !important; + background: white; + &:hover { + background: white; + border: none; //1px solid; + color: $link_color !important; + } +} +.btn-accent { + padding: 3px 5px; + border: 1px solid $text_accent; + background: $text_accent; + color: white !important; + outline: $text_accent; + &:hover { + background: $text_accent; + border: 1px solid $text_accent; + } +} +.btn-inline { + display: inline; +} .form-narrow { margin: 0 auto; max-width: 60%; @@ -94,3 +123,8 @@ table { @include fontsize(24); @include fancy-serif; } +.highlight { + border: 1px solid lighten($body_font_light, 20); + border-radius: 4px; + padding: 6px; +} diff --git a/design/sass/_global.scss b/design/sass/_global.scss index e852855..d394042 100644 --- a/design/sass/_global.scss +++ b/design/sass/_global.scss @@ -153,11 +153,11 @@ h3 { } .wrapper { @include constrain_wide; - margin-top: 5rem; + //margin-top: 5rem; } //************** Universals ************************ .hide { - display: none; + display: none !important; } .strike { @@ -183,6 +183,9 @@ h3 { .sm { max-width: 80px; } +.left-margin-2 { + margin-left: 2px; +} //************** other global classes ************************ .sans { @include generic_sans; diff --git a/design/sass/_header.scss b/design/sass/_header.scss index 8b8c3da..7f98419 100644 --- a/design/sass/_header.scss +++ b/design/sass/_header.scss @@ -13,6 +13,7 @@ header { } } .right { + margin-top: 4px; float: right; } } diff --git a/design/sass/_mixins.scss b/design/sass/_mixins.scss index 1aeeb83..bfccc17 100644 --- a/design/sass/_mixins.scss +++ b/design/sass/_mixins.scss @@ -9,6 +9,7 @@ $headline_font_serif: Georgia, 'Times New Roman', serif; $body_p_font: normal 100% / 1.5 "proxima-nova",helvetica,arial,sans-serif; $body_font_color: #6a6a6a; $body_font_light: #b3aeae; +$text_accent: #1be223; $archive_p_line_height: 1.6; //$light; diff --git a/design/sass/_notes.scss b/design/sass/_notes.scss new file mode 100644 index 0000000..09d234a --- /dev/null +++ b/design/sass/_notes.scss @@ -0,0 +1,71 @@ +.note-title { + @include fontsize(22); +} +.note-header { + @extend %clearfix; +} +.note-header-float { + width: 30%; + float: right; + text-align: right; +} +.note-time, .note-url { + text-align: right; + @include fancy-sans; + @include fontsize(13); +} +.note-container { + @include constrain(80%); +} +#note-body { + @include fancy-sans; + @include fontsize(15); +} +.inactive { + .ql-editor { + padding:0; + line-height:inherit; + p { + padding:inherit; + margin-bottom:10px; + } + } + .ql-toolbar { + display:none; + } + .ql-container.ql-snow { + border:none; + font-family:inherit; + font-size:inherit; + } +} +.note-list-container { + max-width: 300px; + ul { + padding: 0; + list-style-type: none; + } + li { + @include fontsize(13); + height: 4.5rem; + box-shadow: 0 -1px 0 #e7e2ee inset; + margin: 0; + a { + color: lighten($body_font_color, 15); + text-decoration: none; + } + } + h4 { + @include fontsize(15); + margin: 0; + padding: 8px 0 8px 6px; + font-weight: normal; + overflow: hidden; + white-space: nowrap; + } + .note-preview { + padding-left: 6px; + overflow: hidden; + white-space: nowrap; + } +} diff --git a/design/sass/screenv1.scss b/design/sass/screenv1.scss index 73a1fd1..d54027e 100644 --- a/design/sass/screenv1.scss +++ b/design/sass/screenv1.scss @@ -6,3 +6,5 @@ @import "_footer.scss"; @import "_forms.scss"; @import "_modal.scss"; +@import "_breadcrumbs.scss"; +@import "_notes.scss"; diff --git a/design/templates/base.html b/design/templates/base.html index 6b6ef8c..f14e5df 100644 --- a/design/templates/base.html +++ b/design/templates/base.html @@ -1,4 +1,4 @@ - +{% load static %} @@ -10,9 +10,10 @@ - + +{% block extrastyles %}{%endblock%} - + @@ -37,11 +38,17 @@
+ {% block content %} {% endblock %}