added folder creation, folder meta, folder upload, tables

This commit is contained in:
Alexander Karpov 2023-04-24 14:48:15 +03:00
parent 83ea171886
commit 6d65e771d7
18 changed files with 349 additions and 85 deletions

14
akarpov/files/filters.py Normal file
View File

@ -0,0 +1,14 @@
import django_filters
from .models import File
class FileFilter(django_filters.FilterSet):
modified = django_filters.DateFromToRangeFilter(
label="Dates",
widget=django_filters.widgets.RangeWidget(attrs={"type": "date"}),
)
class Meta:
model = File
fields = ["modified", "private", "parent"]

View File

@ -1,9 +1,15 @@
from django import forms from django import forms
from akarpov.files.models import File from akarpov.files.models import File, Folder
class FileForm(forms.ModelForm): class FileForm(forms.ModelForm):
class Meta: class Meta:
model = File model = File
fields = ["name", "private", "description"] fields = ["name", "private", "description"]
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ["name", "private"]

View File

@ -39,13 +39,13 @@ class Meta:
def is_file(self): def is_file(self):
return type(self) is File return type(self) is File
def get_folder_chain(self): def get_top_folders(self):
folders = [self] folders = []
obj = self obj = self
while obj.parent: while obj.parent:
folders.append(obj.parent) folders.append(obj.parent)
obj = obj.parent obj = obj.parent
return folders return folders[::-1]
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
update_fields = kwargs.get("update_fields", None) update_fields = kwargs.get("update_fields", None)

View File

