diff --git a/akarpov/files/migrations/0001_squashed_0016_alter_file_file_type_alter_file_name.py b/akarpov/files/migrations/0001_squashed_0016_alter_file_file_type_alter_file_name.py new file mode 100644 index 0000000..8574c9b --- /dev/null +++ b/akarpov/files/migrations/0001_squashed_0016_alter_file_file_type_alter_file_name.py @@ -0,0 +1,223 @@ +# Generated by Django 4.2 on 2023-04-22 09:07 + +import akarpov.files.services.files +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + replaces = [ + ("files", "0001_initial"), + ("files", "0002_alter_basefile_options_alter_folder_options_and_more"), + ("files", "0003_basefile_short_link_folder_short_link"), + ("files", "0004_alter_basefile_short_link_alter_folder_short_link"), + ("files", "0005_alter_basefile_description_alter_basefile_folder_and_more"), + ("files", "0006_alter_basefile_slug"), + ("files", "0007_file_alter_folder_parent_delete_basefile_file_folder_and_more"), + ("files", "0008_file_completed_on_file_created_on_file_filename_and_more"), + ("files", "0009_remove_file_completed_on_remove_file_created_on_and_more"), + ("files", "0010_fileintrash"), + ("files", "0011_file_file_type_alter_file_description_and_more"), + ("files", "0012_alter_file_options_alter_file_file"), + ("files", "0013_alter_file_options"), + ("files", "0014_alter_fileintrash_file"), + ("files", "0015_fileintrash_name"), + ("files", "0016_alter_file_file_type_alter_file_name"), + ] + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("shortener", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Folder", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("name", models.CharField(max_length=100)), + ("slug", models.SlugField(blank=True, max_length=20)), + ( + "parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="files.folder", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="files_folders", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "short_link", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="shortener.link", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="FileInTrash", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "file", + models.FileField( + upload_to=akarpov.files.services.files.trash_file_upload + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="trash_files", + to=settings.AUTH_USER_MODEL, + ), + ), + ("name", models.CharField(blank=True, max_length=200)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="File", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(blank=True, max_length=20, unique=True)), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ("name", models.CharField(blank=True, max_length=255, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("private", models.BooleanField(default=True)), + ("preview", models.FileField(blank=True, upload_to="file/previews/")), + ( + "file", + models.FileField( + upload_to=akarpov.files.services.files.user_unique_file_upload + ), + ), + ( + "folder", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to="files.folder", + ), + ), + ( + "short_link", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="shortener.link", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to=settings.AUTH_USER_MODEL, + ), + ), + ("file_type", models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + "abstract": False, + "ordering": ["-modified"], + }, + ), + ] diff --git a/akarpov/files/migrations/0017_alter_folder_options_rename_file_file_file_obj_and_more.py b/akarpov/files/migrations/0017_alter_folder_options_rename_file_file_file_obj_and_more.py new file mode 100644 index 0000000..e9904ee --- /dev/null +++ b/akarpov/files/migrations/0017_alter_folder_options_rename_file_file_file_obj_and_more.py @@ -0,0 +1,106 @@ +# Generated by Django 4.2 on 2023-04-22 09:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("files", "0001_squashed_0016_alter_file_file_type_alter_file_name"), + ] + + operations = [ + migrations.AlterModelOptions( + name="folder", + options={"base_manager_name": "objects"}, + ), + migrations.RenameField( + model_name="file", + old_name="file", + new_name="file_obj", + ), + migrations.RemoveField( + model_name="file", + name="folder", + ), + migrations.RemoveField( + model_name="file", + name="id", + ), + migrations.RemoveField( + model_name="folder", + name="id", + ), + migrations.RemoveField( + model_name="folder", + name="parent", + ), + migrations.CreateModel( + name="BaseFileItem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="files.basefileitem", + ), + ), + ( + "polymorphic_ctype", + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="polymorphic_%(app_label)s.%(class)s_set+", + to="contenttypes.contenttype", + ), + ), + ], + options={ + "abstract": False, + "base_manager_name": "objects", + }, + ), + migrations.AddField( + model_name="file", + name="basefileitem_ptr", + field=models.OneToOneField( + auto_created=True, + default=1, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="files.basefileitem", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="folder", + name="basefileitem_ptr", + field=models.OneToOneField( + auto_created=True, + default=2, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="files.basefileitem", + ), + preserve_default=False, + ), + ] diff --git a/akarpov/files/migrations/0018_remove_file_created_remove_file_modified_and_more.py b/akarpov/files/migrations/0018_remove_file_created_remove_file_modified_and_more.py new file mode 100644 index 0000000..be8f991 --- /dev/null +++ b/akarpov/files/migrations/0018_remove_file_created_remove_file_modified_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2 on 2023-04-22 09:21 + +from django.db import migrations +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("files", "0017_alter_folder_options_rename_file_file_file_obj_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="file", + name="created", + ), + migrations.RemoveField( + model_name="file", + name="modified", + ), + migrations.RemoveField( + model_name="folder", + name="created", + ), + migrations.RemoveField( + model_name="folder", + name="modified", + ), + migrations.AddField( + model_name="basefileitem", + name="created", + field=model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + migrations.AddField( + model_name="basefileitem", + name="modified", + field=model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="updated", + ), + ), + ] diff --git a/akarpov/files/migrations/0019_alter_basefileitem_options_alter_file_options_and_more.py b/akarpov/files/migrations/0019_alter_basefileitem_options_alter_file_options_and_more.py new file mode 100644 index 0000000..adc3eb8 --- /dev/null +++ b/akarpov/files/migrations/0019_alter_basefileitem_options_alter_file_options_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2 on 2023-04-22 09:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("files", "0018_remove_file_created_remove_file_modified_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="basefileitem", + options={"ordering": ["-modified"]}, + ), + migrations.AlterModelOptions( + name="file", + options={"base_manager_name": "objects"}, + ), + migrations.RemoveField( + model_name="file", + name="user", + ), + migrations.RemoveField( + model_name="folder", + name="user", + ), + migrations.AddField( + model_name="basefileitem", + name="user", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to=settings.AUTH_USER_MODEL, + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="basefileitem", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="files.folder", + ), + ), + ] diff --git a/akarpov/files/migrations/0020_folder_size.py b/akarpov/files/migrations/0020_folder_size.py new file mode 100644 index 0000000..835c8ca --- /dev/null +++ b/akarpov/files/migrations/0020_folder_size.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2 on 2023-04-22 09:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("files", "0019_alter_basefileitem_options_alter_file_options_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="folder", + name="size", + field=models.IntegerField(default=0), + ), + ] diff --git a/akarpov/files/migrations/0021_folder_amount_folder_private.py b/akarpov/files/migrations/0021_folder_amount_folder_private.py new file mode 100644 index 0000000..6498b49 --- /dev/null +++ b/akarpov/files/migrations/0021_folder_amount_folder_private.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2 on 2023-04-22 09:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("files", "0020_folder_size"), + ] + + operations = [ + migrations.AddField( + model_name="folder", + name="amount", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="folder", + name="private", + field=models.BooleanField(default=True), + ), + ] diff --git a/akarpov/files/models.py b/akarpov/files/models.py index d19ddf7..d03e7e3 100644 --- a/akarpov/files/models.py +++ b/akarpov/files/models.py @@ -7,28 +7,60 @@ CharField, FileField, ForeignKey, + IntegerField, SlugField, TextField, ) from django.urls import reverse +from model_utils.fields import AutoCreatedField, AutoLastModifiedField from model_utils.models import TimeStampedModel +from polymorphic.models import PolymorphicModel from akarpov.files.services.files import trash_file_upload, user_unique_file_upload from akarpov.tools.shortener.models import ShortLink -class File(TimeStampedModel, ShortLink): +class BaseFileItem(PolymorphicModel): + parent = ForeignKey( + to="files.Folder", + null=True, + blank=True, + on_delete=CASCADE, + related_name="children", + ) + user = ForeignKey("users.User", related_name="files", on_delete=CASCADE) + created = AutoCreatedField("created") + modified = AutoLastModifiedField("updated") + + class Meta: + ordering = ["-modified"] + + @property + def is_file(self): + return type(self) is File + + def get_folder_chain(self): + folders = [self] + obj = self + while obj.parent: + folders.append(obj.parent) + obj = obj.parent + return folders + + def save(self, *args, **kwargs): + update_fields = kwargs.get("update_fields", None) + if update_fields: + kwargs["update_fields"] = set(update_fields).union({"modified"}) + super().save(*args, **kwargs) + + +class File(BaseFileItem, TimeStampedModel, ShortLink): """model to store user's files""" private = BooleanField(default=True) - user = ForeignKey("users.User", related_name="files", on_delete=CASCADE) - folder = ForeignKey( - "files.Folder", related_name="files", blank=True, null=True, on_delete=CASCADE - ) - preview = FileField(blank=True, upload_to="file/previews/") - file = FileField(blank=False, upload_to=user_unique_file_upload) + file_obj = FileField(blank=False, upload_to=user_unique_file_upload) # meta name = CharField(max_length=255, null=True, blank=True) @@ -39,6 +71,10 @@ class File(TimeStampedModel, ShortLink): def file_name(self): return self.file.path.split("/")[-1] + @property + def file(self): + return self.file_obj + @property def file_image_url(self): if self.preview: @@ -61,27 +97,24 @@ def get_absolute_url(self): def __str__(self): return f"file: {self.name}" - class Meta: - ordering = ["-modified"] + +class Folder(BaseFileItem, ShortLink): + name = CharField(max_length=100) + slug = SlugField(max_length=20, blank=True) + private = BooleanField(default=True) + + # meta + size = IntegerField(default=0) + amount = IntegerField(default=0) + + def get_absolute_url(self): + return reverse("files:folder", kwargs={"slug": self.slug}) + + def __str__(self): + return f"folder: {self.name}" class FileInTrash(TimeStampedModel): name = CharField(max_length=200, blank=True) user = ForeignKey("users.User", related_name="trash_files", on_delete=CASCADE) file = FileField(blank=False, upload_to=trash_file_upload) - - -class Folder(TimeStampedModel, ShortLink): - name = CharField(max_length=100) - slug = SlugField(max_length=20, blank=True) - - user = ForeignKey("users.User", related_name="files_folders", on_delete=CASCADE) - parent = ForeignKey( - "self", null=True, blank=True, related_name="children", on_delete=CASCADE - ) - - def get_absolute_url(self): - return reverse("files:folder", kwargs={"slug": self.slug}) - - def __str__(self): - return f"file: {self.name}" diff --git a/akarpov/files/previews/text/common.py b/akarpov/files/previews/text/common.py index 9b85c94..8b98e23 100644 --- a/akarpov/files/previews/text/common.py +++ b/akarpov/files/previews/text/common.py @@ -65,18 +65,18 @@ def view(file: File) -> (str, str): def meta(file: File): - descr = "" + description = "" i = 0 with file.file.open("r") as f: lines = f.readlines() for line in lines: if i == 0: - descr += line + "\n" + description += line + "\n" else: - descr += line + " " + description += line + " " i += 1 if i > 20: - descr += "..." + description += "..." break url = file.get_absolute_url() section = "" @@ -88,7 +88,7 @@ def meta(file: File): - + diff --git a/akarpov/files/previews/text/html.py b/akarpov/files/previews/text/html.py index 72fd393..7db7e21 100644 --- a/akarpov/files/previews/text/html.py +++ b/akarpov/files/previews/text/html.py @@ -47,36 +47,3 @@ def view(file: File) -> (str, str): """ return static, content - - -def meta(file: File): - descr = "" - i = 0 - with file.file.open("r") as f: - lines = f.readlines() - for line in lines: - if i == 0: - descr += line + "\n" - else: - descr += line + " " - i += 1 - if i > 20: - descr += "..." - break - url = file.get_absolute_url() - section = "" - if file.file_type: - section = file.file_type.split("/")[0] - - meat_f = f""" - - - - - - - - - - """ - return meat_f diff --git a/akarpov/files/urls.py b/akarpov/files/urls.py index 275c4a5..12de9dd 100644 --- a/akarpov/files/urls.py +++ b/akarpov/files/urls.py @@ -1,7 +1,6 @@ from django.urls import path from akarpov.files.views import ( - ChunkedUploadDemo, MyChunkedUploadCompleteView, MyChunkedUploadView, TopFolderView, @@ -14,12 +13,16 @@ app_name = "files" urlpatterns = [ path("", TopFolderView.as_view(), name="main"), - path("upload", ChunkedUploadDemo.as_view(), name="chunked_upload"), path( "api/chunked_upload_complete/", MyChunkedUploadCompleteView.as_view(), name="api_chunked_upload_complete", ), + path( + "api/chunked_upload_complete/", + MyChunkedUploadCompleteView.as_view(), + name="api_chunked_upload_complete_folder", + ), path( "api/chunked_upload/", MyChunkedUploadView.as_view(), name="api_chunked_upload" ), diff --git a/akarpov/files/views.py b/akarpov/files/views.py index 3f58af4..c24d04c 100644 --- a/akarpov/files/views.py +++ b/akarpov/files/views.py @@ -6,6 +6,7 @@ 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 @@ -16,7 +17,7 @@ ChunkedUploadView, ) from akarpov.files.forms import FileForm -from akarpov.files.models import File, Folder +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 @@ -26,17 +27,43 @@ class TopFolderView(LoginRequiredMixin, ListView): template_name = "files/list.html" paginate_by = 19 - model = File - - def get_queryset(self): - return File.objects.filter(user=self.request.user, folder__isnull=True) + model = BaseFileItem def get_context_data(self, **kwargs): - contex = super().get_context_data(**kwargs) - contex["folders"] = Folder.objects.filter( - user=self.request.user, parent__isnull=True - ) - return contex + context = super().get_context_data(**kwargs) + context["folder_slug"] = None + return context + + def get_queryset(self): + return BaseFileItem.objects.filter(user=self.request.user, parent__isnull=True) + + +class FileFolderView(ListView): + template_name = "files/folder.html" + model = BaseFileItem + paginate_by = 39 + slug_field = "slug" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.object = None + + def get_context_data(self, **kwargs): + folder = self.get_object() + context = super().get_context_data(**kwargs) + context["folder_slug"] = folder.slug + return context + + def get_object(self, *args): + obj = get_object_or_404(Folder, slug=self.kwargs["slug"]) + if self.object: + return self.object + self.object = obj + return obj + + def get_queryset(self): + folder = self.get_object() + return BaseFileItem.objects.filter(parent=folder) class FileUpdateView(LoginRequiredMixin, UpdateView): @@ -131,12 +158,6 @@ def get_redirect_url(self, *args, **kwargs): delete_file_view = DeleteFileView.as_view() -class FileFolderView(DetailView): - template_name = "files/folder.html" - model = Folder - slug_field = "slug" - - folder_view = FileFolderView.as_view() @@ -169,17 +190,42 @@ def check_permissions(self, request): ) def on_completion(self, uploaded_file, request): - if uploaded_file.size <= request.user.left_file_upload: + folder = None + prepared = True + if "slug" in self.kwargs and self.kwargs["slug"]: + try: + folder = Folder.objects.get(slug=self.kwargs["slug"]) + if folder.user != self.request.user: + self.message = { + "message": "You can't upload to this folder", + "status": False, + } + prepared = False + except Folder.DoesNotExist: + self.message = { + "message": "Folder doesn't exist", + "status": False, + } + prepared = False + if prepared and uploaded_file.size <= request.user.left_file_upload: f = File.objects.create( - user=request.user, file=uploaded_file, name=uploaded_file.name + user=request.user, + file_obj=uploaded_file, + 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 = { "message": f"File {f.file.name.split('/')[-1]} successfully uploaded", "status": True, } - else: + elif prepared: self.message = { "message": "File is too large, please increase disk space", "status": False, diff --git a/akarpov/templates/files/folder.html b/akarpov/templates/files/folder.html new file mode 100644 index 0000000..1b91213 --- /dev/null +++ b/akarpov/templates/files/folder.html @@ -0,0 +1 @@ +{% extends 'files/list.html' %} diff --git a/akarpov/templates/files/list.html b/akarpov/templates/files/list.html index 3c3a827..5c52aa1 100644 --- a/akarpov/templates/files/list.html +++ b/akarpov/templates/files/list.html @@ -37,6 +37,7 @@ {{ folder.name }} {% endfor %}
+{% if request.user.is_authenticated %}
{% csrf_token %} @@ -64,8 +65,10 @@
-{% for file in file_list %} +{% endif %} +{% for file in basefileitem_list %}
+ {% if file.is_file %}
{{ file.name }}

{{ file.file_size | filesizeformat }}

@@ -76,6 +79,19 @@

{{ file.modified | naturaltime }}

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

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

+
+ +
+ + +

{{ file.modified | naturaltime }}

+
+ + {% endif %}
{% endfor %}
@@ -207,7 +223,11 @@ sel.text(progress + "%"); $.ajax({ type: "POST", + {% if folder_slug %} + url: "{% url 'files:api_chunked_upload_complete_folder' slug=folder_slug %}", + {% else %} url: "{% url 'files:api_chunked_upload_complete' %}", + {% endif %} data: { csrfmiddlewaretoken: csrf, upload_id: data.result.upload_id, diff --git a/models.png b/models.png index 8077625..a67f49c 100644 Binary files a/models.png and b/models.png differ