From a98ac48d8772ca6f8403f3e6c7835b628dc91923 Mon Sep 17 00:00:00 2001
From: mrmn2 <188248364+mrmn2@users.noreply.github.com>
Date: Fri, 9 Jan 2026 14:11:00 +0100
Subject: [PATCH 2/3] feat: Add Shared pdf details sidebar
---
.../includes/shared_details_sidebar.html | 36 +++++++++++++++++++
1 file changed, 36 insertions(+)
create mode 100644 pdfding/pdf/templates/includes/shared_details_sidebar.html
diff --git a/pdfding/pdf/templates/includes/shared_details_sidebar.html b/pdfding/pdf/templates/includes/shared_details_sidebar.html
new file mode 100644
index 00000000..f3472024
--- /dev/null
+++ b/pdfding/pdf/templates/includes/shared_details_sidebar.html
@@ -0,0 +1,36 @@
+{% extends 'layouts/base_sidebar.html' %}
+{% block sidebar_content %}
+
+{% endblock %}
From f1e1e89a5b64a13aa6b899801c1a53f9e59dbd1d Mon Sep 17 00:00:00 2001
From: mrmn2 <188248364+mrmn2@users.noreply.github.com>
Date: Sun, 18 Jan 2026 10:56:54 +0100
Subject: [PATCH 3/3] feat: Add collection details
---
pdfding/e2e/test_collection_e2e.py | 42 +++++++
pdfding/e2e/test_workspace_e2e.py | 24 ++--
pdfding/pdf/forms.py | 25 ++++
pdfding/pdf/services/collection_services.py | 52 ++++++++
pdfding/pdf/templates/collection_details.html | 111 ++++++++++++++++++
pdfding/pdf/templates/includes/sidebar.html | 4 +-
.../templates/includes/workspace_sidebar.html | 22 +++-
.../partials/collection_dropdown.html | 29 +++--
.../partials/workspace_dropdown.html | 8 +-
.../test_services/test_collection_services.py | 58 +++++++++
.../tests/test_views/test_collection_views.py | 72 +++++++++++-
.../pdf/tests/test_views/test_pdf_views.py | 8 ++
.../pdf/tests/test_views/test_share_views.py | 15 +++
.../tests/test_views/test_workspace_views.py | 14 +++
pdfding/pdf/urls.py | 4 +
pdfding/pdf/views/collection_views.py | 67 ++++++++++-
pdfding/pdf/views/pdf_views.py | 2 +
pdfding/pdf/views/share_views.py | 8 +-
pdfding/pdf/views/workspace_views.py | 30 ++++-
pdfding/users/models.py | 11 ++
pdfding/users/tests/test_models.py | 15 ++-
21 files changed, 589 insertions(+), 32 deletions(-)
create mode 100644 pdfding/pdf/services/collection_services.py
create mode 100644 pdfding/pdf/templates/collection_details.html
create mode 100644 pdfding/pdf/tests/test_services/test_collection_services.py
diff --git a/pdfding/e2e/test_collection_e2e.py b/pdfding/e2e/test_collection_e2e.py
index efefbb27..7718227d 100644
--- a/pdfding/e2e/test_collection_e2e.py
+++ b/pdfding/e2e/test_collection_e2e.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
from django.contrib.auth.models import User
from django.urls import reverse
from helpers import PdfDingE2ETestCase
@@ -33,3 +35,43 @@ def test_change_collection(self):
self.page.locator("#current_collection_name").click()
self.page.locator("#collection_modal").get_by_text("other_collection").click()
expect(self.page.locator("#current_collection_name")).to_contain_text("other_collection")
+
+ def test_details(self):
+ collection = self.user.profile.current_collection
+
+ with sync_playwright() as p:
+ self.open(reverse('collection_details', kwargs={'identifier': collection.id}), p)
+
+ expect(self.page.locator("#name")).to_contain_text("Defaul")
+ expect(self.page.locator("#default_collection")).to_contain_text("Yes")
+ expect(self.page.locator("#description")).to_contain_text("Default Collection")
+
+ @patch('pdf.services.collection_services.move')
+ def test_change_details(self, mock_move):
+ collection = self.user.profile.current_collection
+
+ with sync_playwright() as p:
+ self.open(reverse('collection_details', kwargs={'identifier': collection.id}), p)
+
+ self.page.locator("#name-edit").click()
+ self.page.locator("#id_name").dblclick()
+ self.page.locator("#id_name").fill("other-name")
+ self.page.get_by_role("button", name="Submit").click()
+ expect(self.page.locator("#name")).to_contain_text("other-name")
+ self.page.locator("#description-edit").click()
+ self.page.locator("#id_description").click()
+ self.page.locator("#id_description").fill("other description")
+ self.page.get_by_role("button", name="Submit").click()
+ expect(self.page.locator("#description")).to_contain_text("other description")
+
+ def test_details_change_collection(self):
+ create_collection(self.user.profile.current_workspace, 'other_collection')
+
+ collection = self.user.profile.current_collection
+
+ with sync_playwright() as p:
+ self.open(reverse('collection_details', kwargs={'identifier': collection.id}), p)
+ expect(self.page.locator("#current_collection_name")).to_contain_text("Default")
+ self.page.locator("#current_collection_name").click()
+ self.page.get_by_text("other_collection").click()
+ expect(self.page.locator("#current_collection_name")).to_contain_text("other_collection")
diff --git a/pdfding/e2e/test_workspace_e2e.py b/pdfding/e2e/test_workspace_e2e.py
index 16574cb8..1668776d 100644
--- a/pdfding/e2e/test_workspace_e2e.py
+++ b/pdfding/e2e/test_workspace_e2e.py
@@ -38,21 +38,17 @@ def test_change_workspace(self):
expect(self.page.locator("#current_collection_name")).to_contain_text("All")
def test_details(self):
- ws = self.user.profile.current_workspace
-
with sync_playwright() as p:
- self.open(reverse('workspace_details', kwargs={'identifier': ws.id}), p)
+ self.open(reverse('workspace_details'), p)
expect(self.page.locator("#name")).to_contain_text("Personal")
expect(self.page.locator("#personal_workspace")).to_contain_text("Yes")
expect(self.page.locator("#description")).to_contain_text("Personal Workspace")
def test_change_details(self):
- ws = self.user.profile.current_workspace
-
# also test changing from inactive to active
with sync_playwright() as p:
- self.open(reverse('workspace_details', kwargs={'identifier': ws.id}), p)
+ self.open(reverse('workspace_details'), p)
self.page.locator("#name-edit").click()
self.page.locator("#id_name").dblclick()
@@ -65,6 +61,16 @@ def test_change_details(self):
self.page.get_by_role("button", name="Submit").click()
expect(self.page.locator("#description")).to_contain_text("other description")
+ def test_details_change_workspace(self):
+ create_workspace('other_ws', self.user)
+
+ with sync_playwright() as p:
+ self.open(reverse('workspace_details'), p)
+ expect(self.page.locator("#current_ws_name")).to_contain_text("Personal")
+ self.page.locator("#current_ws_name").click()
+ self.page.get_by_text("other_ws").click()
+ expect(self.page.locator("#current_ws_name")).to_contain_text("other_ws")
+
def test_cancel_delete(self):
other_ws = create_workspace('other_ws', self.user)
self.user.profile.current_workspace_id = other_ws.id
@@ -72,7 +78,7 @@ def test_cancel_delete(self):
with sync_playwright() as p:
# only display one pdf
- self.open(reverse('workspace_details', kwargs={'identifier': other_ws.id}), p)
+ self.open(reverse('workspace_details'), p)
expect(self.page.locator("#delete_workspace_modal").first).not_to_be_visible()
self.page.locator("#delete-workspace").click()
@@ -86,7 +92,7 @@ def test_delete(self):
self.user.profile.save()
with sync_playwright() as p:
- self.open(reverse('workspace_details', kwargs={'identifier': other_ws.id}), p)
+ self.open(reverse('workspace_details'), p)
self.page.locator("#delete-workspace").click()
self.page.locator("#confirm_delete").get_by_text("Submit").click()
@@ -96,6 +102,6 @@ def test_delete(self):
def test_delete_not_visible_personal(self):
with sync_playwright() as p:
- self.open(reverse('workspace_details', kwargs={'identifier': self.user.id}), p)
+ self.open(reverse('workspace_details'), p)
expect(self.page.locator("#delete-workspace").first).not_to_be_visible()
diff --git a/pdfding/pdf/forms.py b/pdfding/pdf/forms.py
index 623c3e92..ff60ee66 100644
--- a/pdfding/pdf/forms.py
+++ b/pdfding/pdf/forms.py
@@ -5,6 +5,7 @@
from django.contrib.auth.hashers import check_password, make_password
from django.core.exceptions import ValidationError
from django.core.files import File
+from pdf.models.collection_models import Collection
from pdf.models.pdf_models import Pdf
from pdf.models.shared_pdf_models import SharedPdf
from pdf.models.workspace_models import Workspace
@@ -535,6 +536,30 @@ def clean_name(self) -> str:
return collection_name
+class CollectionNameForm(forms.ModelForm):
+ """Form for changing the name of a collection."""
+
+ class Meta:
+ model = Collection
+ fields = ['name']
+
+ def clean_name(self) -> str: # pragma: no cover
+ """Clean the submitted collection name. Removes trailing and multiple whitespaces."""
+
+ collection_name = CleanHelpers.clean_workspace_name(self.cleaned_data['name'])
+
+ return collection_name
+
+
+class CollectionDescriptionForm(forms.ModelForm):
+ """Form for changing the description of a collection."""
+
+ class Meta:
+ model = Collection
+ widgets = {'description': forms.Textarea(attrs={'rows': 3})}
+ fields = ['description']
+
+
class CleanHelpers:
@staticmethod
def clean_file(file: File) -> File:
diff --git a/pdfding/pdf/services/collection_services.py b/pdfding/pdf/services/collection_services.py
new file mode 100644
index 00000000..852c097e
--- /dev/null
+++ b/pdfding/pdf/services/collection_services.py
@@ -0,0 +1,52 @@
+from shutil import move
+
+from core.settings import MEDIA_ROOT
+from pdf.models.collection_models import Collection
+from pdf.models.pdf_models import Pdf
+
+
+def move_collection(collection: Collection) -> None:
+ """
+ Change the name of a collection and rename the collection directory on file system
+ accordingly. Also handle all PDFs and shared PDFs.
+ """
+
+ # The new name is already set to the collection object by the form but not saved yet.
+ new_collection_name = collection.name.lower()
+ old_collection_name = Collection.objects.get(id=collection.id).name.lower()
+
+ # move all files
+ old_collection_path = MEDIA_ROOT / collection.workspace.id / old_collection_name
+ new_collection_path = MEDIA_ROOT / collection.workspace.id / new_collection_name
+ move(old_collection_path, new_collection_path)
+
+ # adjust the file paths of the pdf and shared pdf objects
+ for pdf in collection.pdfs:
+ adjust_pdf_path(pdf, f'/{old_collection_name}/', f'/{new_collection_name}/')
+
+ collection.save()
+
+
+def adjust_pdf_path(pdf: Pdf, to_be_replaced: str, replace_with: str) -> None:
+ """Adjust path of PDF and its shared PDFs when the path of a collection is changed"""
+
+ old_pdf_file_name = pdf.file.name
+ new_pdf_file_name = old_pdf_file_name.replace(to_be_replaced, replace_with, 1)
+ pdf.file.name = new_pdf_file_name
+
+ old_preview_file_name = pdf.preview.name
+ new_preview_file_name = old_preview_file_name.replace(to_be_replaced, replace_with, 1)
+ pdf.preview.name = new_preview_file_name
+
+ old_thumbnail_file_name = pdf.thumbnail.name
+ new_thumbnail_file_name = old_thumbnail_file_name.replace(to_be_replaced, replace_with, 1)
+ pdf.thumbnail.name = new_thumbnail_file_name
+
+ pdf.save()
+
+ for shared_pdf in pdf.sharedpdf_set.all():
+ old_qr_file_name = shared_pdf.file.name
+ new_qr_file_name = old_qr_file_name.replace(to_be_replaced, replace_with, 1)
+
+ shared_pdf.file.name = new_qr_file_name
+ shared_pdf.save()
diff --git a/pdfding/pdf/templates/collection_details.html b/pdfding/pdf/templates/collection_details.html
new file mode 100644
index 00000000..9cfd06f3
--- /dev/null
+++ b/pdfding/pdf/templates/collection_details.html
@@ -0,0 +1,111 @@
+{% extends 'layouts/blank.html' %}
+
+{% block content %}
+
+
+
+ {% include 'includes/workspace_sidebar.html' with page='collection_details' %}
+
+
+
+
+
+ Collection Details
+
+
+ Name
+
+
+
+
+ {{ collection.name }}
+
+
+
+
+
+ Default Collection
+
+
+
+
+ {% if collection.default_collection %}
+ Yes
+ {% else %}
+ No
+ {% endif%}
+
+
+
+
+ Description
+
+
+
+
+ {% if collection.description %}
+ {{ collection.description }}
+ {% else %}
+ no description available
+ {% endif%}
+
+
+
+
+
+ Date added
+
+
+
+ {{ collection.creation_date }}
+
+
+ {% if not collection.default_collection %}
+
Danger Zone
+
+ Delete Account
+
+
+
+ Delete the collection and all its PDFs
+
+
+
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/pdfding/pdf/templates/includes/sidebar.html b/pdfding/pdf/templates/includes/sidebar.html
index f45b519a..cd98c7ec 100644
--- a/pdfding/pdf/templates/includes/sidebar.html
+++ b/pdfding/pdf/templates/includes/sidebar.html
@@ -11,8 +11,10 @@
/>
- {% include 'partials/workspace_dropdown.html' with page='pdf_overview' %}
+ {% include 'partials/workspace_dropdown.html' with page='overview' %}
+ {% if current_collection_id %}
{% include 'partials/collection_dropdown.html' %}
+ {% endif %}
{% endblock %}
diff --git a/pdfding/pdf/templates/partials/collection_dropdown.html b/pdfding/pdf/templates/partials/collection_dropdown.html
index cfd04c50..503a7700 100644
--- a/pdfding/pdf/templates/partials/collection_dropdown.html
+++ b/pdfding/pdf/templates/partials/collection_dropdown.html
@@ -14,11 +14,7 @@
- {% if request.user.profile.current_collection_id == 'all' %}
- All
- {% else %}
- {{ request.user.profile.current_collection.name }}
- {% endif %}
+ {{ current_collection_name }}