@ -3,6 +3,7 @@
from django.core.files.base import File from django.core.files.base import File
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now
from akarpov.files.models import File as FileModel from akarpov.files.models import File as FileModel
from akarpov.files.models import FileInTrash from akarpov.files.models import FileInTrash
@ -10,8 +11,13 @@
@receiver(post_save, sender=FileModel) @receiver(post_save, sender=FileModel)
def post_on_create(sender, instance: FileModel, created, **kwargs): def file_on_create(sender, instance: FileModel, created, **kwargs):
if created: if created:
for folder in instance.get_top_folders():
folder.modified = now()
folder.size += instance.file.size
folder.amount += 1
folder.save()
process_file.apply_async( process_file.apply_async(
kwargs={ kwargs={
"pk": instance.pk, "pk": instance.pk,
@ -23,6 +29,12 @@ def post_on_create(sender, instance: FileModel, created, **kwargs):
@receiver(post_delete, sender=FileModel) @receiver(post_delete, sender=FileModel)
def move_file_to_trash(sender, instance, **kwargs): def move_file_to_trash(sender, instance, **kwargs):
if instance.file: if instance.file:
for folder in instance.get_top_folders():
folder.modified = now()
folder.size -= instance.file.size
folder.amount -= 1
folder.save()
name = instance.file.name.split("/")[-1] name = instance.file.name.split("/")[-1]
trash = FileInTrash(user=instance.user, name=name) trash = FileInTrash(user=instance.user, name=name)
trash.file = File(instance.file, name=name) trash.file = File(instance.file, name=name)

30
akarpov/files/tables.py Normal file
View File

@ -0,0 +1,30 @@
import django_tables2 as tables
from akarpov.files.models import File
class FileTable(tables.Table):
name = tables.columns.Column("name", linkify=True)
time = tables.columns.DateTimeColumn(
format="H:m:s", accessor="modified", verbose_name="Time"
)
folder = tables.columns.Column(
linkify=True, accessor="parent", verbose_name="Folder"
)
file = tables.columns.FileColumn(
linkify=True, accessor="file_obj", verbose_name="File"
)
class Meta:
model = File
template_name = "django_tables2/bootstrap5.html"
fields = (
"name",
"created",
"modified",
"time",
"folder",
"private",
"file",
"file_type",
)

View File

@ -5,14 +5,17 @@
MyChunkedUploadView, MyChunkedUploadView,
TopFolderView, TopFolderView,
delete_file_view, delete_file_view,
file_table,
file_update, file_update,
files_view, files_view,
folder_create,
folder_view, folder_view,
) )
app_name = "files" app_name = "files"
urlpatterns = [ urlpatterns = [
path("", TopFolderView.as_view(), name="main"), path("", TopFolderView.as_view(), name="main"),
path("table/", file_table, name="table"),
path( path(
"api/chunked_upload_complete/", "api/chunked_upload_complete/",
MyChunkedUploadCompleteView.as_view(), MyChunkedUploadCompleteView.as_view(),
@ -26,6 +29,8 @@
path( path(
"api/chunked_upload/", MyChunkedUploadView.as_view(), name="api_chunked_upload" "api/chunked_upload/", MyChunkedUploadView.as_view(), name="api_chunked_upload"
), ),
path("api/folder/create/", folder_create, name="folder_create"),
path("api/folder/create/<str:slug>", folder_create, name="sub_folder_create"),
path("<str:slug>", files_view, name="view"), path("<str:slug>", files_view, name="view"),
path("<str:slug>/update", file_update, name="update"), path("<str:slug>/update", file_update, name="update"),
path("<str:slug>/delete", delete_file_view, name="delete"), path("<str:slug>/delete", delete_file_view, name="delete"),

View File

@ -6,9 +6,16 @@
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.views.generic import (
from django.views.generic import DetailView, ListView, RedirectView, UpdateView CreateView,
from django.views.generic.base import TemplateView DetailView,
ListView,
RedirectView,
UpdateView,
)
from django_filters.views import FilterView
from django_tables2 import SingleTableView
from django_tables2.export import ExportMixin
from akarpov.contrib.chunked_upload.exceptions import ChunkedUploadError from akarpov.contrib.chunked_upload.exceptions import ChunkedUploadError
from akarpov.contrib.chunked_upload.models import ChunkedUpload from akarpov.contrib.chunked_upload.models import ChunkedUpload
@ -16,22 +23,29 @@
ChunkedUploadCompleteView, ChunkedUploadCompleteView,
ChunkedUploadView, ChunkedUploadView,
) )
from akarpov.files.forms import FileForm from akarpov.files.filters import FileFilter
from akarpov.files.forms import FileForm, FolderForm
from akarpov.files.models import BaseFileItem, File, Folder from akarpov.files.models import BaseFileItem, File, Folder
from akarpov.files.previews import extensions, meta, meta_extensions, previews from akarpov.files.previews import extensions, meta, meta_extensions, previews
from akarpov.files.services.preview import get_base_meta from akarpov.files.services.preview import get_base_meta
from akarpov.files.tables import FileTable
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
class TopFolderView(LoginRequiredMixin, ListView): class TopFolderView(LoginRequiredMixin, ListView):
template_name = "files/list.html" template_name = "files/list.html"
paginate_by = 19 paginate_by = 18
model = BaseFileItem model = BaseFileItem
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["folder_slug"] = None context["folder_slug"] = None
context["folder_form"] = FolderForm()
context["is_folder_owner"] = True
# folder path
context["folders"] = []
return context return context
def get_queryset(self): def get_queryset(self):
@ -41,17 +55,28 @@ def get_queryset(self):
class FileFolderView(ListView): class FileFolderView(ListView):
template_name = "files/folder.html" template_name = "files/folder.html"
model = BaseFileItem model = BaseFileItem
paginate_by = 39 paginate_by = 38
slug_field = "slug" slug_field = "slug"
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.object = None self.object = None
def get_paginate_by(self, queryset):
if self.request.user == self.get_object().user:
# return 38 items for owner to fit file and folder forms
return 38
return 40
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
folder = self.get_object() folder = self.get_object()
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["folder_slug"] = folder.slug context["folder_slug"] = folder.slug
context["folder_form"] = FolderForm()
context["is_folder_owner"] = self.request.user == self.get_object().user
# folder path
context["folders"] = folder.get_top_folders() + [folder]
return context return context
def get_object(self, *args): def get_object(self, *args):
@ -70,7 +95,7 @@ class FileUpdateView(LoginRequiredMixin, UpdateView):
model = File model = File
form_class = FileForm form_class = FileForm
def get_object(self): def get_object(self, *args):
file = get_object_or_404(File, slug=self.kwargs["slug"]) file = get_object_or_404(File, slug=self.kwargs["slug"])
if file.user != self.request.user: if file.user != self.request.user:
raise PermissionDenied raise PermissionDenied
@ -82,6 +107,27 @@ def get_object(self):
file_update = FileUpdateView.as_view() file_update = FileUpdateView.as_view()
class FolderCreateView(LoginRequiredMixin, CreateView):
model = Folder
form_class = FolderForm
def form_valid(self, form):
folder = None
if "slug" in self.kwargs and self.kwargs["slug"]:
folder = get_object_or_404(Folder, slug=self.kwargs["slug"])
form.instance.user = self.request.user
form.instance.parent = folder
super().form_valid(form)
if folder:
return HttpResponseRedirect(
reverse("files:folder", kwargs={"slug": folder.slug})
)
return HttpResponseRedirect(reverse("files:main"))
folder_create = FolderCreateView.as_view()
class FileView(DetailView): class FileView(DetailView):
template_name = "files/view.html" template_name = "files/view.html"
model = File model = File
@ -161,10 +207,6 @@ def get_redirect_url(self, *args, **kwargs):
folder_view = FileFolderView.as_view() folder_view = FileFolderView.as_view()
class ChunkedUploadDemo(LoginRequiredMixin, TemplateView):
template_name = "files/upload.html"
class MyChunkedUploadView(ChunkedUploadView): class MyChunkedUploadView(ChunkedUploadView):
model = ChunkedUpload model = ChunkedUpload
field_name = "the_file" field_name = "the_file"
@ -214,11 +256,6 @@ def on_completion(self, uploaded_file, request):
name=uploaded_file.name, name=uploaded_file.name,
parent=folder, parent=folder,
) )
if folder:
folder.modified = now()
folder.size += uploaded_file.size
folder.amount += 1
folder.save()
request.user.left_file_upload -= uploaded_file.size request.user.left_file_upload -= uploaded_file.size
request.user.save() request.user.save()
self.message = { self.message = {
@ -235,3 +272,17 @@ def on_completion(self, uploaded_file, request):
def get_response_data(self, chunked_upload, request): def get_response_data(self, chunked_upload, request):
return self.message return self.message
class FileTableView(LoginRequiredMixin, FilterView, ExportMixin, SingleTableView):
model = File
table_class = FileTable
filterset_class = FileFilter
template_name = "files/tables.html"
paginate_by = 200
def get_queryset(self, **kwargs):
return File.objects.filter(user=self.request.user)
file_table = FileTableView.as_view()

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1 +1,8 @@
{% extends 'files/list.html' %} {% extends 'files/list.html' %}
{% block meta %}
<meta property="og:type" content="website">
<meta property="og:title" content="{{ folder.name }}">
<meta property="og:url" content="{{ folder.get_absolute_url }}">
<meta property="og:description" content="folder on akarpov.ru">
{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static crispy_forms_tags %}
{% load crispy_forms_tags %}
{% block title %}editing file on akarpov{% endblock %} {% block title %}editing file on akarpov{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load humanize static %} {% load humanize static crispy_forms_tags %}
{% block javascript %} {% block javascript %}
<script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/jquery.min.js' %}"></script>
@ -33,67 +33,101 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% for folder in folders %} <div class="ms-3 row">
{{ folder.name }} {% if is_folder_owner %}
{% endfor %} {% if folder_slug %}
<div class="row justify-content-center"> <nav aria-label="breadcrumb">
{% if request.user.is_authenticated %} <ol class="breadcrumb">
<div class="col-lg-2 col-xxl-2 col-md-4 col-sm-6 col-xs-12 mb-3 m-3 d-flex align-items-stretch card"> <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'files:main' %}">home</a></li>
<div class="card-body d-flex flex-column justify-content-center align-items-center"> {% for f in folders %}
{% csrf_token %} <li class="breadcrumb-item"><a href="{% url 'files:folder' slug=f.slug %}">{{ f.name }}</a></li>
<fieldset class="upload_dropZone text-center mb-3 p-4"> {% endfor %}
<legend class="visually-hidden">Image uploader</legend> </ol>
<svg class="upload_svg" width="60" height="60" aria-hidden="true"> </nav>
<use href="#icon-imageUpload"></use> {% else %}
</svg> <nav aria-label="breadcrumb">
<p class="small my-2">Drag &amp; Drop file(s) <br><i>or</i></p> <ol class="breadcrumb">
<input class="position-absolute invisible" id="chunked_upload" type="file" name="the_file"> <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'files:main' %}">home</a></li>
<label class="btn btn-upload mb-3" for="chunked_upload">Choose file(s)</label> </ol>
<div class="upload_gallery d-flex flex-wrap justify-content-center gap-3 mb-0"></div> </nav>
</fieldset> {% endif %}
<svg style="display:none"> {% endif %}
<defs> <div class="d-flex justify-content-end me-5">
<symbol id="icon-imageUpload" clip-rule="evenodd" viewBox="0 0 96 96"> <a class="me-5" href="{% url 'files:table' %}">View in table view</a>
<path d="M47 6a21 21 0 0 0-12.3 3.8c-2.7 2.1-4.4 5-4.7 7.1-5.8 1.2-10.3 5.6-10.3 10.6 0 6 5.8 11 13 11h12.6V22.7l-7.1 6.8c-.4.3-.9.5-1.4.5-1 0-2-.8-2-1.7 0-.4.3-.9.6-1.2l10.3-8.8c.3-.4.8-.6 1.3-.6.6 0 1 .2 1.4.6l10.2 8.8c.4.3.6.8.6 1.2 0 1-.9 1.7-2 1.7-.5 0-1-.2-1.3-.5l-7.2-6.8v15.6h14.4c6.1 0 11.2-4.1 11.2-9.4 0-5-4-8.8-9.5-9.4C63.8 11.8 56 5.8 47 6Zm-1.7 42.7V38.4h3.4v10.3c0 .8-.7 1.5-1.7 1.5s-1.7-.7-1.7-1.5Z M27 49c-4 0-7 2-7 6v29c0 3 3 6 6 6h42c3 0 6-3 6-6V55c0-4-3-6-7-6H28Zm41 3c1 0 3 1 3 3v19l-13-6a2 2 0 0 0-2 0L44 79l-10-5a2 2 0 0 0-2 0l-9 7V55c0-2 2-3 4-3h41Z M40 62c0 2-2 4-5 4s-5-2-5-4 2-4 5-4 5 2 5 4Z"></path>
</symbol>
</defs>
</svg>
<p class="text-break" id="progress-message"></p>
<div id="progress" class="progress w-100" style="display: none" role="progressbar" aria-label="Warning example" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div id="progress-bar" class="progress-bar text-bg-warning" style="width: 0%">0%</div>
</div>
<div id="messages"></div>
</div>
</div>
{% endif %}
{% for file in basefileitem_list %}
<div class="col-lg-2 col-xxl-2 col-md-4 col-sm-6 col-xs-12 mb-3 m-3 d-flex align-items-stretch card">
{% if file.is_file %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ file.name }}</h5>
<p class="card-text mb-4"><small class="text-muted">{{ file.file_size | filesizeformat }}</small></p>
<div class="align-self-stretch align-items-center justify-content-center d-flex flex-column fill-height controlsdiv">
<img src="{{ file.file_image_url }}" class="img-fluid" alt="">
</div>
<a href="{% url 'files:view' file.slug %}" class="stretched-link"></a>
<p class="card-text mb-4 mt-2 ms-3"><small class="text-muted">{{ file.modified | naturaltime }}</small></p>
</div>
{% else %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ file.name }}</h5>
<p class="card-text mb-4"><small class="text-muted">{{ file.size | filesizeformat }}, {{ file.amount }} {% if file.amount == 1 %} item {% else %} items {% endif %}</small></p>
<div class="align-self-stretch align-items-center justify-content-center d-flex flex-column fill-height controlsdiv">
<i class="bi bi-folder"></i>
</div>
<a href="{% url 'files:folder' file.slug %}" class="stretched-link"></a>
<p class="card-text mb-4 mt-2 ms-3"><small class="text-muted">{{ file.modified | naturaltime }}</small></p>
</div>
{% endif %}
</div> </div>
{% endfor %} {% if request.user.is_authenticated and is_folder_owner %}
<div class="col-lg-2 col-xxl-2 col-md-4 col-sm-6 col-xs-12 mb-3 m-3 d-flex align-items-stretch card">
<div class="card-body d-flex flex-column justify-content-center align-items-center">
{% csrf_token %}
<fieldset class="upload_dropZone text-center mb-3 p-4">
<legend class="visually-hidden">Image uploader</legend>
<svg class="upload_svg" width="60" height="60" aria-hidden="true">
<use href="#icon-imageUpload"></use>
</svg>
<p class="small my-2">Drag &amp; Drop file(s) <br><i>or</i></p>
<input class="position-absolute invisible" id="chunked_upload" type="file" name="the_file">
<label class="btn btn-upload mb-3" for="chunked_upload">Choose file(s)</label>
<div class="upload_gallery d-flex flex-wrap justify-content-center gap-3 mb-0"></div>
</fieldset>
<svg style="display:none">
<defs>
<symbol id="icon-imageUpload" clip-rule="evenodd" viewBox="0 0 96 96">
<path d="M47 6a21 21 0 0 0-12.3 3.8c-2.7 2.1-4.4 5-4.7 7.1-5.8 1.2-10.3 5.6-10.3 10.6 0 6 5.8 11 13 11h12.6V22.7l-7.1 6.8c-.4.3-.9.5-1.4.5-1 0-2-.8-2-1.7 0-.4.3-.9.6-1.2l10.3-8.8c.3-.4.8-.6 1.3-.6.6 0 1 .2 1.4.6l10.2 8.8c.4.3.6.8.6 1.2 0 1-.9 1.7-2 1.7-.5 0-1-.2-1.3-.5l-7.2-6.8v15.6h14.4c6.1 0 11.2-4.1 11.2-9.4 0-5-4-8.8-9.5-9.4C63.8 11.8 56 5.8 47 6Zm-1.7 42.7V38.4h3.4v10.3c0 .8-.7 1.5-1.7 1.5s-1.7-.7-1.7-1.5Z M27 49c-4 0-7 2-7 6v29c0 3 3 6 6 6h42c3 0 6-3 6-6V55c0-4-3-6-7-6H28Zm41 3c1 0 3 1 3 3v19l-13-6a2 2 0 0 0-2 0L44 79l-10-5a2 2 0 0 0-2 0l-9 7V55c0-2 2-3 4-3h41Z M40 62c0 2-2 4-5 4s-5-2-5-4 2-4 5-4 5 2 5 4Z"></path>
</symbol>
</defs>
</svg>
<p class="text-break" id="progress-message"></p>
<div id="progress" class="progress w-100" style="display: none" role="progressbar" aria-label="Warning example" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div id="progress-bar" class="progress-bar text-bg-warning" style="width: 0%">0%</div>
</div>
<div id="messages"></div>
</div>
</div>
<div class="col-lg-2 col-xxl-2 col-md-4 col-sm-6 col-xs-12 mb-3 m-3 d-flex align-items-stretch card">
<div class="card-body d-flex flex-column justify-content-center align-items-center">
<h5>Add folder</h5>
<form class="pt-2" method="POST" id="designer-form" action="{% if folder_slug %}{% url 'files:sub_folder_create' slug=folder_slug %}{% else %}{% url 'files:folder_create' %}{% endif %}">
{% csrf_token %}
{% for field in folder_form %}
{{ field|as_crispy_field }}
{% endfor %}
<div class="mt-4 flex justify-end space-x-4">
<button class="btn btn-success" type="submit" id="submit">
Create
</button>
</div>
</form>
</div>
</div>
{% endif %}
{% for file in basefileitem_list %}
<div class="col-lg-2 col-xxl-2 col-md-4 col-sm-6 col-xs-12 mb-3 m-3 d-flex align-items-stretch card justify-content-center">
{% if file.is_file %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ file.name }}</h5>
<p class="card-text mb-4"><small class="text-muted">{{ file.file_size | filesizeformat }}</small></p>
<div class="align-self-stretch align-items-center justify-content-center d-flex flex-column fill-height controlsdiv">
<img src="{{ file.file_image_url }}" class="img-fluid" alt="">
</div>
<a href="{% url 'files:view' file.slug %}" class="stretched-link"></a>
<p class="card-text mb-4 mt-2 ms-3"><small class="text-muted">{{ file.modified | naturaltime }}</small></p>
</div>
{% else %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ file.name }}</h5>
<p class="card-text mb-4"><small class="text-muted">{{ file.size | filesizeformat }}, {{ file.amount }} {% if file.amount == 1 %} item {% else %} items {% endif %}</small></p>
<div class="align-self-stretch align-items-center justify-content-center d-flex flex-column fill-height controlsdiv">
<img src="{% static 'images/files/folder.jpg' %}" class="img-fluid" alt="">
</div>
<a href="{% url 'files:folder' file.slug %}" class="stretched-link"></a>
<p class="card-text mb-4 mt-2 ms-3"><small class="text-muted">{{ file.modified | naturaltime }}</small></p>
</div>
{% endif %}
</div>
{% endfor %}
</div> </div>
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<div class="btn-group" role="group" aria-label="Item pagination"> <div class="btn-group" role="group" aria-label="Item pagination">

