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:
parent
0471f151b6
commit
42e9de6942
13 changed files with 123 additions and 12 deletions
|
@ -227,3 +227,7 @@ table.dataTable {
|
|||
.error-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.admin-header a {
|
||||
color: #9cdcfe;
|
||||
}
|
|
@ -30,6 +30,8 @@ boto3 = "*"
|
|||
argon2-cffi = "*"
|
||||
django-csp = "*"
|
||||
django-rest-polymorphic = "*"
|
||||
django-crispy-forms = "*"
|
||||
crispy-bootstrap5 = "*"
|
||||
|
||||
[tool.poetry.group.mysql.dependencies]
|
||||
mysqlclient = "*"
|
||||
|
|
|
@ -39,6 +39,8 @@ INSTALLED_APPS = [
|
|||
"django_celery_results",
|
||||
"drf_spectacular",
|
||||
"drf_spectacular_sidecar",
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
"quackscape",
|
||||
"quackscape.users",
|
||||
"quackscape.tours",
|
||||
|
@ -218,7 +220,7 @@ QUACKSCAPE_CONTENT_RESOLUTIONS = [
|
|||
(65536, 32768),
|
||||
]
|
||||
|
||||
MAX_IMAGE_PIXELS = 34359738368 # 262144x131072 - should be enough, no?
|
||||
MAX_IMAGE_PIXELS = 34359738368 # 262144x131072 - should be enough, no?
|
||||
|
||||
# ffmpeg settings
|
||||
|
||||
|
@ -248,4 +250,9 @@ FFMPEG_DEFAULT_OPTION = ASK.config.get("ffmpeg", "DefaultOption", fallback="defa
|
|||
|
||||
LOGIN_URL = reverse_lazy("quackscape.users:login")
|
||||
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
21
quackscape/tours/forms.py
Normal 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")
|
18
quackscape/tours/migrations/0018_category_description.py
Normal file
18
quackscape/tours/migrations/0018_category_description.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -22,6 +22,7 @@ def upload_to(instance, filename):
|
|||
class Category(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
title = models.CharField(max_length=100)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
owner = models.ForeignKey(
|
||||
get_user_model(),
|
||||
related_name="owned_categories",
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
<h2>Quackscape</h2>
|
||||
<div class="user-info">
|
||||
{% 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 %}
|
||||
<p>Not logged in</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="col-md-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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>
|
||||
<ul>
|
||||
{% for category in categories %}
|
||||
|
|
|
@ -3,9 +3,16 @@
|
|||
<h4>{{ category.title }}</h4>
|
||||
{% if category in request.user.category_memberships %}
|
||||
<button type="button" class="btn btn-danger">End category membership</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
|
@ -142,7 +149,9 @@
|
|||
<h5>User permissions</h5>
|
||||
<div>
|
||||
<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>
|
||||
<table id="permissionsTable" class="display">
|
||||
|
|
27
quackscape/users/templates/users/generic_form.html
Normal file
27
quackscape/users/templates/users/generic_form.html
Normal 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 %}
|
|
@ -6,7 +6,7 @@
|
|||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">Login</h4>
|
||||
<h4 class="mb-0">{{ title }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'quackscape.users:login' %}">
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -7,6 +7,7 @@ app_name = 'quackscape.users'
|
|||
urlpatterns = [
|
||||
path('', UserAreaMainView.as_view(), name='user-area-main'),
|
||||
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>/upload/', FileUploadView.as_view(), name='media-upload'),
|
||||
path('login/', Login.as_view(), name='login'),
|
||||
|
|
|
@ -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.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.views import LoginView, LogoutView
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.response import Response
|
||||
|
@ -14,6 +22,7 @@ from quackscape.tours.serializers import (
|
|||
OriginalImageSerializer,
|
||||
OriginalVideoSerializer,
|
||||
)
|
||||
from quackscape.tours.forms import CategoryForm
|
||||
|
||||
|
||||
class TitleMixin:
|
||||
|
@ -63,9 +72,20 @@ class MediaUploadView(LoginRequiredMixin, TitleMixin, TemplateView):
|
|||
title = "Upload Media"
|
||||
|
||||
|
||||
class CategoryCreateView(LoginRequiredMixin, TitleMixin, TemplateView):
|
||||
template_name = "users/category_create.html"
|
||||
class CategoryCreateView(LoginRequiredMixin, TitleMixin, CreateView):
|
||||
template_name = "users/generic_form.html"
|
||||
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):
|
||||
|
|
Loading…
Reference in a new issue