From 6d65e771d7faab58906d8dc246f0faffe747d568 Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Mon, 24 Apr 2023 14:48:15 +0300 Subject: [PATCH] added folder creation, folder meta, folder upload, tables --- akarpov/files/filters.py | 14 ++ akarpov/files/forms.py | 8 +- akarpov/files/models.py | 6 +- akarpov/files/signals.py | 14 +- akarpov/files/tables.py | 30 ++++ akarpov/files/urls.py | 5 + akarpov/files/views.py | 83 ++++++++-- akarpov/static/images/files/folder.jpg | Bin 0 -> 3873 bytes akarpov/templates/files/folder.html | 7 + akarpov/templates/files/form.html | 3 +- akarpov/templates/files/list.html | 156 +++++++++++------- akarpov/templates/files/tables.html | 35 ++++ akarpov/templates/files/view.html | 11 ++ .../test_platform/templates/files/folder.html | 0 .../test_platform/templates/files/view.html | 0 config/settings/base.py | 2 + poetry.lock | 57 ++++++- pyproject.toml | 3 + 18 files changed, 349 insertions(+), 85 deletions(-) create mode 100644 akarpov/files/filters.py create mode 100644 akarpov/files/tables.py create mode 100644 akarpov/static/images/files/folder.jpg create mode 100644 akarpov/templates/files/tables.html delete mode 100644 akarpov/test_platform/templates/files/folder.html delete mode 100644 akarpov/test_platform/templates/files/view.html diff --git a/akarpov/files/filters.py b/akarpov/files/filters.py new file mode 100644 index 0000000..09fa65c --- /dev/null +++ b/akarpov/files/filters.py @@ -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"] diff --git a/akarpov/files/forms.py b/akarpov/files/forms.py index 8f42422..89d4fb4 100644 --- a/akarpov/files/forms.py +++ b/akarpov/files/forms.py @@ -1,9 +1,15 @@ from django import forms -from akarpov.files.models import File +from akarpov.files.models import File, Folder class FileForm(forms.ModelForm): class Meta: model = File fields = ["name", "private", "description"] + + +class FolderForm(forms.ModelForm): + class Meta: + model = Folder + fields = ["name", "private"] diff --git a/akarpov/files/models.py b/akarpov/files/models.py index d03e7e3..1bcc61e 100644 --- a/akarpov/files/models.py +++ b/akarpov/files/models.py @@ -39,13 +39,13 @@ class Meta: def is_file(self): return type(self) is File - def get_folder_chain(self): - folders = [self] + def get_top_folders(self): + folders = [] obj = self while obj.parent: folders.append(obj.parent) obj = obj.parent - return folders + return folders[::-1] def save(self, *args, **kwargs): update_fields = kwargs.get("update_fields", None) diff --git a/akarpov/files/signals.py b/akarpov/files/signals.py index a15958e..b70cc5c 100644 --- a/akarpov/files/signals.py +++ b/akarpov/files/signals.py @@ -3,6 +3,7 @@ from django.core.files.base import File from django.db.models.signals import post_delete, post_save from django.dispatch import receiver +from django.utils.timezone import now from akarpov.files.models import File as FileModel from akarpov.files.models import FileInTrash @@ -10,8 +11,13 @@ @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: + for folder in instance.get_top_folders(): + folder.modified = now() + folder.size += instance.file.size + folder.amount += 1 + folder.save() process_file.apply_async( kwargs={ "pk": instance.pk, @@ -23,6 +29,12 @@ def post_on_create(sender, instance: FileModel, created, **kwargs): @receiver(post_delete, sender=FileModel) def move_file_to_trash(sender, instance, **kwargs): 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] trash = FileInTrash(user=instance.user, name=name) trash.file = File(instance.file, name=name) diff --git a/akarpov/files/tables.py b/akarpov/files/tables.py new file mode 100644 index 0000000..c4d9006 --- /dev/null +++ b/akarpov/files/tables.py @@ -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", + ) diff --git a/akarpov/files/urls.py b/akarpov/files/urls.py index 12de9dd..21e357d 100644 --- a/akarpov/files/urls.py +++ b/akarpov/files/urls.py @@ -5,14 +5,17 @@ MyChunkedUploadView, TopFolderView, delete_file_view, + file_table, file_update, files_view, + folder_create, folder_view, ) app_name = "files" urlpatterns = [ path("", TopFolderView.as_view(), name="main"), + path("table/", file_table, name="table"), path( "api/chunked_upload_complete/", MyChunkedUploadCompleteView.as_view(), @@ -26,6 +29,8 @@ path( "api/chunked_upload/", MyChunkedUploadView.as_view(), name="api_chunked_upload" ), + path("api/folder/create/", folder_create, name="folder_create"), + path("api/folder/create/", folder_create, name="sub_folder_create"), path("", files_view, name="view"), path("/update", file_update, name="update"), path("/delete", delete_file_view, name="delete"), diff --git a/akarpov/files/views.py b/akarpov/files/views.py index c24d04c..995264d 100644 --- a/akarpov/files/views.py +++ b/akarpov/files/views.py @@ -6,9 +6,16 @@ from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse -from django.utils.timezone import now -from django.views.generic import DetailView, ListView, RedirectView, UpdateView -from django.views.generic.base import TemplateView +from django.views.generic import ( + CreateView, + 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.models import ChunkedUpload @@ -16,22 +23,29 @@ ChunkedUploadCompleteView, 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.previews import extensions, meta, meta_extensions, previews from akarpov.files.services.preview import get_base_meta +from akarpov.files.tables import FileTable logger = structlog.get_logger(__name__) class TopFolderView(LoginRequiredMixin, ListView): template_name = "files/list.html" - paginate_by = 19 + paginate_by = 18 model = BaseFileItem def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["folder_slug"] = None + context["folder_form"] = FolderForm() + context["is_folder_owner"] = True + + # folder path + context["folders"] = [] return context def get_queryset(self): @@ -41,17 +55,28 @@ def get_queryset(self): class FileFolderView(ListView): template_name = "files/folder.html" model = BaseFileItem - paginate_by = 39 + paginate_by = 38 slug_field = "slug" def __init__(self, **kwargs): super().__init__(**kwargs) 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): folder = self.get_object() context = super().get_context_data(**kwargs) 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 def get_object(self, *args): @@ -70,7 +95,7 @@ class FileUpdateView(LoginRequiredMixin, UpdateView): model = File form_class = FileForm - def get_object(self): + def get_object(self, *args): file = get_object_or_404(File, slug=self.kwargs["slug"]) if file.user != self.request.user: raise PermissionDenied @@ -82,6 +107,27 @@ def get_object(self): 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): template_name = "files/view.html" model = File @@ -161,10 +207,6 @@ def get_redirect_url(self, *args, **kwargs): folder_view = FileFolderView.as_view() -class ChunkedUploadDemo(LoginRequiredMixin, TemplateView): - template_name = "files/upload.html" - - class MyChunkedUploadView(ChunkedUploadView): model = ChunkedUpload field_name = "the_file" @@ -214,11 +256,6 @@ def on_completion(self, uploaded_file, request): name=uploaded_file.name, 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.save() self.message = { @@ -235,3 +272,17 @@ def on_completion(self, uploaded_file, request): def get_response_data(self, chunked_upload, request): 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() diff --git a/akarpov/static/images/files/folder.jpg b/akarpov/static/images/files/folder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43e81facdc95b267945fb36d4f577b4f4ecfa990 GIT binary patch literal 3873 zcmeHKc~p~E7Jo^AAWA_Y0?Jk?RzpQnM4&|z5f@Ow3JD;TU?KrQHp5N`QCew12#QNs z6dWmt1VO^4RESGJ-~(e(uwV>j8%C5}LLke0^qh|WbQtHH`Dfku~1~>rl$s=c{T^@10Z$-&(iP3WYlq7?#>b0@>e4ahDCi*+gzE__i z9~NJrwf|DQx3r_2mUC22bXDA0*u8ixFJ~?#=QhsgZvJ=ro7d&O)H-zX=*6Ii51k%7 z5i=6*+sEqa%roZmXQcV`@g#@Zp2kq|cxa+%A;09W*AiU=BO{h91v#|c|~aPd*cS>TUk5P%or&{K5XSk$z4J#jj5;d8=c6n@y52ptGF!xH3u@ z`|cK6wsP)SOiD`1W&RU3Ua=DX!k(Zd?o`)vIY!p6NvX3FoH0tH&P-OcDO|B?b1zT6!w~}f(u@|8Z7e8YKi%M-Y zqF0w^75g=z0!VX2a8VKgt(s<}#-a2<_ z2uo(u-ZPCq?v3t*v#s9_p5Vnk>*)1cUW_bocvQbC4HbZ7-BD+usLbp%@sxW-9>tX% z{{X8EX4mw9ZElUAJJHwvJL)gq=-9rJcpjwS@0B+f# zDr2ERT=vN#%=A{|^q~}H2*wV@vIZBYR-l!n_;0aHgGo{u$VU`H2CPI%X6YLf4GO7f zAV(sddmx1znZJl;k}5lUN(LgINOw6~Q(l|U9oOA2m>hl@>UxR|8xnxk84%|#E6^;> z<>73L2Igv#tA2g=+K|qKz84X0_G(c(@6c30=%58_p`)yy9%nwn*3SPPs9e$fQP+A|pqhZbo8}_$Ezgvp2ELj&{;*EpVI{Pj%}7M2$wZF@gO6p)mGhU2-&~ukvl$uU_;4>j8=A1~1hY~xu-FtD5ADGyC$LKF zpjDZw3fR_n3Fk?iXG(iB$>wz{)z-Cng6L#0+=PsH;2k}ecGm`j*WtFM;3hKSdlO1? zo5Xe~@FH&W$T^Q!+iqgJC}9V0(AQI7bi#O>(1UNyA@I@-k7N5c*2@?$Yy+ zU6Y<*#|nP#OF$q%M|3pwmozvjbM|;gJMq2v;8Sw7!)AJjZaJb}y|Q)VWA3^5D@0-7 zG>Ug^OfeQnHO}x}Kl4{kB6U3dSC2XTs}l>#|a*uXX5GiTtob>C&4v>R{CT>Ekc<3p|k$AN1Q_1SH|A z+lZU6S51N&lUrI(=HC^WzHE2#M@x^gE@k481fb6Hs||;^9*jL%$vW~(581{A&DPlK zvlZdZzN7PIK!pWBZh#{+@8K}|xiT|30YXzqAT-F#FcQhu!O-qau(F_v zj5>J75tc7p$oo<&I^4Fx_^&RHqi50m>z*>u4v;eUWb(iHuQ}FRzsec_| z5p;i-1%R;#2H`)b^`l%Ao?yhsGo|j)iJE!HT|A5OhBfhI)d9O7LX!g$gAbqX7_yJ3l3ZgKx50okOe^Sx7&J58l!OdjGPl!`I}mk1DS@_jYEsFFKNQB zRDnRLf9+O((ni6nTwh&vk8oe8jzaUC^sDnK0fmzkNPh-7^^I1 z6rYjw>hd1DjP)28My)OG)+*%5KbK{@N4ghIVH%SZ5fd%~&s7qE%x1*E2YohPhNhBTLRSXmG*Cwb4s|?b$sijhl-*zRe1xd&YSm7Pa%DcJqhZ0; zPVv0m6TFK^vPITbyHD+S^$l)cv*B7EIaYR2TR&6QC(pns4+@C#GAb!X>8u>aDo}>WaPwO= s`=b62A21y(|L69f##h1(nI{VYW*#i+X6m|D!S@5_J+4l-9sSSz9k5-{761SM literal 0 HcmV?d00001 diff --git a/akarpov/templates/files/folder.html b/akarpov/templates/files/folder.html index 1b91213..9308c23 100644 --- a/akarpov/templates/files/folder.html +++ b/akarpov/templates/files/folder.html @@ -1 +1,8 @@ {% extends 'files/list.html' %} + +{% block meta %} + + + + +{% endblock %} diff --git a/akarpov/templates/files/form.html b/akarpov/templates/files/form.html index 95e2326..acac21f 100644 --- a/akarpov/templates/files/form.html +++ b/akarpov/templates/files/form.html @@ -1,6 +1,5 @@ {% extends "base.html" %} -{% load static %} -{% load crispy_forms_tags %} +{% load static crispy_forms_tags %} {% block title %}editing file on akarpov{% endblock %} diff --git a/akarpov/templates/files/list.html b/akarpov/templates/files/list.html index 5c52aa1..4aa8b50 100644 --- a/akarpov/templates/files/list.html +++ b/akarpov/templates/files/list.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load humanize static %} +{% load humanize static crispy_forms_tags %} {% block javascript %} @@ -33,67 +33,101 @@ {% endblock %} {% block content %} -{% for folder in folders %} - {{ folder.name }} -{% endfor %} -
-{% if request.user.is_authenticated %} -
-
- {% csrf_token %} -
- Image uploader - -

Drag & Drop file(s)
or

- - - -
- - - - - - - -

- -
-
-
-{% endif %} -{% for file in basefileitem_list %} -
- {% if file.is_file %} - - {% else %} -
-
{{ file.name }}
-

{{ file.size | filesizeformat }}, {{ file.amount }} {% if file.amount == 1 %} item {% else %} items {% endif %}

-
- -
- - -

{{ file.modified | naturaltime }}

-
- - {% endif %} +
+ {% if is_folder_owner %} + {% if folder_slug %} + + {% else %} + + {% endif %} + {% endif %} + -{% endfor %} + {% if request.user.is_authenticated and is_folder_owner %} +
+
+ {% csrf_token %} +
+ Image uploader + +

Drag & Drop file(s)
or

+ + + +
+ + + + + + + +

+ +
+
+
+
+
+
Add folder
+
+ {% csrf_token %} + {% for field in folder_form %} + {{ field|as_crispy_field }} + {% endfor %} +
+ +
+
+
+
+ {% endif %} + {% for file in basefileitem_list %} +
+ {% if file.is_file %} +
+
{{ file.name }}
+

{{ file.file_size | filesizeformat }}

+
+ +
+ + +

{{ file.modified | naturaltime }}

+
+ {% else %} +
+
{{ file.name }}
+

{{ file.size | filesizeformat }}, {{ file.amount }} {% if file.amount == 1 %} item {% else %} items {% endif %}

+
+ +
+ + +

{{ file.modified | naturaltime }}

+
+ + {% endif %} +
+ {% endfor %}
{% if page_obj.has_other_pages %}
diff --git a/akarpov/templates/files/tables.html b/akarpov/templates/files/tables.html new file mode 100644 index 0000000..53504a1 --- /dev/null +++ b/akarpov/templates/files/tables.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% load django_tables2 crispy_forms_tags %} + +{% block css %} + +{% endblock %} + + +{% block content %} + +
+
+ {% for field in filter.form %} +
{{ field | as_crispy_field }}
+ {% endfor %} +
+
+
+ +
+ +
+
+
+ {% render_table table %} +
+{% endblock %} diff --git a/akarpov/templates/files/view.html b/akarpov/templates/files/view.html index f784910..a288229 100644 --- a/akarpov/templates/files/view.html +++ b/akarpov/templates/files/view.html @@ -14,6 +14,17 @@ {% block content %} +{% if has_perm %} + +{% endif %}

{{ file.name }} {% if has_perm %} diff --git a/akarpov/test_platform/templates/files/folder.html b/akarpov/test_platform/templates/files/folder.html deleted file mode 100644 index e69de29..0000000 diff --git a/akarpov/test_platform/templates/files/view.html b/akarpov/test_platform/templates/files/view.html deleted file mode 100644 index e69de29..0000000 diff --git a/config/settings/base.py b/config/settings/base.py index 09961c2..b4adb23 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -115,6 +115,8 @@ "akarpov.contrib.chunked_upload", "active_link", "robots", + "django_filters", + "django_tables2", # django-cms "cms", "menus", diff --git a/poetry.lock b/poetry.lock index 3eb2ec9..03cffa7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1519,6 +1519,21 @@ files = [ [package.dependencies] 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]] name = "django-formtools" version = "2.4" @@ -1757,6 +1772,24 @@ files = [ django = "*" 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]] name = "django-timezone-field" 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"] 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]] name = "termcolor" version = "2.2.0" @@ -5642,4 +5697,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "667d1226a682d79bea11fdee05d079b1b494ac325167ca069f79f4d18d1a7d04" +content-hash = "c065bd831f46b58518895fa101821a580bebe3f7792348ca31d6a4fb4aab2b82" diff --git a/pyproject.toml b/pyproject.toml index 69fc439..ab7abe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,9 @@ cairosvg = "^2.7.0" textract = "^1.6.5" spotipy = "2.16.1" django-robots = "^5.0" +django-tables2 = "^2.5.3" +django-filter = "^23.1" +tablib = "^3.4.0" [build-system]