View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% load django_tables2 crispy_forms_tags %}
{% block css %}
<style>
.pagination {
gap: 20px;
}
</style>
{% endblock %}
{% block content %}
<div class="justify-content-end d-flex">
<a href="{% url 'files:main' %}">View in folder view</a>
</div>
<form class="mb-4 p-2" >
<div class="row d-flex">
{% for field in filter.form %}
<div class="col col-md-4 col-sm-5 justify-content-center align-self-center">{{ field | as_crispy_field }}</div>
{% endfor %}
</div>
<div class="row">
<div class="col col-auto justify-content-center align-self-center">
<button class="btn btn-success mt-3">Apply filters</button>
</div>
<div class="col col-auto justify-content-center align-self-center">
<a style="text-decoration: none" href="{% export_url "xlsx" %}" class="btn btn-warning mt-3">Export into Excel</a>
</div>
</div>
</form>
<div class="table-responsive">
{% render_table table %}
</div>
{% endblock %}

View File

@ -14,6 +14,17 @@
{% block content %} {% block content %}
{% if has_perm %}
<nav class="ms-3" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item active" aria-current="page"><a href="{% url 'files:main' %}">home</a></li>
{% for f in file.get_top_folders %}
<li class="breadcrumb-item"><a href="{% url 'files:folder' slug=f.slug %}">{{ f.name }}</a></li>
{% endfor %}
<li class="breadcrumb-item active" aria-current="page">{{ file.name }}</li>
</ol>
</nav>
{% endif %}
<div class="row m-2"> <div class="row m-2">
<h1 class="fs-1 text-break mb-4">{{ file.name }} <h1 class="fs-1 text-break mb-4">{{ file.name }}
{% if has_perm %} {% if has_perm %}

