Compare commits
2 commits
587bab9c64
...
42e9de6942
Author | SHA1 | Date | |
---|---|---|---|
42e9de6942 | |||
0471f151b6 |
14 changed files with 149 additions and 14 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/0017_originalvideo_thumbnail.py
Normal file
18
quackscape/tours/migrations/0017_originalvideo_thumbnail.py
Normal 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=''),
|
||||
),
|
||||
]
|
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",
|
||||
|
@ -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 (
|
||||
|
|
|
@ -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