feat: enhance UI and extend form functionality

This update brings several enhancements to the user interface and
extends the forms functionality across the application. Specifically,
the changes include the introduction of `django-crispy-forms` and
`crispy-bootstrap5` to improve form aesthetics and usability
significantly. Additionally, the admin area now features enhanced link
color for better visibility, and user interface improvements have been
made across various templates, like adding direct edit and delete
category links and more intuitive navigation options for better user
experience. Moreover, the inclusion of new forms for Scene, Category,
and Original Media creation aligns with the app's need for structured
data entry and complements the existing models by ensuring a more
user-friendly interaction with the database.

Key changes include:
- Introduction of `django-crispy-forms` and `crispy-bootstrap5` for
better form rendering.
- UI enhancements for clarity and ease of use in the admin area and
templates.
- New forms for Scene, Category, and Original Media to streamline
content creation processes.

These changes aim to improve both the appearance and functionality of
the application, making it more appealing and accessible to users while
facilitating easier content management.
This commit is contained in:
Kumi 2024-03-27 09:22:04 +01:00
parent 0471f151b6
commit 42e9de6942
Signed by: kumi
GPG key ID: ECBCC9082395383F
13 changed files with 123 additions and 12 deletions

View file

@ -227,3 +227,7 @@ table.dataTable {
.error-message { .error-message {
color: red; color: red;
} }
.admin-header a {
color: #9cdcfe;
}

View file

@ -30,6 +30,8 @@ boto3 = "*"
argon2-cffi = "*" argon2-cffi = "*"
django-csp = "*" django-csp = "*"
django-rest-polymorphic = "*" django-rest-polymorphic = "*"
django-crispy-forms = "*"
crispy-bootstrap5 = "*"
[tool.poetry.group.mysql.dependencies] [tool.poetry.group.mysql.dependencies]
mysqlclient = "*" mysqlclient = "*"

View file

@ -39,6 +39,8 @@ INSTALLED_APPS = [
"django_celery_results", "django_celery_results",
"drf_spectacular", "drf_spectacular",
"drf_spectacular_sidecar", "drf_spectacular_sidecar",
"crispy_forms",
"crispy_bootstrap5",
"quackscape", "quackscape",
"quackscape.users", "quackscape.users",
"quackscape.tours", "quackscape.tours",
@ -218,7 +220,7 @@ QUACKSCAPE_CONTENT_RESOLUTIONS = [
(65536, 32768), (65536, 32768),
] ]
MAX_IMAGE_PIXELS = 34359738368 # 262144x131072 - should be enough, no? MAX_IMAGE_PIXELS = 34359738368 # 262144x131072 - should be enough, no?
# ffmpeg settings # ffmpeg settings
@ -248,4 +250,9 @@ FFMPEG_DEFAULT_OPTION = ASK.config.get("ffmpeg", "DefaultOption", fallback="defa
LOGIN_URL = reverse_lazy("quackscape.users:login") LOGIN_URL = reverse_lazy("quackscape.users:login")
LOGIN_REDIRECT_URL = reverse_lazy("quackscape.users:categories") LOGIN_REDIRECT_URL = reverse_lazy("quackscape.users:categories")
LOGOUT_REDIRECT_URL = reverse_lazy("quackscape.users:login") LOGOUT_REDIRECT_URL = reverse_lazy("quackscape.users:login")
# Crispy forms settings
CRISPY_ALLOWED_TEMPLATE_PACKS = {"bootstrap5"}
CRISPY_TEMPLATE_PACK = "bootstrap5"

21
quackscape/tours/forms.py Normal file
View file

@ -0,0 +1,21 @@
from django.forms import ModelForm
from .models import Scene, Element, Category, OriginalMedia
class SceneForm(ModelForm):
class Meta:
model = Scene
fields = ("title", "description")
class CategoryForm(ModelForm):
class Meta:
model = Category
fields = ("title", "description")
class OriginalMediaForm(ModelForm):
class Meta:
model = OriginalMedia
fields = ("title", "description")

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.3 on 2024-03-27 07:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tours', '0017_originalvideo_thumbnail'),
]
operations = [
migrations.AddField(
model_name='category',
name='description',
field=models.TextField(blank=True, null=True),
),
]

View file

@ -22,6 +22,7 @@ def upload_to(instance, filename):
class Category(models.Model): class Category(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
description = models.TextField(null=True, blank=True)
owner = models.ForeignKey( owner = models.ForeignKey(
get_user_model(), get_user_model(),
related_name="owned_categories", related_name="owned_categories",

View file

@ -11,7 +11,8 @@
<h2>Quackscape</h2> <h2>Quackscape</h2>
<div class="user-info"> <div class="user-info">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p>Logged in as <strong>{{ user.email }}</strong></p> <p>Logged in as <strong>{{ user.email }}</strong>
<a href="{% url "quackscape.users:logout" %}">Logout</a></p>
{% else %} {% else %}
<p>Not logged in</p> <p>Not logged in</p>
{% endif %} {% endif %}

View file

@ -15,7 +15,7 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h4>Available categories</h4> <h4>Available categories</h4>
<button type="button" class="btn btn-primary">Create category</button> <a href="{% url "quackscape.users:category-create" %}" class="btn btn-primary">Create category</a>
</div> </div>
<ul> <ul>
{% for category in categories %} {% for category in categories %}

View file

@ -3,9 +3,16 @@
<h4>{{ category.title }}</h4> <h4>{{ category.title }}</h4>
{% if category in request.user.category_memberships %} {% if category in request.user.category_memberships %}
<button type="button" class="btn btn-danger">End category membership</button> <button type="button" class="btn btn-danger">End category membership</button>
{% endif %} {% endif %}
{% if request.user.is_superuser or request.user == category.owner %} {% if request.user.is_superuser or request.user == category.owner %}
<button type="button" class="btn btn-danger">Delete category</button> <div>
<a href="/tours/category/{{ category.id }}/edit/" class="btn btn-primary"
>Edit category</a
>
<a href="/tours/category/{{ category.id }}/delete/" class="btn btn-danger"
>Delete category</a
>
</div>
{% endif %} {% endif %}
</div> </div>
@ -142,7 +149,9 @@
<h5>User permissions</h5> <h5>User permissions</h5>
<div> <div>
<button type="button" class="btn btn-primary">Invite user</button> <button type="button" class="btn btn-primary">Invite user</button>
<button type="button" class="btn btn-primary">Transfer ownership</button> <button type="button" class="btn btn-primary">
Transfer ownership
</button>
</div> </div>
</div> </div>
<table id="permissionsTable" class="display"> <table id="permissionsTable" class="display">

View file

@ -0,0 +1,27 @@
{% extends "users/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">{{ title }}</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }} {% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %} {{ error }} {% endfor %}
</div>
{% endif %}
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -6,7 +6,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4 class="mb-0">Login</h4> <h4 class="mb-0">{{ title }}</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" action="{% url 'quackscape.users:login' %}"> <form method="post" action="{% url 'quackscape.users:login' %}">

View file

@ -1,4 +1,4 @@
from .views import UserAreaMainView, CategoriesView, CategoryView, FileUploadView, Login, Logout from .views import UserAreaMainView, CategoriesView, CategoryView, FileUploadView, Login, Logout, CategoryCreateView
from django.urls import path from django.urls import path
@ -7,6 +7,7 @@ app_name = 'quackscape.users'
urlpatterns = [ urlpatterns = [
path('', UserAreaMainView.as_view(), name='user-area-main'), path('', UserAreaMainView.as_view(), name='user-area-main'),
path('categories/', CategoriesView.as_view(), name='categories'), path('categories/', CategoriesView.as_view(), name='categories'),
path('category/create/', CategoryCreateView.as_view(), name='category-create'),
path('category/<uuid:category>/', CategoryView.as_view(), name='category'), path('category/<uuid:category>/', CategoryView.as_view(), name='category'),
path('category/<uuid:category>/upload/', FileUploadView.as_view(), name='media-upload'), path('category/<uuid:category>/upload/', FileUploadView.as_view(), name='media-upload'),
path('login/', Login.as_view(), name='login'), path('login/', Login.as_view(), name='login'),

View file

@ -1,7 +1,15 @@
from django.views.generic import TemplateView, ListView, DetailView from django.views.generic import (
TemplateView,
ListView,
DetailView,
CreateView,
UpdateView,
DeleteView,
)
from django.http import Http404 from django.http import Http404
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView from django.contrib.auth.views import LoginView, LogoutView
from django.urls import reverse_lazy
from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response from rest_framework.response import Response
@ -14,6 +22,7 @@ from quackscape.tours.serializers import (
OriginalImageSerializer, OriginalImageSerializer,
OriginalVideoSerializer, OriginalVideoSerializer,
) )
from quackscape.tours.forms import CategoryForm
class TitleMixin: class TitleMixin:
@ -63,9 +72,20 @@ class MediaUploadView(LoginRequiredMixin, TitleMixin, TemplateView):
title = "Upload Media" title = "Upload Media"
class CategoryCreateView(LoginRequiredMixin, TitleMixin, TemplateView): class CategoryCreateView(LoginRequiredMixin, TitleMixin, CreateView):
template_name = "users/category_create.html" template_name = "users/generic_form.html"
title = "Create Category" title = "Create Category"
form_class = CategoryForm
model = Category
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"quackscape.users:category", kwargs={"category": self.object.id}
)
class FileUploadView(LoginRequiredMixin, GenericAPIView): class FileUploadView(LoginRequiredMixin, GenericAPIView):