View File

@ -115,6 +115,8 @@
"akarpov.contrib.chunked_upload", "akarpov.contrib.chunked_upload",
"active_link", "active_link",
"robots", "robots",
"django_filters",
"django_tables2",
# django-cms # django-cms
"cms", "cms",
"menus", "menus",

57
poetry.lock generated
View File

@ -1519,6 +1519,21 @@ files = [
[package.dependencies] [package.dependencies]
jsonfield = ">=3.0.0" jsonfield = ">=3.0.0"
[[package]]
name = "django-filter"
version = "23.1"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "django-filter-23.1.tar.gz", hash = "sha256:dee5dcf2cea4d7f767e271b6d01f767fce7500676d5e5dc58dac8154000b87df"},
{file = "django_filter-23.1-py3-none-any.whl", hash = "sha256:e3c52ad83c32fb5882125105efb5fea2a1d6a85e7dc64b04ef52edbf14451b6c"},
]
[package.dependencies]
Django = ">=3.2"
[[package]] [[package]]
name = "django-formtools" name = "django-formtools"
version = "2.4" version = "2.4"
@ -1757,6 +1772,24 @@ files = [
django = "*" django = "*"
typing-extensions = "*" typing-extensions = "*"
[[package]]
name = "django-tables2"
version = "2.5.3"
description = "Table/data-grid framework for Django"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "django-tables2-2.5.3.tar.gz", hash = "sha256:f6c1623aac188d29aae9cf6b4de3211c96c525e49890654bec3359c181600eb9"},
{file = "django_tables2-2.5.3-py2.py3-none-any.whl", hash = "sha256:e336fdf8899a8fab110550a40cad956064bd4054818e0b972c1893b3e2542168"},
]
[package.dependencies]
Django = ">=3.2"
[package.extras]
tablib = ["tablib"]
[[package]] [[package]]
name = "django-timezone-field" name = "django-timezone-field"
version = "5.0" version = "5.0"
@ -4780,6 +4813,28 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]
typing = ["mypy", "rich", "twisted"] typing = ["mypy", "rich", "twisted"]
[[package]]
name = "tablib"
version = "3.4.0"
description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "tablib-3.4.0-py3-none-any.whl", hash = "sha256:0f9c45141195c472202f30d82c0c035bf7e0dd7e4da2257815e506acff4ab364"},
{file = "tablib-3.4.0.tar.gz", hash = "sha256:77ea97faf6f92a7e198c05bd0c690f3cba57b83ea45a636b72f967cb6fe6f160"},
]
[package.extras]
all = ["markuppy", "odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"]
cli = ["tabulate"]
html = ["markuppy"]
ods = ["odfpy"]
pandas = ["pandas"]
xls = ["xlrd", "xlwt"]
xlsx = ["openpyxl (>=2.6.0)"]
yaml = ["pyyaml"]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "2.2.0" version = "2.2.0"
@ -5642,4 +5697,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "667d1226a682d79bea11fdee05d079b1b494ac325167ca069f79f4d18d1a7d04" content-hash = "c065bd831f46b58518895fa101821a580bebe3f7792348ca31d6a4fb4aab2b82"

View File

@ -88,6 +88,9 @@ cairosvg = "^2.7.0"
textract = "^1.6.5" textract = "^1.6.5"
spotipy = "2.16.1" spotipy = "2.16.1"
django-robots = "^5.0" django-robots = "^5.0"
django-tables2 = "^2.5.3"
django-filter = "^23.1"
tablib = "^3.4.0"
[build-system] [build-system]