diff --git a/common/admin.py b/common/admin.py new file mode 100644 index 0000000..0994159 --- /dev/null +++ b/common/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from common.models import Draft + +admin.site.register(Draft) diff --git a/common/migrations/0001_initial.py b/common/migrations/0001_initial.py new file mode 100644 index 0000000..992904f --- /dev/null +++ b/common/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.5 on 2022-07-03 22:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Draft', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description_md', models.TextField(blank=True, verbose_name='Description')), + ('created', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='drafts', to=settings.AUTH_USER_MODEL, verbose_name='Player')), + ], + ), + ] diff --git a/common/migrations/__init__.py b/common/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/models/__init__.py b/common/models/__init__.py index c5b81cb..d4ccfe7 100644 --- a/common/models/__init__.py +++ b/common/models/__init__.py @@ -1,3 +1,4 @@ from .descriptionmodel import DescriptionModel from .nameslugmodel import NameSlugModel from .historymodel import HistoryModel +from .draft import Draft diff --git a/common/models/draft.py b/common/models/draft.py new file mode 100644 index 0000000..fae9366 --- /dev/null +++ b/common/models/draft.py @@ -0,0 +1,21 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from rpg_notes.settings import AUTH_USER_MODEL + + +class Draft(models.Model): + description_md = models.TextField(_("Description"), blank=True) + created = models.DateTimeField(auto_now_add=True) + last_modified = models.DateTimeField(auto_now=True) + author = models.ForeignKey( + AUTH_USER_MODEL, on_delete=models.PROTECT, + related_name="drafts", verbose_name=_("Player") + ) + + def __str__(self): + # todo: add which object this is a draft of + return f"{self.created}: {self.author}" + + class Meta: + get_latest_by = "created" diff --git a/common/urls.py b/common/urls.py new file mode 100644 index 0000000..edbfb41 --- /dev/null +++ b/common/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from common import views + +urlpatterns = [ + path("api/draft/save", views.save_draft, name="save_draft"), +] diff --git a/common/views.py b/common/views.py index d32f720..50e52e8 100644 --- a/common/views.py +++ b/common/views.py @@ -1,10 +1,13 @@ -from django.http import HttpResponse +import json + +from django.http import HttpResponse, HttpRequest, HttpResponseBadRequest, JsonResponse from django.shortcuts import render from django.views.decorators.http import condition from django.views.generic import TemplateView from ipware import get_client_ip from sentry_sdk import last_event_id +from common.models import Draft from rpg_notes.secrets import SENTRY_DSN from utils.assets import get_css, get_file_hash @@ -17,7 +20,7 @@ class LanguageSelectView(TemplateView): template_name = "common/languageselect.jinja" -def print_ip(request): +def print_ip(request: HttpRequest) -> HttpResponse: client_ip, is_routable = get_client_ip(request) return HttpResponse(repr(client_ip), content_type="text/plain") @@ -26,6 +29,28 @@ def calc_etag(*args, **kwargs): return get_file_hash()[:6] +def save_draft(request: HttpRequest) -> HttpResponse: + body = json.loads(request.body) + draft_md = body.get("draft_md", None) + if not draft_md: + return HttpResponseBadRequest() + try: + last_draft = Draft.objects.filter(author=request.user).latest() + if last_draft.description_md == draft_md: + return JsonResponse({ + "message": "saved (unchanged)" + }) + except Draft.DoesNotExist: + pass + draft = Draft() + draft.description_md = draft_md + draft.author = request.user + draft.save() + return JsonResponse({ + "message": "saved" + }) + + @condition(etag_func=calc_etag) def debug_css(request): css, source_map = get_css(debug=True) diff --git a/rpg_notes/urls.py b/rpg_notes/urls.py index f086194..d5f36a1 100644 --- a/rpg_notes/urls.py +++ b/rpg_notes/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ path('note/', include("notes.urls")), path('loot/', include("loot.urls")), path('search/', include("search.urls")), + path('', include("common.urls")), path('', include("campaigns.urls")) ] diff --git a/static/js/markdown.js b/static/js/markdown.js index bfa8edd..deea651 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -6,7 +6,7 @@ */ document.addEventListener('DOMContentLoaded', function () { - + const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value; const ids = ["id_description_md"]; ids.forEach(function (id) { const element = document.getElementById(id); @@ -33,7 +33,28 @@ document.addEventListener('DOMContentLoaded', function () { second: '2-digit', }, }, - } + }, + inputStyle: "contenteditable", + status: ["lines", "words", "cursor", "saveStatus"], }); + window.editor = easyMDE + setInterval(function () { + const content = easyMDE.value(); + fetch("/api/draft/save", { + method: "POST", + body: JSON.stringify({ + "draft_md": content + }), + headers: {'X-CSRFToken': csrftoken}, + }) + .then(response => response.json()) + .then(data => { + easyMDE.updateStatusBar("saveStatus", data.message) + setTimeout(e => easyMDE.updateStatusBar("saveStatus", ""), 5000) + }).catch(e => { + easyMDE.updateStatusBar("saveStatus", "error saving draft") + }) + + }, 1000 * 30) }); });