From 4f7b84194b056b5d6d9acca4cceb2cabc04fd8a5 Mon Sep 17 00:00:00 2001 From: luxagraf Date: Sat, 29 Dec 2018 08:37:39 -0600 Subject: cleaned up JS and made modal handler. --- README.md | 2 + apps/notes/admin.py | 7 +- apps/notes/forms.py | 31 +- apps/notes/migrations/0002_auto_20181204_0620.py | 42 + apps/notes/migrations/0003_auto_20181204_0641.py | 59 ++ apps/notes/migrations/0004_auto_20181204_0653.py | 19 + apps/notes/migrations/0005_luxtag_owner.py | 22 + apps/notes/migrations/0006_auto_20181204_0957.py | 22 + apps/notes/migrations/0007_auto_20181204_1050.py | 22 + apps/notes/migrations/0008_auto_20181204_1311.py | 19 + apps/notes/migrations/0009_remove_luxtag_owner.py | 17 + apps/notes/migrations/0010_auto_20181204_2117.py | 22 + apps/notes/migrations/0011_auto_20181221_1029.py | 39 + apps/notes/migrations/0012_auto_20181221_1038.py | 22 + apps/notes/migrations/0013_remove_luxtag_owner.py | 17 + apps/notes/models.py | 29 + apps/notes/notes_urls.py | 2 +- apps/notes/serializers.py | 4 +- apps/notes/tests/test_models.py | 2 +- apps/notes/views.py | 154 ++- apps/utils/views.py | 27 +- apps/utils/widgets.py | 33 +- config/base_urls.py | 2 - config/settings.py | 4 +- design/sass/_forms.scss | 13 +- design/sass/_global.scss | 24 +- design/sass/_modal.scss | 201 ++-- design/sass/_notes.scss | 121 ++- design/sass/screenv1.scss | 2 +- design/templates/base.html | 6 +- design/templates/notes/notebook_create.html | 36 - design/templates/notes/notebook_detail.html | 4 +- design/templates/notes/notebook_list.html | 55 ++ design/templates/notes/notes_create.html | 30 +- design/templates/notes/notes_detail.html | 34 +- design/templates/notes/notes_list.html | 54 +- design/templates/notes/notes_listold.html | 9 - design/templates/notes/partials/note_list.html | 6 +- design/templates/notes/partials/notebook_form.html | 19 + design/templates/pages/page.html | 11 - scripts/package.json | 17 +- scripts/shrinkwrap.yaml | 1011 +++++++++++++++++++- scripts/src/color-picker.js | 0 scripts/src/lib/overlay.js | 253 ----- scripts/src/main-nav.js | 12 + scripts/src/note-edit.js | 181 ++-- scripts/src/note-list.js | 33 + scripts/src/note-new.js | 27 + scripts/src/notebook-edit.js | 74 +- scripts/src/util.js | 114 ++- 50 files changed, 2333 insertions(+), 633 deletions(-) create mode 100644 apps/notes/migrations/0002_auto_20181204_0620.py create mode 100644 apps/notes/migrations/0003_auto_20181204_0641.py create mode 100644 apps/notes/migrations/0004_auto_20181204_0653.py create mode 100644 apps/notes/migrations/0005_luxtag_owner.py create mode 100644 apps/notes/migrations/0006_auto_20181204_0957.py create mode 100644 apps/notes/migrations/0007_auto_20181204_1050.py create mode 100644 apps/notes/migrations/0008_auto_20181204_1311.py create mode 100644 apps/notes/migrations/0009_remove_luxtag_owner.py create mode 100644 apps/notes/migrations/0010_auto_20181204_2117.py create mode 100644 apps/notes/migrations/0011_auto_20181221_1029.py create mode 100644 apps/notes/migrations/0012_auto_20181221_1038.py create mode 100644 apps/notes/migrations/0013_remove_luxtag_owner.py delete mode 100644 design/templates/notes/notebook_create.html create mode 100644 design/templates/notes/notebook_list.html delete mode 100644 design/templates/notes/notes_listold.html create mode 100644 design/templates/notes/partials/notebook_form.html delete mode 100644 scripts/src/color-picker.js delete mode 100644 scripts/src/lib/overlay.js create mode 100644 scripts/src/note-list.js create mode 100644 scripts/src/note-new.js diff --git a/README.md b/README.md index 3602b22..7796141 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Create, edit and view notes +The concept of snoozing a note. DeQuincy via Manly P Hall: after weeping in the British museum over the fact that one cannot possibly read all the books contained in the libraries, DeQuincy said, "as I cannot read all these books, I will read only the best". Sometimes you take notes that end up not being useful to you, you can snooze these notes, surface them later, reflect on them a second time and then decide, is this something I want to keep around, is it something that has knowledge I need or is it something I should let go of and move on to something else? + ## Internal Apps ### Accounts * [User](https://git.luxagraf.net/luxagraf/writer/src/branch/master/apps/accounts/models.py) diff --git a/apps/notes/admin.py b/apps/notes/admin.py index 3958d55..44915f2 100644 --- a/apps/notes/admin.py +++ b/apps/notes/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Note, Notebook +from .models import Note, Notebook, LuxTag @admin.register(Note) @@ -8,6 +8,11 @@ class NoteAdmin(admin.ModelAdmin): pass +@admin.register(LuxTag) +class TagAdmin(admin.ModelAdmin): + pass + + @admin.register(Notebook) class NotebookAdmin(admin.ModelAdmin): pass diff --git a/apps/notes/forms.py b/apps/notes/forms.py index 5ef9f84..4dc79d2 100644 --- a/apps/notes/forms.py +++ b/apps/notes/forms.py @@ -1,10 +1,19 @@ from django import forms from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import NON_FIELD_ERRORS + +from utils.widgets import RelatedFieldWidgetCanAdd from .models import Note, Notebook -class NoteForm(forms.ModelForm): +class BaseNoteForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) + super(BaseNoteForm, self).__init__(*args, **kwargs) + + +class NoteForm(BaseNoteForm): class Meta: model = Note fields = ['title', 'body_text', 'body_html', 'body_qjson', 'notebook', 'url', 'tags'] @@ -13,14 +22,28 @@ class NoteForm(forms.ModelForm): } def __init__(self, *args, **kwargs): - self.user = kwargs.pop("user", None) + user = kwargs.pop("user", None) super(NoteForm, self).__init__(*args, **kwargs) + self.fields['notebook'].widget = RelatedFieldWidgetCanAdd(Notebook, related_url="notebooks:list") + self.fields['notebook'].queryset = Notebook.objects.filter(owner__username=user) -class NotebookForm(NoteForm): +class NotebookForm(BaseNoteForm): class Meta: model = Notebook - fields = ['name', 'color_rgb'] + fields = ['owner', 'name', 'color_rgb'] + widgets = {'owner': forms.HiddenInput()} labels = { "name": _("Notebook Name"), + "color_rgb": _("Notebook Color"), + } + error_messages = { + NON_FIELD_ERRORS: { + 'unique_together': "You already have a notebook by that name, please choose a different name", + } } + + def __init__(self, *args, **kwargs): + user = kwargs.pop("user", None) + super(NotebookForm, self).__init__(*args, **kwargs) + self.fields['owner'].initial = user diff --git a/apps/notes/migrations/0002_auto_20181204_0620.py b/apps/notes/migrations/0002_auto_20181204_0620.py new file mode 100644 index 0000000..e7cb38d --- /dev/null +++ b/apps/notes/migrations/0002_auto_20181204_0620.py @@ -0,0 +1,42 @@ +# Generated by Django 2.1.2 on 2018-12-04 12:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('notes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('slug', models.SlugField(blank=True)), + ('color_hex', models.CharField(max_length=6)), + ('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)), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_tag', to='notes.Tag')), + ], + ), + migrations.AlterModelOptions( + name='note', + options={'ordering': ('-date_created', '-date_updated')}, + ), + migrations.AddField( + model_name='note', + name='tagstwo', + field=models.ManyToManyField(blank=True, to='notes.Tag'), + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together={('owner', 'name')}, + ), + ] diff --git a/apps/notes/migrations/0003_auto_20181204_0641.py b/apps/notes/migrations/0003_auto_20181204_0641.py new file mode 100644 index 0000000..9423058 --- /dev/null +++ b/apps/notes/migrations/0003_auto_20181204_0641.py @@ -0,0 +1,59 @@ +# Generated by Django 2.1.2 on 2018-12-04 12:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('notes', '0002_auto_20181204_0620'), + ] + + operations = [ + migrations.CreateModel( + name='LuxTag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='Name')), + ('slug', models.SlugField(max_length=100, unique=True, verbose_name='Slug')), + ('color_hex', models.CharField(max_length=6)), + ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tags', + }, + ), + migrations.CreateModel( + name='TaggedNotes', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.IntegerField(db_index=True, verbose_name='Object id')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes_taggednotes_tagged_items', to='contenttypes.ContentType', verbose_name='Content type')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes_taggednotes_items', to='notes.LuxTag')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together=set(), + ), + migrations.RemoveField( + model_name='tag', + name='owner', + ), + migrations.RemoveField( + model_name='tag', + name='parent', + ), + migrations.RemoveField( + model_name='note', + name='tagstwo', + ), + migrations.DeleteModel( + name='Tag', + ), + ] diff --git a/apps/notes/migrations/0004_auto_20181204_0653.py b/apps/notes/migrations/0004_auto_20181204_0653.py new file mode 100644 index 0000000..fc6d911 --- /dev/null +++ b/apps/notes/migrations/0004_auto_20181204_0653.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.2 on 2018-12-04 12:53 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0003_auto_20181204_0641'), + ] + + operations = [ + migrations.AlterField( + model_name='note', + name='tags', + field=taggit.managers.TaggableManager(blank=True, help_text='Tags', through='notes.TaggedNotes', to='notes.LuxTag', verbose_name='Tags'), + ), + ] diff --git a/apps/notes/migrations/0005_luxtag_owner.py b/apps/notes/migrations/0005_luxtag_owner.py new file mode 100644 index 0000000..168bd0b --- /dev/null +++ b/apps/notes/migrations/0005_luxtag_owner.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-04 13:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('notes', '0004_auto_20181204_0653'), + ] + + operations = [ + migrations.AddField( + model_name='luxtag', + name='owner', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/apps/notes/migrations/0006_auto_20181204_0957.py b/apps/notes/migrations/0006_auto_20181204_0957.py new file mode 100644 index 0000000..bf4293c --- /dev/null +++ b/apps/notes/migrations/0006_auto_20181204_0957.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-04 15:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0005_luxtag_owner'), + ] + + operations = [ + migrations.RemoveField( + model_name='notebook', + name='url', + ), + migrations.AddField( + model_name='notebook', + name='color_hex', + field=models.CharField(blank=True, max_length=6, null=True), + ), + ] diff --git a/apps/notes/migrations/0007_auto_20181204_1050.py b/apps/notes/migrations/0007_auto_20181204_1050.py new file mode 100644 index 0000000..a4bdc30 --- /dev/null +++ b/apps/notes/migrations/0007_auto_20181204_1050.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-04 16:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0006_auto_20181204_0957'), + ] + + operations = [ + migrations.RemoveField( + model_name='notebook', + name='color_hex', + ), + migrations.AddField( + model_name='notebook', + name='color_rgb', + field=models.CharField(blank=True, max_length=20, null=True), + ), + ] diff --git a/apps/notes/migrations/0008_auto_20181204_1311.py b/apps/notes/migrations/0008_auto_20181204_1311.py new file mode 100644 index 0000000..02bf272 --- /dev/null +++ b/apps/notes/migrations/0008_auto_20181204_1311.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.2 on 2018-12-04 19:11 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('notes', '0007_auto_20181204_1050'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='notebook', + unique_together={('owner', 'name')}, + ), + ] diff --git a/apps/notes/migrations/0009_remove_luxtag_owner.py b/apps/notes/migrations/0009_remove_luxtag_owner.py new file mode 100644 index 0000000..18896f3 --- /dev/null +++ b/apps/notes/migrations/0009_remove_luxtag_owner.py @@ -0,0 +1,17 @@ +# Generated by Django 2.1.2 on 2018-12-05 03:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0008_auto_20181204_1311'), + ] + + operations = [ + migrations.RemoveField( + model_name='luxtag', + name='owner', + ), + ] diff --git a/apps/notes/migrations/0010_auto_20181204_2117.py b/apps/notes/migrations/0010_auto_20181204_2117.py new file mode 100644 index 0000000..69da825 --- /dev/null +++ b/apps/notes/migrations/0010_auto_20181204_2117.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-05 03:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0009_remove_luxtag_owner'), + ] + + operations = [ + migrations.RemoveField( + model_name='luxtag', + name='color_hex', + ), + migrations.AddField( + model_name='luxtag', + name='color_rgb', + field=models.CharField(blank=True, max_length=20, null=True), + ), + ] diff --git a/apps/notes/migrations/0011_auto_20181221_1029.py b/apps/notes/migrations/0011_auto_20181221_1029.py new file mode 100644 index 0000000..7b88a62 --- /dev/null +++ b/apps/notes/migrations/0011_auto_20181221_1029.py @@ -0,0 +1,39 @@ +# Generated by Django 2.1.2 on 2018-12-21 16:29 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('notes', '0010_auto_20181204_2117'), + ] + + operations = [ + migrations.CreateModel( + name='Annotation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('unique_id', models.UUIDField(default=uuid.uuid4, editable=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ('highlight_text', models.TextField(null=True)), + ('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)), + ('is_public', models.BooleanField(default=False)), + ('note', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.Note')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='luxtag', + name='owner', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/notes/migrations/0012_auto_20181221_1038.py b/apps/notes/migrations/0012_auto_20181221_1038.py new file mode 100644 index 0000000..f02cbeb --- /dev/null +++ b/apps/notes/migrations/0012_auto_20181221_1038.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-21 16:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +from django.contrib import auth +User = auth.get_user_model() + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0011_auto_20181221_1029'), + ] + + operations = [ + migrations.AlterField( + model_name='luxtag', + name='owner', + field=models.ForeignKey(default=User.objects.get(username='luxagraf').id, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/apps/notes/migrations/0013_remove_luxtag_owner.py b/apps/notes/migrations/0013_remove_luxtag_owner.py new file mode 100644 index 0000000..a96b105 --- /dev/null +++ b/apps/notes/migrations/0013_remove_luxtag_owner.py @@ -0,0 +1,17 @@ +# Generated by Django 2.1.2 on 2018-12-21 17:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notes', '0012_auto_20181221_1038'), + ] + + operations = [ + migrations.RemoveField( + model_name='luxtag', + name='owner', + ), + ] diff --git a/apps/notes/models.py b/apps/notes/models.py index 95c1c96..9dc4f13 100644 --- a/apps/notes/models.py +++ b/apps/notes/models.py @@ -25,6 +25,10 @@ class LuxTag(TagBase): verbose_name = _("Tag") verbose_name_plural = _("Tags") + @cached_property + def get_absolute_url(self): + return reverse("notes:tags", kwargs={"slug": self.slug}) + class TaggedNotes(GenericTaggedItemBase): tag = models.ForeignKey(LuxTag, related_name="%(app_label)s_%(class)s_items", on_delete=models.CASCADE) @@ -55,6 +59,15 @@ class Notebook(models.Model): def get_absolute_url(self): return reverse("notebooks:detail", kwargs={"slug": self.slug}) + @cached_property + def color_rgba(self, opacity=".5"): + try: + color = self.color_rgb.split('(')[1].split(')')[0] + rgba = "rgba(%s,%s)" % (color, opacity) + except AttributeError: + rgba = self.color_rgb + return rgba + class Note(models.Model): unique_id = models.UUIDField(default=uuid.uuid4, editable=False) @@ -88,3 +101,19 @@ class Note(models.Model): if self._state.adding: self.slug = unique_slug_generator(self) super(Note, self).save() + + +class Annotation(models.Model): + 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) + highlight_text = models.TextField(null=True) + body_text = models.TextField(null=True) + body_html = models.TextField(null=True, blank=True) + body_qjson = JSONField(null=True, blank=True) + note = models.ForeignKey(Note, null=True, blank=True, on_delete=models.SET_NULL) + is_public = models.BooleanField(default=False) + + def __str__(self): + return self.body_text[:30] diff --git a/apps/notes/notes_urls.py b/apps/notes/notes_urls.py index f2573ce..55fb32b 100644 --- a/apps/notes/notes_urls.py +++ b/apps/notes/notes_urls.py @@ -11,7 +11,7 @@ app_name = "notes" urlpatterns = [ path(r'create/', NoteCreateView.as_view(), name='create',), - path(r'/', NoteDetailView.as_view(), name='detail',), path(r't/', NoteTagView.as_view(), name='tags',), + path(r'/', NoteDetailView.as_view(), name='detail',), path(r'', NoteListView.as_view(), name='list',), ] diff --git a/apps/notes/serializers.py b/apps/notes/serializers.py index f811edd..6bb08de 100644 --- a/apps/notes/serializers.py +++ b/apps/notes/serializers.py @@ -27,7 +27,7 @@ class NoteSerializer(TaggitSerializer, serializers.ModelSerializer): class Meta: model = Note - fields = ('title', 'body_text', 'body_qjson', 'body_html', 'url', 'notebook', 'tags') + fields = ('id', 'title', 'body_text', 'body_qjson', 'body_html', 'url', 'notebook', 'tags') class NotebookSerializer(serializers.HyperlinkedModelSerializer): @@ -38,7 +38,7 @@ class NotebookSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Notebook - fields = ('name', 'color_rgb', 'json_absolute_url', 'owner') + fields = ('id', 'name', 'color_rgb', 'json_absolute_url', 'owner') class NoteTagSerializer(serializers.HyperlinkedModelSerializer): diff --git a/apps/notes/tests/test_models.py b/apps/notes/tests/test_models.py index 05f2618..0c53a25 100644 --- a/apps/notes/tests/test_models.py +++ b/apps/notes/tests/test_models.py @@ -3,7 +3,7 @@ from django.urls import reverse from django.contrib import auth from mixer.backend.django import mixer -from notes.models import Note, Notebook +from notes.models import Note, Notebook User = auth.get_user_model() diff --git a/apps/notes/views.py b/apps/notes/views.py index 5d55720..6751340 100644 --- a/apps/notes/views.py +++ b/apps/notes/views.py @@ -1,6 +1,11 @@ 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.views.generic.edit import FormView, ModelFormMixin +from django.http import JsonResponse +from django.core import serializers +from django.forms import modelformset_factory +from django.db.models import Count +from django.views.generic.base import 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 @@ -14,10 +19,25 @@ from rest_framework import permissions from .serializers import NoteSerializer, NotebookSerializer, NoteTagSerializer from .models import Note, Notebook, LuxTag from .forms import NoteForm, NotebookForm +from utils.views import AjaxableResponseMixin + +################## +# Base Views +################## + + +@method_decorator(login_required, name='dispatch') +class BaseListView(ListView): + pass + + +@method_decorator(login_required, name='dispatch') +class BaseDetailView(DetailView): + pass @method_decorator(login_required, name='dispatch') -class LoggedInViewWithUser(View): +class LoggedInViewWithUser(FormView): def get_form_kwargs(self, **kwargs): kwargs = super().get_form_kwargs(**kwargs) @@ -25,12 +45,23 @@ class LoggedInViewWithUser(View): return kwargs -class NoteListView(LoggedInViewWithUser, ListView): +################## +# Note Views +################## + + +class NoteListView(BaseListView): model = Note def get_queryset(self): if not self.request.user.is_anonymous: - return Note.objects.filter(owner=self.request.user) + return Note.objects.prefetch_related('tags').filter(owner=self.request.user).select_related('notebook') + + def get_context_data(self, **kwargs): + context = super(NoteListView, self).get_context_data(**kwargs) + context['notebook_list'] = Notebook.objects.filter(owner=self.request.user).exclude(name="Trash").annotate(note_count=Count('note')) + context['tag_list'] = LuxTag.objects.filter(note__owner=self.request.user).annotate(note_count=Count('note')) + return context def get_template_names(self): # print("IP Address for debug-toolbar: " + self.request.META['REMOTE_ADDR']) @@ -40,36 +71,10 @@ class NoteListView(LoggedInViewWithUser, ListView): return ['sell.html'] -class NoteTagView(LoggedInViewWithUser, ListView): - model = Note - template_name = 'notes/notes_list.html' - - def get_queryset(self): - ''' - This can generate a crazy amount of joins if there's a lot of tags - have to keep an eye on it. Would be better to do: - from django.db.models import Q - from functools import reduce - from operator import and_, or_ - #query = reduce(and_, (Q(tags__slug=t) for t in self.tag_list)) - # Note.objects.filter(query, owner=self.request.user) - But that doesn't work for some reason. - ''' - if not self.request.user.is_anonymous: - self.tag_list = [x.strip() for x in self.kwargs['slug'].split("+")] - qs = Note.objects.filter(owner=self.request.user) - for tag in self.tag_list: - qs = qs.filter(tags__slug=tag) - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['notes_list'] = Note.objects.filter(owner=self.request.user) - context['tags'] = self.tag_list - return context - - -class NoteDetailView(UpdateView, LoggedInViewWithUser): +class NoteDetailView(LoggedInViewWithUser, AjaxableResponseMixin, UpdateView): + ''' + POST only works as AJAX + ''' model = Note form_class = NoteForm template_name = 'notes/notes_detail.html' @@ -80,11 +85,20 @@ class NoteDetailView(UpdateView, LoggedInViewWithUser): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['notes_list'] = Note.objects.filter(owner=self.request.user) + context['notebook_form'] = NotebookForm return context + def form_valid(self, form): + self.object = form.save() + tags = serializers.serialize("json", self.object.tags.all()) + data = { + 'tags': tags, + 'notebook': {'name': self.object.notebook.name, 'color': self.object.notebook.color_rgb} + } + return JsonResponse(data, safe=True) + -class NoteCreateView(CreateView, LoggedInViewWithUser): +class NoteCreateView(LoggedInViewWithUser, CreateView): model = Note form_class = NoteForm template_name = 'notes/notes_create.html' @@ -99,27 +113,76 @@ class NoteCreateView(CreateView, LoggedInViewWithUser): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['notes_list'] = Note.objects.filter(owner=self.request.user).select_related() + context['notebook_form'] = NotebookForm + # context['notes_list'] = Note.objects.filter(owner=self.request.user).select_related() + return context + + +class NoteTagView(BaseListView): + model = Note + template_name = 'notes/notes_list.html' + + def get_queryset(self): + ''' + This can generate a crazy amount of joins if there's a lot of tags + have to keep an eye on it. Would be better to do: + from django.db.models import Q + from functools import reduce + from operator import and_, or_ + #query = reduce(and_, (Q(tags__slug=t) for t in self.tag_list)) + # Note.objects.filter(query, owner=self.request.user) + But that doesn't work for some reason. + ''' + if not self.request.user.is_anonymous: + try: + tags = self.kwargs['slug'].split("+") + except ValueError: + tags = self.kwargs['slug'] + self.tag_list = [x.strip() for x in tags] + qs = Note.objects.prefetch_related('tags').filter(owner=self.request.user).select_related('notebook') + for tag in self.tag_list: + qs = qs.filter(tags__slug=tag) + return qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + #context['notes_list'] = Note.objects.filter(owner=self.request.user) + context['tags'] = LuxTag.objects.filter(slug__in=self.tag_list) return context -class NotebookListView(CreateView, LoggedInViewWithUser): +################## +# Notebook Views +################## + + +class NotebookListView(LoggedInViewWithUser, CreateView): model = Notebook form_class = NotebookForm - template_name = 'notes/notebook_create.html' + template_name = 'notes/notebook_list.html' def get_queryset(self): if not self.request.user.is_anonymous: return Notebook.objects.filter(owner=self.request.user) + def form_valid(self, form): + form.instance.owner = self.request.user + self.object = form.save() + return super(NotebookListView, self).form_valid(form) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['notebook_list'] = Notebook.objects.filter(owner=self.request.user) - context['notes_list'] = Note.objects.filter(owner=self.request.user).select_related() + NotebookFormSet = modelformset_factory(Notebook, form=NotebookForm, extra=0) + context['notebook_form_list'] = NotebookFormSet(queryset=Notebook.objects.filter(owner=self.request.user).exclude(name="Trash").select_related().annotate(note_count=Count('note'))) + #context['notebook_list'] = Notebook.objects.filter(owner=self.request.user).exclude(name="Trash").select_related().annotate(note_count=Count('note')) + #context['notes_list'] = Note.objects.filter(owner=self.request.user).select_related() return context + def get_success_url(self): + return reverse_lazy('notebooks:detail', kwargs={'slug': self.object.slug}) + -class NotebookDetailView(DetailView, LoggedInViewWithUser): +class NotebookDetailView(BaseDetailView): model = Notebook def get_object(self): @@ -129,11 +192,16 @@ class NotebookDetailView(DetailView, LoggedInViewWithUser): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['notes_list'] = Note.objects.filter(owner=self.request.user).select_related() + #context['notes_list'] = Note.objects.filter(owner=self.request.user).select_related() context['form'] = self.form return context +################## +# API Views +################## + + class IsOwnerOrDeny(permissions.BasePermission): """ Custom permission to only allow owners to post to their endpoint diff --git a/apps/utils/views.py b/apps/utils/views.py index 595e102..ec3a902 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -1,6 +1,6 @@ from itertools import chain import json -from django.http import Http404, HttpResponse +from django.http import Http404, HttpResponse, JsonResponse from django.apps import apps from django.views.generic import ListView from django.views.generic.base import View, RedirectView @@ -42,6 +42,31 @@ class LoggedInViewWithUser(View): return kwargs + +class AjaxableResponseMixin: + """ + Mixin to add AJAX support to a form. + Must be used with an object-based FormView (e.g. CreateView) + """ + def form_invalid(self, form): + response = super().form_invalid(form) + if self.request.is_ajax(): + return JsonResponse(form.errors, status=400) + else: + return response + + def form_valid(self, form): + # We make sure to call the parent's form_valid() method because + # it might do some processing (in the case of CreateView, it will + # call form.save() for example). + response = super().form_valid(form) + if self.request.is_ajax(): + data = { + 'pk': self.object.pk, + } + return JsonResponse(data) + else: + return response ''' class TagAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): diff --git a/apps/utils/widgets.py b/apps/utils/widgets.py index f4a7a4a..2745932 100644 --- a/apps/utils/widgets.py +++ b/apps/utils/widgets.py @@ -2,8 +2,9 @@ import os from django import forms from django.contrib import admin from django.contrib.admin.widgets import AdminFileWidget -from django.contrib.gis.admin import OSMGeoAdmin from django.utils.safestring import mark_safe +from django.forms import widgets +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string from django.template import Context @@ -130,15 +131,21 @@ class LGEntryFormSmall(forms.ModelForm): } -class OLAdminBase(OSMGeoAdmin): - default_lon = -9285175 - default_lat = 4025046 - default_zoom = 15 - units = True - scrollable = False - map_width = 700 - map_height = 425 - map_template = 'gis/admin/osm.html' - openlayers_url = '/static/admin/js/OpenLayers.js' - - +class RelatedFieldWidgetCanAdd(widgets.Select): + """ + Modifies standard django Select widget to add link after to add new instance + of related model (doesn't check permissions, that's for the form instance) + """ + def __init__(self, related_model, related_url=None, *args, **kw): + super(RelatedFieldWidgetCanAdd, self).__init__(*args, **kw) + if not related_url: + rel_to = related_model + info = (rel_to._meta.app_label, rel_to._meta.object_name.lower()) + related_url = 'admin:%s_%s_add' % info + self.related_url = related_url + + def render(self, name, value, *args, **kwargs): + self.related_url = reverse(self.related_url) + output = [super(RelatedFieldWidgetCanAdd, self).render(name, value, *args, **kwargs)] + output.append('New' % (self.related_url, name, name, name.capitalize())) + return mark_safe(u''.join(output)) diff --git a/config/base_urls.py b/config/base_urls.py index 775f07d..efbe233 100644 --- a/config/base_urls.py +++ b/config/base_urls.py @@ -45,7 +45,6 @@ urlpatterns += [ #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 = [ @@ -55,4 +54,3 @@ if settings.DEBUG: # url(r'^__debug__/', include(debug_toolbar.urls)), ] + urlpatterns -''' diff --git a/config/settings.py b/config/settings.py index 8e2ba3c..c7bef91 100644 --- a/config/settings.py +++ b/config/settings.py @@ -65,7 +65,7 @@ THIRD_PARTY_APPS = [ 'taggit_serializer', 'django_extensions', 'rest_framework', - #'debug_toolbar' + 'debug_toolbar' ] LOCAL_APPS = [ 'utils', @@ -82,7 +82,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - #'debug_toolbar.middleware.DebugToolbarMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', diff --git a/design/sass/_forms.scss b/design/sass/_forms.scss index cce0e0f..d8a6a03 100644 --- a/design/sass/_forms.scss +++ b/design/sass/_forms.scss @@ -148,6 +148,10 @@ table { } .btn-inline { display: inline; + width: auto; +} +.note-save { + float: right; } .form-narrow { margin: 0 auto; @@ -171,7 +175,14 @@ select { background: white; border-radius: 4px; } -#id_tags { +.note-detail #id_tags { @include fontsize(13); padding: 8px; } +#fs-notebook{ + label { + top: -1.5rem; + left: .25rem; + } + margin: 3rem 0 1.5rem; +} diff --git a/design/sass/_global.scss b/design/sass/_global.scss index 9ef2e99..b858866 100644 --- a/design/sass/_global.scss +++ b/design/sass/_global.scss @@ -90,7 +90,7 @@ blockquote:before { @include fontsize(68); content: '\201C'; position: absolute; - top: -1rem; + top: -1.35rem; left: 50%; transform: translate(-50%, -50%); width: 3rem; @@ -152,7 +152,7 @@ h3 { } } .wrapper { - @include constrain(1440px); + @include constrain(1280px); //margin-top: 5rem; } //************** Universals ************************ @@ -189,6 +189,12 @@ h3 { .right-padding-0 { padding-right: 0 !important; } +.top-margin-0 { + margin-top: 0 !important; +} +.bottom-margin-0 { + margin-bottom: 0 !important; +} .center { text-align: center; margin-right: auto; @@ -201,9 +207,15 @@ h3 { .vertical li { display: block; } +.block { + display: block; +} +.inline-block { + display: inline-block; +} .single-col { display: block; - @include constrain_narrow; + @include constrain(66%); } .wide{ display: block; @@ -212,6 +224,12 @@ h3 { .small > * { @include fontsize(14); } +.hed-small { + @include fontsize(22); + @include fancy_sans; + margin-bottom: .5rem; + margin-top: 2rem; +} //************** other global classes ************************ .sans { @include generic_sans; diff --git a/design/sass/_modal.scss b/design/sass/_modal.scss index 57a7e51..dd21816 100644 --- a/design/sass/_modal.scss +++ b/design/sass/_modal.scss @@ -1,112 +1,103 @@ -/** - * Component: Overlay - */ -/* BACKDROP */ -.novi-backdrop { - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 7000; - position: fixed; - overflow-x: hidden; - overflow-y: auto; - background: rgba(0, 0, 0, 0.75); - opacity: 0; - animation-name: fadeIn; - animation-duration: .4s; - animation-fill-mode: forwards; +#overlay{ + font-family:Lato; + position:fixed; + width:100vw; + height:100vh; + overflow:hidden; + top:0; + left:0; + right:0; + bottom:0; + animation:overlay .3s forwards ease; + background-color:rgba(0,0,0,.8); + transform:scale(1); + transform-origin:center center; + z-index: 2000; + display: block; + > div { + position:absolute; + top:50%; + left:50%; + transform:translate(-50%,-50%); + } + header{ + @include fancy_sans; + @include fontsize(18); + padding: 1rem 1rem 1rem 0; + margin-left: 0; + } } -/* OVERLAY */ -.novi-overlay { - text-align: center; - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0; +.top { + z-index: 10000; } -.novi-overlay:before { - content: ''; - display: inline-block; - height: 10%; - vertical-align: middle; -} -.novi-overlay__container { - width: 100%; - position: relative; - display: inline-block; - vertical-align: middle; - margin: 0 auto; - text-align: left; - z-index: 8000; - padding: 0 15px; -} -.novi-overlay__content { - position: relative; - background: #FFF; - padding: 40px; - width: auto; - margin: 15px auto; - width: 100%; - max-width: 700px; - animation-name: fadeZoomIn; - animation-duration: .4s; - opacity: 0; - animation-fill-mode: forwards; - animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); - border-radius: 8px; -} -.novi-overlay__content--video { - padding: 0; - height: 360px; -} -.novi-overlay__content--video .novi-overlay-close { - top: -25px; - right: 0; -} -/* CLOSE BUTTON */ -.novi-overlay-close { - padding: 0; - background: none; - position: absolute; - top: 15px; - right: 15px; - display: block; - width: 15px; - height: 15px; - z-index: 1; - border: 0; - background-size: 100%; - background-repeat: no-repeat; - background-position: 100% 0; - background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDIxLjkgMjEuOSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjEuOSAyMS45IiB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4Ij4KICA8cGF0aCBkPSJNMTQuMSwxMS4zYy0wLjItMC4yLTAuMi0wLjUsMC0wLjdsNy41LTcuNWMwLjItMC4yLDAuMy0wLjUsMC4zLTAuN3MtMC4xLTAuNS0wLjMtMC43bC0xLjQtMS40QzIwLDAuMSwxOS43LDAsMTkuNSwwICBjLTAuMywwLTAuNSwwLjEtMC43LDAuM2wtNy41LDcuNWMtMC4yLDAuMi0wLjUsMC4yLTAuNywwTDMuMSwwLjNDMi45LDAuMSwyLjYsMCwyLjQsMFMxLjksMC4xLDEuNywwLjNMMC4zLDEuN0MwLjEsMS45LDAsMi4yLDAsMi40ICBzMC4xLDAuNSwwLjMsMC43bDcuNSw3LjVjMC4yLDAuMiwwLjIsMC41LDAsMC43bC03LjUsNy41QzAuMSwxOSwwLDE5LjMsMCwxOS41czAuMSwwLjUsMC4zLDAuN2wxLjQsMS40YzAuMiwwLjIsMC41LDAuMywwLjcsMC4zICBzMC41LTAuMSwwLjctMC4zbDcuNS03LjVjMC4yLTAuMiwwLjUtMC4yLDAuNywwbDcuNSw3LjVjMC4yLDAuMiwwLjUsMC4zLDAuNywwLjNzMC41LTAuMSwwLjctMC4zbDEuNC0xLjRjMC4yLTAuMiwwLjMtMC41LDAuMy0wLjcgIHMtMC4xLTAuNS0wLjMtMC43TDE0LjEsMTEuM3oiIGZpbGw9IiMwMDAwMDAiLz4KPC9zdmc+Cg==); +@keyframes modal{ + from{ + -webkit-transform: scale(0.5); + -moz-transform: scale(0.5); + -ms-transform: scale(0.5); + transform: scale(0.5); + opacity: 0; + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + transition: all 0.4s; + }; + to{ + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + opacity: 1; + } } -.novi-overlay-close:hover, -.novi-overlay-close:focus, -.novi-overlay-close:active { - outline: none; - cursor: pointer; +@keyframes overlay{ + from{ + background-color:rgba(0,0,0,0); + }; + to{ + background-color:rgba(0,0,0,.8); + } } -/* HELPER CLASSES */ -.no-scroll { - overflow: hidden; +#overlay-wrapper { + max-width: 52%; + width: 90%; } -@keyframes fadeZoomIn { - from { - opacity: 0; - transform: scale(0.5) translateY(300px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } +#modal { + min-height: 330px; + padding: 0 1rem 1rem 1rem; + background-color: white; + border-radius: 4px; + overflow:hidden; + animation:modal .2s forwards ease; + -webkit-box-shadow: 0px 2px 16px 2px rgba(0,0,0,0.5); + -moz-box-shadow: 0px 2px 16px 2px rgba(0,0,0,0.5); + box-shadow: 0px 2px 16px 2px rgba(0,0,0,0.5); + + + & > div { + color: #424242; + background-color: white; + } + // specific fixes for notebook create form + #nb-create-form { width: 99%;} + .flex-wrapper { + display: block; + margin-top: .5rem; + #color-picker { + margin-top: 3rem; + margin-left: .25rem; + } + .nb-name { width: 100%;} + } + input[type="submit"] { + float: right; + } + } -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } +#hed-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + & > * { + width: auto; + } } diff --git a/design/sass/_notes.scss b/design/sass/_notes.scss index cde15a5..4ab42b4 100644 --- a/design/sass/_notes.scss +++ b/design/sass/_notes.scss @@ -85,7 +85,7 @@ main { font-weight: normal; overflow: hidden; white-space: nowrap; - color: $body_font_color; + color: darken($body_font_color, 10); } .notebook { padding-right: 8px; @@ -176,18 +176,20 @@ main { input { width: 200%; } + a { margin-right: .25rem;} } } .notebook { display: block; } .note-container { - max-width: 60%; + max-width: 70%; position: relative; flex:1; order: 2; background: #fff; z-index: 4; + margin: 0 auto; } #note-body { @include fancy-sans; @@ -211,14 +213,16 @@ main { font-size:inherit; } } -#user-menu, #notebooks-menu { +#user-menu, #notebooks-menu, #notebook-drop-menu, #tags-drop-menu { display: none; } .active { display: block !important; } .notebook-colored { - border-left: 3px solid #fff; + -webkit-background-clip: padding-box; /* for Safari */ + background-clip: padding-box; /* for IE9+, Firefox 4+, Opera, Chrome */ + border-left: 3px solid rgba(255, 255, 255, .5); } .notebook-title { @include fontsize(24); @@ -249,6 +253,115 @@ main { cursor: pointer; } } +#nb-create-form { + .color-picker-fieldset { + width: 30px; + height: 30px; + label { + top: -25px; + width: 140px; + left: -5px; + } + } + .nb-name { + margin: 1rem 2rem 1rem 0; + width: 90%; +} +} +.small-circle { + width: 18px; + height: 18px; + margin-left: 6px; +} +.small-circle.plus:before { + width: 2px; + margin: 5px auto; +} +.small-circle.plus:after{ + margin: auto 5px; + width: 8px; +} + + +.url-field { + input { + @include fontsize(16); + color: $body_font_color; + } +} +.note-hed-wrapper { + margin-bottom: 1.5rem; +} +.note-hed { + @include fontsize(22); + margin-bottom: 0; +} +.note-subhed { + @include fontsize(16); + margin-top: 0; +} + +.nb-list { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-top: 0; +} +.nb-list-item { + list-style-type: none; + padding: 2rem; + margin: 1rem; + flex-grow: 1; + border: 1px #e7e2ee solid; + border-radius: 4px; + min-width: 160px; +} +.color-picker-inner { + width: 100%; + height: 100%; +} + +.dropmenu-search { + margin: 0; + padding: 0; + .dropmenu-search-wrapper { + border-top: 1px solid #e9e9e9; + border-bottom: 1px solid #e9e9e9; + padding: 5px; + } + input { + @include fontsize(16); + padding: 4px; + width: auto; + border: none; + } + .dropmenu-list { + padding: 3px; + margin-top: 0; + max-height: 300px; + overflow-x: auto; + } + a { + display: block; + text-decoration: none; + padding: 4px 6px; + &:hover { + background: $link_color; + } + } +} + +.ql-snow .ql-editor blockquote { + border-left: none !important; + margin-bottom: 5px; + margin-top: 5px; + padding-left: none !important; + padding: 1rem .5rem; +} +.ql-container { + min-height: 300px; +} + /* Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull diff --git a/design/sass/screenv1.scss b/design/sass/screenv1.scss index d54027e..f3df73e 100644 --- a/design/sass/screenv1.scss +++ b/design/sass/screenv1.scss @@ -1,10 +1,10 @@ @import "_fonts.scss"; @import "_mixins.scss"; @import "_queries.scss"; +@import "_awesomeplete.scss"; @import "_global.scss"; @import "_header.scss"; @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 50749cb..fc369ae 100644 --- a/design/templates/base.html +++ b/design/templates/base.html @@ -15,6 +15,7 @@ + {% block jsinclude %}{%endblock%} @@ -22,7 +23,7 @@