Compare commits

...

2 commits

Author SHA1 Message Date
42e9de6942
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.
2024-03-27 09:22:04 +01:00
0471f151b6
feat(tours): add thumbnail field to OriginalVideo
Added an optional 'thumbnail' FileField to the OriginalVideo model and
created a corresponding migration to allow storing video thumbnails
directly within the database. This change enables more efficient access
to video thumbnails without needing to generate them on the fly,
improving loading times and reducing server load. The Scene model's
'thumbnail' property has also been updated to leverage the new field,
thus unifying the thumbnail retrieval process across different media
types. This modification is expected to enhance the user experience by
providing quicker access to media previews.
2024-03-27 08:03:23 +01:00
14 changed files with 149 additions and 14 deletions

View file

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

View file

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

View file

@ -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
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:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tours', '0016_alter_categorypermission_category_and_more'),
]
operations = [
migrations.AddField(
model_name='originalvideo',
name='thumbnail',
field=models.FileField(blank=True, null=True, upload_to=''),
),
]

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):
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",
@ -246,15 +247,21 @@ class OriginalImage(OriginalMedia):
create_image_resolutions.delay(self.id)
@property
def thumbnail(self):
return self.resolutions.order_by("width").first()
class OriginalVideo(OriginalMedia):
thumbnail = models.FileField(null=True, blank=True)
def media_type(self) -> str:
return "video"
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# TODO: Get and save video resolution
# TODO: Get and save video resolution and thumbnail
create_video_resolutions(self.id)
@ -289,7 +296,7 @@ class Scene(models.Model):
@property
def thumbnail(self):
return self.base_content.resolutions.order_by("width").first()
return self.base_content.thumbnail
def user_has_permission(self, user):
return user.is_authenticated and (

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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">

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="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' %}">

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
@ -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'),

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.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):