From db291b50c08717f337ac01db71d78627cac940c7 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sat, 5 Feb 2022 14:57:54 +0100 Subject: [PATCH] Add simpletodo htmx example app --- server/config/settings/base.py | 2 + server/config/urls.py | 28 ++--- server/example.env | 2 +- server/requirements/requirements-dev.txt | 3 + server/requirements/requirements.in | 1 + server/requirements/requirements.txt | 3 + server/vbv_lernwelt/core/tests/factories.py | 2 +- server/vbv_lernwelt/simpletodo/admin.py | 38 +++++- server/vbv_lernwelt/simpletodo/apps.py | 4 +- .../simpletodo/migrations/0001_initial.py | 90 +++++++++++--- server/vbv_lernwelt/simpletodo/models.py | 3 + server/vbv_lernwelt/simpletodo/serializers.py | 23 ++++ .../templates/simpletodo/index.html | 14 +++ .../simpletodo/partials/add_task_form.html | 21 ++++ .../simpletodo/partials/simple_list.html | 13 ++ .../templates/simpletodo/partials/task.html | 35 ++++++ server/vbv_lernwelt/simpletodo/tests.py | 3 - .../vbv_lernwelt/simpletodo/tests/__init__.py | 0 .../simpletodo/tests/test_serializers.py | 24 ++++ server/vbv_lernwelt/simpletodo/urls.py | 13 ++ server/vbv_lernwelt/simpletodo/views.py | 86 ++++++++++++- server/vbv_lernwelt/static/sass/project.scss | 4 + server/vbv_lernwelt/templates/base.html | 113 ++---------------- server/vbv_lernwelt/templates/pages/home.html | 107 +++++++++++++++++ 24 files changed, 481 insertions(+), 151 deletions(-) create mode 100644 server/vbv_lernwelt/simpletodo/serializers.py create mode 100644 server/vbv_lernwelt/simpletodo/templates/simpletodo/index.html create mode 100644 server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/add_task_form.html create mode 100644 server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/simple_list.html create mode 100644 server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/task.html delete mode 100644 server/vbv_lernwelt/simpletodo/tests.py create mode 100644 server/vbv_lernwelt/simpletodo/tests/__init__.py create mode 100644 server/vbv_lernwelt/simpletodo/tests/test_serializers.py create mode 100644 server/vbv_lernwelt/simpletodo/urls.py diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 4afdb9de..5fabe732 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -82,6 +82,7 @@ THIRD_PARTY_APPS = [ "rest_framework.authtoken", "corsheaders", "drf_spectacular", + "django_htmx", ] LOCAL_APPS = [ @@ -145,6 +146,7 @@ MIDDLEWARE = [ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.common.BrokenLinkEmailsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_htmx.middleware.HtmxMiddleware", "vbv_lernwelt.core.middleware.auth.AuthenticationRequiredMiddleware", "vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware", ] diff --git a/server/config/urls.py b/server/config/urls.py index a353b863..23311735 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -11,26 +11,15 @@ from rest_framework.authtoken.views import obtain_auth_token from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt +# fmt: off urlpatterns = [ - path( - "", - django_view_authentication_exempt( - TemplateView.as_view(template_name="pages/home.html") - ), - name="home", - ), - path( - "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" - ), + path("", django_view_authentication_exempt(TemplateView.as_view(template_name="pages/home.html")), name="home"), + path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"), # Django Admin, use {% url 'admin:index' %} path(settings.ADMIN_URL, admin.site.urls), # Your stuff: custom urls includes go here - path( - "login/", - django_view_authentication_exempt( - auth_views.LoginView.as_view(template_name="core/login.html") - ), - ), + path("login/", django_view_authentication_exempt(auth_views.LoginView.as_view(template_name="core/login.html"))), + path("todo/", include("vbv_lernwelt.simpletodo.urls")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development @@ -43,12 +32,9 @@ urlpatterns += [ # DRF auth token path("auth-token/", obtain_auth_token), path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"), - path( - "api/docs/", - SpectacularSwaggerView.as_view(url_name="api-schema"), - name="api-docs", - ), + path("api/docs/", SpectacularSwaggerView.as_view(url_name="api-schema"), name="api-docs",), ] +# fmt: on if settings.DEBUG: # This allows the error pages to be debugged during development, just visit diff --git a/server/example.env b/server/example.env index f5e90aee..1c3ab747 100644 --- a/server/example.env +++ b/server/example.env @@ -1,3 +1,3 @@ export VBV_DATABASE_URL='postgres://vbv_lernwelt@localhost:5432/vbv_lernwelt' -#export VBV_DJANGO_LOGGING_CONF=VBV_DJANGO_LOGGING_CONF_CONSOLE_COLOR +export VBV_DJANGO_LOGGING_CONF=VBV_DJANGO_LOGGING_CONF_CONSOLE_COLOR export VBV_DJANGO_DEBUG=True diff --git a/server/requirements/requirements-dev.txt b/server/requirements/requirements-dev.txt index 59276d67..700275ed 100644 --- a/server/requirements/requirements-dev.txt +++ b/server/requirements/requirements-dev.txt @@ -69,6 +69,7 @@ django==3.2.12 # django-cors-headers # django-debug-toolbar # django-extensions + # django-htmx # django-model-utils # django-redis # django-stubs @@ -83,6 +84,8 @@ django-debug-toolbar==3.2.4 # via -r requirements-dev.in django-extensions==3.1.5 # via -r requirements-dev.in +django-htmx==1.8.0 + # via -r requirements.in django-model-utils==4.2.0 # via -r requirements.in django-redis==5.2.0 diff --git a/server/requirements/requirements.in b/server/requirements/requirements.in index 631571f5..3cebca46 100644 --- a/server/requirements/requirements.in +++ b/server/requirements/requirements.in @@ -19,6 +19,7 @@ djangorestframework # https://github.com/encode/django-rest-framework django-cors-headers # https://github.com/adamchainz/django-cors-headers # DRF-spectacular for api documentation drf-spectacular +django-htmx psycopg2-binary gunicorn diff --git a/server/requirements/requirements.txt b/server/requirements/requirements.txt index 81bd8fd2..ee060681 100644 --- a/server/requirements/requirements.txt +++ b/server/requirements/requirements.txt @@ -30,12 +30,15 @@ django==3.2.12 # via # -r requirements.in # django-cors-headers + # django-htmx # django-model-utils # django-redis # djangorestframework # drf-spectacular django-cors-headers==3.11.0 # via -r requirements.in +django-htmx==1.8.0 + # via -r requirements.in django-model-utils==4.2.0 # via -r requirements.in django-redis==5.2.0 diff --git a/server/vbv_lernwelt/core/tests/factories.py b/server/vbv_lernwelt/core/tests/factories.py index dfe56660..02da3fbc 100644 --- a/server/vbv_lernwelt/core/tests/factories.py +++ b/server/vbv_lernwelt/core/tests/factories.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password -class SimpleUserFactory(factory.django.DjangoModelFactory): +class UserFactory(factory.django.DjangoModelFactory): class Meta: model = get_user_model() django_get_or_create = ("username",) diff --git a/server/vbv_lernwelt/simpletodo/admin.py b/server/vbv_lernwelt/simpletodo/admin.py index 8c38f3f3..e18c8a68 100644 --- a/server/vbv_lernwelt/simpletodo/admin.py +++ b/server/vbv_lernwelt/simpletodo/admin.py @@ -1,3 +1,39 @@ +# Register your models here. from django.contrib import admin -# Register your models here. +from .models import SimpleTask, SimpleList + + +@admin.register(SimpleList) +class SimpleListAdmin(admin.ModelAdmin): + list_display = [ + "title", + "user", + "created", + ] + list_filter = [ + "user", + ] + search_fields = [ + "title", + ] + + +@admin.register(SimpleTask) +class SimpleTaskAdmin(admin.ModelAdmin): + date_hierarchy = "deadline" + list_display = [ + "title", + "deadline", + "created", + "list", + "done", + ] + list_filter = [ + "list", + "done", + ] + search_fields = [ + "title", + "text", + ] diff --git a/server/vbv_lernwelt/simpletodo/apps.py b/server/vbv_lernwelt/simpletodo/apps.py index 4524d7c3..45efffa1 100644 --- a/server/vbv_lernwelt/simpletodo/apps.py +++ b/server/vbv_lernwelt/simpletodo/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class SimpletodoConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'vbv_lernwelt.simpletodo' + default_auto_field = "django.db.models.BigAutoField" + name = "vbv_lernwelt.simpletodo" diff --git a/server/vbv_lernwelt/simpletodo/migrations/0001_initial.py b/server/vbv_lernwelt/simpletodo/migrations/0001_initial.py index 0ebfc0cb..e850a29e 100644 --- a/server/vbv_lernwelt/simpletodo/migrations/0001_initial.py +++ b/server/vbv_lernwelt/simpletodo/migrations/0001_initial.py @@ -18,32 +18,88 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='SimpleList', + name="SimpleList", fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('title', models.CharField(max_length=255)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("title", models.CharField(max_length=255)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='SimpleTask', + name="SimpleTask", fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('title', models.CharField(max_length=255)), - ('text', models.TextField(blank=True, default='')), - ('done', models.BooleanField(default=False)), - ('deadline', models.DateTimeField(blank=True, null=True)), - ('list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='simpletodo.simplelist')), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("title", models.CharField(max_length=255)), + ("text", models.TextField(blank=True, default="")), + ("done", models.BooleanField(default=False)), + ("deadline", models.DateTimeField(blank=True, null=True)), + ( + "list", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="simpletodo.simplelist", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/server/vbv_lernwelt/simpletodo/models.py b/server/vbv_lernwelt/simpletodo/models.py index bc06a0bb..042b46b1 100644 --- a/server/vbv_lernwelt/simpletodo/models.py +++ b/server/vbv_lernwelt/simpletodo/models.py @@ -10,6 +10,9 @@ class SimpleList(TimeStampedModel): title = models.CharField(max_length=255) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + def __str__(self): + return f"{self.title} ({self.user})" + class SimpleTask(TimeStampedModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/server/vbv_lernwelt/simpletodo/serializers.py b/server/vbv_lernwelt/simpletodo/serializers.py new file mode 100644 index 00000000..bb58d995 --- /dev/null +++ b/server/vbv_lernwelt/simpletodo/serializers.py @@ -0,0 +1,23 @@ +from rest_framework import serializers +from rest_framework.serializers import ModelSerializer + +from vbv_lernwelt.simpletodo.models import SimpleTask, SimpleList + + +class SimpleTaskSerializer(ModelSerializer): + list_title = serializers.CharField(max_length=100) + + class Meta: + model = SimpleTask + fields = ['id', 'title', 'text', 'done', 'deadline', 'list_title', ] + + def create(self, validated_data): + user = validated_data.pop('user', None) + if user is None: + raise serializers.ValidationError('User is required') + + list_title = validated_data.pop('list_title') + simple_list, _ = SimpleList.objects.get_or_create(title=list_title, user=user) + + validated_data['list'] = simple_list + return super().create(validated_data) diff --git a/server/vbv_lernwelt/simpletodo/templates/simpletodo/index.html b/server/vbv_lernwelt/simpletodo/templates/simpletodo/index.html new file mode 100644 index 00000000..13b6df51 --- /dev/null +++ b/server/vbv_lernwelt/simpletodo/templates/simpletodo/index.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + + +{% block content %} + +
+

Hello Todos

+ + {% for list in simple_lists %} + {% include "simpletodo/partials/simple_list.html" with list=list%} + {% endfor %} +
+ +{% endblock %} diff --git a/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/add_task_form.html b/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/add_task_form.html new file mode 100644 index 00000000..0450e452 --- /dev/null +++ b/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/add_task_form.html @@ -0,0 +1,21 @@ +
+ {% csrf_token %} + + + + +
diff --git a/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/simple_list.html b/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/simple_list.html new file mode 100644 index 00000000..991ced46 --- /dev/null +++ b/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/simple_list.html @@ -0,0 +1,13 @@ +
+ +
+
+

{{ list.title }}

+ {% include "simpletodo/partials/add_task_form.html" with task=task %} +
+ {% for task in list.simpletask_set.all %} + {% include "simpletodo/partials/task.html" with task=task %} + {% endfor %} +
+ +
diff --git a/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/task.html b/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/task.html new file mode 100644 index 00000000..70d4371f --- /dev/null +++ b/server/vbv_lernwelt/simpletodo/templates/simpletodo/partials/task.html @@ -0,0 +1,35 @@ +
+
+ {% if task.done %} +

+ {% else %} +

+ {% endif %} + {{ task.title }} +

+ + + +
+
diff --git a/server/vbv_lernwelt/simpletodo/tests.py b/server/vbv_lernwelt/simpletodo/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/server/vbv_lernwelt/simpletodo/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/server/vbv_lernwelt/simpletodo/tests/__init__.py b/server/vbv_lernwelt/simpletodo/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/simpletodo/tests/test_serializers.py b/server/vbv_lernwelt/simpletodo/tests/test_serializers.py new file mode 100644 index 00000000..4399b3bb --- /dev/null +++ b/server/vbv_lernwelt/simpletodo/tests/test_serializers.py @@ -0,0 +1,24 @@ +from django.test import TestCase + +from vbv_lernwelt.core.tests.factories import UserFactory +from vbv_lernwelt.simpletodo.models import SimpleTask +from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer + + +class SimpleTaskSerializerTestCase(TestCase): + def setUp(self) -> None: + self.user = UserFactory() + + def test_serializer(self): + serializer = SimpleTaskSerializer(data={ + 'title': 'Test', + 'list_title': 'Todos', + }) + + serializer.is_valid(raise_exception=True) + serializer.save(user=self.user) + + task = SimpleTask.objects.first() + + self.assertEqual(task.title, 'Test') + self.assertEqual(task.list.title, 'Todos') diff --git a/server/vbv_lernwelt/simpletodo/urls.py b/server/vbv_lernwelt/simpletodo/urls.py new file mode 100644 index 00000000..b5810b6d --- /dev/null +++ b/server/vbv_lernwelt/simpletodo/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url, include +from django.urls import path +from rest_framework.routers import DefaultRouter + +from . import views + +router = DefaultRouter() +router.register(r'tasks', views.SimpleTaskViewSet, basename='tasks') + +urlpatterns = [ + path("", views.index, name="index"), + url(r'^api/', include(router.urls)), +] diff --git a/server/vbv_lernwelt/simpletodo/views.py b/server/vbv_lernwelt/simpletodo/views.py index 91ea44a2..37adcdb6 100644 --- a/server/vbv_lernwelt/simpletodo/views.py +++ b/server/vbv_lernwelt/simpletodo/views.py @@ -1,3 +1,85 @@ -from django.shortcuts import render +from django.http import HttpResponse +from django.shortcuts import redirect, render +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer +from rest_framework.response import Response -# Create your views here. +from vbv_lernwelt.simpletodo.models import SimpleList, SimpleTask +from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer + + +def index(request): + simple_lists = SimpleList.objects.filter(user=request.user) + return render(request, 'simpletodo/index.html', { + 'simple_lists': simple_lists + }) + + +class SimpleTaskViewSet(viewsets.ModelViewSet): + serializer_class = SimpleTaskSerializer + renderer_classes = [TemplateHTMLRenderer, JSONRenderer] + + def get_queryset(self): + user = self.request.user + return SimpleTask.objects.filter(list__user=user) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + + if request.accepted_renderer.format == 'html': + serializer.is_valid(raise_exception=True) + else: + serializer.is_valid(raise_exception=True) + + serializer.save(user=request.user) + + if request.accepted_renderer.format == 'html': + return redirect("/todo/") + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.delete() + + if request.htmx: + return HttpResponse(status=200, content='') + + return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=True, methods=['post',]) + def toggle_done(self, request, pk=None): + task = self.get_object() + task.done = not task.done + task.save() + + if request.htmx: + return render(request, 'simpletodo/partials/task.html', { + 'task': task + }) + + return Response(self.get_serializer(task), status=status.HTTP_200_OK) + + # + # def get_category_from_request(self, request): + # cat_name = request.query_params.get('cat_name') + # category_obj = None + # + # if cat_name: + # category_obj = VideoCategory.objects.filter(category__iexact=cat_name).first() + # if not category_obj: + # category_obj = VideoCategory.objects.first() + # + # return category_obj + # + # @action(detail=False, methods=['get']) + # def form(self, request): + # category_obj = self.get_category_from_request(request) + # return Response(template_name='videos/partials/video_form.html', data={'category': category_obj}) + # + # @action(detail=False, methods=['get']) + # def cancel(self, request): + # category_obj = self.get_category_from_request(request) + # return Response(template_name='videos/partials/show_add_form.html', data={'category': category_obj}) diff --git a/server/vbv_lernwelt/static/sass/project.scss b/server/vbv_lernwelt/static/sass/project.scss index e69de29b..5e5e574b 100644 --- a/server/vbv_lernwelt/static/sass/project.scss +++ b/server/vbv_lernwelt/static/sass/project.scss @@ -0,0 +1,4 @@ +.task.htmx-swapping { + opacity: 0; + transition: opacity 1s ease-out; +} diff --git a/server/vbv_lernwelt/templates/base.html b/server/vbv_lernwelt/templates/base.html index 18ee129f..168c343a 100644 --- a/server/vbv_lernwelt/templates/base.html +++ b/server/vbv_lernwelt/templates/base.html @@ -20,6 +20,9 @@ {# Placed at the top of the document so pages load faster with defer #} + + + {% block javascript %} @@ -57,115 +60,19 @@ {% block content %} -
-
- -
-

Machen Sie hier eine Selbstevaluation

-

Hier steht noch etwas mehr Text

-
-
-
- -
-
- -
-
- image - -
-
- -
-
- image - -
-
- -
-
- image - -
-
-
-

weitere Kurse entdecken und buchen

- -
-
- - {% endblock content %} {% block modal %}{% endblock modal %} + + {% block inline_javascript %} + {% comment %} Script tags with only code, no src (defer by default). To run with a "defer" so that you run inline code: diff --git a/server/vbv_lernwelt/templates/pages/home.html b/server/vbv_lernwelt/templates/pages/home.html index 94d9808c..7095cd9d 100644 --- a/server/vbv_lernwelt/templates/pages/home.html +++ b/server/vbv_lernwelt/templates/pages/home.html @@ -1 +1,108 @@ {% extends "base.html" %} + + +{% block content %} +
+
+ +
+

Machen Sie hier eine Selbstevaluation

+

Hier steht noch etwas mehr Text

+
+
+
+ +
+
+ +
+
+ image + +
+
+ +
+
+ image + +
+
+ +
+
+ image + +
+
+
+

weitere Kurse entdecken und buchen

+ +
+
+ +{% endblock %}