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/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 %} 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 %}
- + @@ -35,5 +40,16 @@
+
+ + + + + + + + + +
{% 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 }}
+ {% if page != 'collection_details' %}
All
- {% if request.user.profile.current_collection_id == 'all' %} + {% if current_collection_id == 'all' %}
{% endif %}
+ {% endif %} {% for collection in request.user.profile.collections %} + {% if page != 'collection_details' %}
{{ collection.name }}
- {% if collection.id == request.user.profile.current_collection_id %} + {% if collection.id == current_collection_id %}
{% endif %}
+ {% else %} + +
{{ collection.name }}
+ {% if collection.id == current_collection_id %} +
+ {% endif %} +
+ {% endif %} {% endfor %} diff --git a/pdfding/pdf/templates/partials/workspace_dropdown.html b/pdfding/pdf/templates/partials/workspace_dropdown.html index 829f04be..bf4facf8 100644 --- a/pdfding/pdf/templates/partials/workspace_dropdown.html +++ b/pdfding/pdf/templates/partials/workspace_dropdown.html @@ -1,8 +1,10 @@
Create Workspace - {% if page == 'pdf_overview' %} - + {% if page == 'overview' %} + diff --git a/pdfding/pdf/templates/pdf_details.html b/pdfding/pdf/templates/pdf_details.html index 8e13de71..a76f659e 100644 --- a/pdfding/pdf/templates/pdf_details.html +++ b/pdfding/pdf/templates/pdf_details.html @@ -5,7 +5,7 @@
{% include 'includes/pdf_details_sidebar.html' with page='pdf_details' %}
-
+
- {% include 'includes/sidebar.html' %} + {% include 'includes/shared_details_sidebar.html' with page='shared_details' %}
-
+
{% include 'includes/workspace_sidebar.html' with page='workspace_details' %}
-
+
diff --git a/pdfding/pdf/tests/test_services/test_collection_services.py b/pdfding/pdf/tests/test_services/test_collection_services.py new file mode 100644 index 00000000..98af7f87 --- /dev/null +++ b/pdfding/pdf/tests/test_services/test_collection_services.py @@ -0,0 +1,58 @@ +from unittest import mock + +from core.settings import MEDIA_ROOT +from django.contrib.auth.models import User +from django.test import TestCase +from pdf.models.pdf_models import Pdf +from pdf.models.shared_pdf_models import SharedPdf +from pdf.services import collection_services + + +class TestCollectionServices(TestCase): + def setUp(self) -> None: + self.user = User.objects.create_user(username='user', password='12345', email='a@a.com') + + @mock.patch('pdf.services.collection_services.adjust_pdf_path') + @mock.patch('pdf.services.collection_services.move') + def test_move_collection(self, mock_move, mock_adjust_pdf_path): + collection = self.user.profile.current_collection + pdf_1 = Pdf.objects.create(collection=collection, name='pdf_1') + pdf_2 = Pdf.objects.create(collection=collection, name='pdf_2') + assert collection.name == 'Default' + + collection.name = 'NEW_name' + collection_services.move_collection(collection) + + assert collection.name == 'NEW_name' + mock_move.assert_called_once_with( + MEDIA_ROOT / collection.workspace.id / 'default', MEDIA_ROOT / collection.workspace.id / 'new_name' + ) + assert mock_adjust_pdf_path.call_count == 2 + + mock_adjust_pdf_path.assert_has_calls( + [ + mock.call(pdf_1, '/default/', '/new_name/'), + mock.call(pdf_2, '/default/', '/new_name/'), + ], + any_order=True, + ) + + def test_adjust_pdf_path(self): + collection = self.user.profile.current_collection + pdf = Pdf.objects.create(collection=collection, name='pdf') + # make sure only the first occurence is replaced + pdf.file.name = '1/old/pdf/old/pdf.pdf' + pdf.preview.name = '1/old/previews/old/pdf' + pdf.thumbnail.name = '1/old/thumbnails/old/pdf' + pdf.save() + shared_pdf = SharedPdf.objects.create(pdf=pdf, name='shared_pdf') + shared_pdf.file.name = '1/old/qr/old/pdf' + shared_pdf.save() + + collection_services.adjust_pdf_path(pdf, '/old/', '/new/') + assert pdf.file.name == '1/new/pdf/old/pdf.pdf' + assert pdf.thumbnail.name == '1/new/thumbnails/old/pdf' + assert pdf.preview.name == '1/new/previews/old/pdf' + # do not ask me why we need to fetch the shared pdf and not the normal one + changed_shared_pdf = SharedPdf.objects.get(id=shared_pdf.id) + assert changed_shared_pdf.file.name == '1/new/qr/old/pdf' diff --git a/pdfding/pdf/tests/test_views/test_collection_views.py b/pdfding/pdf/tests/test_views/test_collection_views.py index afa92430..f5715607 100644 --- a/pdfding/pdf/tests/test_views/test_collection_views.py +++ b/pdfding/pdf/tests/test_views/test_collection_views.py @@ -1,9 +1,13 @@ from unittest.mock import MagicMock, patch +import pytest from django.contrib.auth.models import User +from django.contrib.messages import get_messages +from django.http import Http404 from django.test import Client, TestCase from django.urls import reverse -from pdf.forms import CollectionForm +from pdf.forms import CollectionDescriptionForm, CollectionForm, CollectionNameForm +from pdf.services.workspace_services import create_collection, create_workspace from pdf.views import collection_views @@ -17,6 +21,28 @@ def setUp(self): self.client.login(username=self.username, password=self.password) +class TestCollectionMixin(CollectionTestCase): + def test_get_object(self): + # do a dummy request so we can get a request object + response = self.client.get(reverse('pdf_overview')) + + other_collection = create_collection(self.user.profile.current_workspace, 'other') + + assert other_collection == collection_views.CollectionMixin.get_object( + response.wsgi_request, other_collection.id + ) + + def test_get_object_other_workspace(self): + # do a dummy request so we can get a request object + response = self.client.get(reverse('pdf_overview')) + + other_ws = create_workspace('other', self.user) + other_collection = create_collection(other_ws, 'other') + + with pytest.raises(Http404, match='Given query not found...'): + collection_views.CollectionMixin.get_object(response.wsgi_request, other_collection.id) + + class TestCreateCollectionMixin(CollectionTestCase): @patch('pdf.views.collection_views.CreateCollectionMixin.form') def test_get_context_get(self, mock_add_form): @@ -49,3 +75,47 @@ def test_obj_save(self): self.assertEqual(created_collection.name, 'some_collection') self.assertEqual(created_collection.description, 'some_description') self.assertFalse(created_collection.default_collection) + + +class TestEditCollectionMixin(CollectionTestCase): + def test_get_edit_form_get(self): + collection = self.user.profile.current_collection + edit_collection_mixin_object = collection_views.EditCollectionMixin() + + for field, form_class, field_value in zip( + ['name', 'description'], + [CollectionNameForm, CollectionDescriptionForm], + ['Default', 'Default Collection'], + ): + form = edit_collection_mixin_object.get_edit_form_get(field, collection) + self.assertIsInstance(form, form_class) + self.assertEqual(form.initial, {field: field_value}) + + @patch('pdf.views.collection_views.move_collection') + def test_process_field_name(self, mock_move_collection): + # do a dummy request so we can get a request object + response = self.client.get(reverse('pdf_overview')) + collection = self.user.profile.current_collection + + collection_views.EditCollectionMixin.process_field( + 'name', collection, response.wsgi_request, {'name': 'new_name'} + ) + mock_move_collection.assert_called_once_with(collection) + + def test_process_field_name_existing(self): + # do a dummy request so we can get a request object + response = self.client.get(reverse('pdf_overview')) + request = response.wsgi_request + collection = self.user.profile.current_collection + create_collection(self.user.profile.current_workspace, 'existing') + + collection_views.EditCollectionMixin.process_field( + 'name', collection, response.wsgi_request, {'name': 'existing'} + ) + + messages = get_messages(request) + + self.assertEqual(len(messages), 1) + self.assertEqual( + list(messages)[0].message, 'This name is already used by another collection in this workspace!' + ) diff --git a/pdfding/pdf/tests/test_views/test_pdf_views.py b/pdfding/pdf/tests/test_views/test_pdf_views.py index 63d7713f..ff0aaf35 100644 --- a/pdfding/pdf/tests/test_views/test_pdf_views.py +++ b/pdfding/pdf/tests/test_views/test_pdf_views.py @@ -416,6 +416,8 @@ def test_get_extra_context(self, mock_get_tag_info_dict): 'page': 'pdf_overview', 'layout': 'Compact', 'needs_nagging': False, + 'current_collection_id': str(self.user.id), + 'current_collection_name': 'Default', } self.assertEqual(generated_extra_context, expected_extra_context) @@ -434,6 +436,8 @@ def test_get_extra_context_selection(self, mock_get_tag_info_dict): 'page': 'pdf_overview_starred', 'layout': 'Compact', 'needs_nagging': False, + 'current_collection_id': str(self.user.id), + 'current_collection_name': 'Default', } self.assertEqual(generated_extra_context, expected_extra_context) @@ -452,6 +456,8 @@ def test_get_extra_context_selection_invalid(self, mock_get_tag_info_dict): 'page': 'pdf_overview', 'layout': 'Compact', 'needs_nagging': False, + 'current_collection_id': str(self.user.id), + 'current_collection_name': 'Default', } self.assertEqual(generated_extra_context, expected_extra_context) @@ -470,6 +476,8 @@ def test_get_extra_context_empty_queries(self, mock_get_tag_info_dict): 'page': 'pdf_overview', 'layout': 'Compact', 'needs_nagging': False, + 'current_collection_id': str(self.user.id), + 'current_collection_name': 'Default', } self.assertEqual(generated_extra_context, expected_extra_context) diff --git a/pdfding/pdf/tests/test_views/test_share_views.py b/pdfding/pdf/tests/test_views/test_share_views.py index d942048d..1a2ba5a5 100644 --- a/pdfding/pdf/tests/test_views/test_share_views.py +++ b/pdfding/pdf/tests/test_views/test_share_views.py @@ -29,6 +29,8 @@ SharedPdfMixin, ) +from pdfding.pdf.views import share_views + def set_up(self): self.client = Client() @@ -130,6 +132,19 @@ def test_filter_objects(self): self.assertEqual(shared_names, ['shared_1', 'shared_2', 'shared_3']) + def test_get_extra_context(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(reverse('shared_pdf_overview')) + + generated_extra_context = share_views.OverviewMixin.get_extra_context(response.wsgi_request) + expected_extra_context = { + 'page': 'shared_pdf_overview', + 'current_collection_id': str(self.user.id), + 'current_collection_name': 'Default', + } + + self.assertEqual(generated_extra_context, expected_extra_context) + class TestSharedPdfMixin(TestCase): username = 'user' diff --git a/pdfding/pdf/tests/test_views/test_workspace_views.py b/pdfding/pdf/tests/test_views/test_workspace_views.py index 46971bf3..d9e02004 100644 --- a/pdfding/pdf/tests/test_views/test_workspace_views.py +++ b/pdfding/pdf/tests/test_views/test_workspace_views.py @@ -66,6 +66,20 @@ def test_get_object(self): assert ws == workspace_views.WorkspaceMixin.get_object(response.wsgi_request, ws.id) +class TestCollectionDetails(WorkspaceTestCase): + def test_get(self): + default_collection = self.user.profile.current_collection + + response = self.client.get(reverse('collection_details', kwargs={'identifier': default_collection.id})) + + self.assertTemplateUsed(response, 'collection_details.html') + + assert response.context['workspace'] == self.user.profile.current_workspace + assert response.context['collection'] == default_collection + assert response.context['current_collection_id'] == default_collection.id + assert response.context['current_collection_name'] == default_collection.name + + class TestEditWorkspaceMixin(WorkspaceTestCase): def test_get_edit_form_get(self): ws = self.user.profile.current_workspace diff --git a/pdfding/pdf/urls.py b/pdfding/pdf/urls.py index f741d9e5..be20e33c 100644 --- a/pdfding/pdf/urls.py +++ b/pdfding/pdf/urls.py @@ -83,10 +83,14 @@ path('edit_tag/', pdf_views.EditTag.as_view(), name='edit_tag'), # workspace related views path('workspace/create', workspace_views.Create.as_view(), name='create_workspace'), + path('workspace/details', workspace_views.Details.as_view(), name='workspace_details'), path('workspace/details/', workspace_views.Details.as_view(), name='workspace_details'), path('workspace/edit//', workspace_views.Edit.as_view(), name='edit_workspace'), path('workspace/delete/', workspace_views.Delete.as_view(), name='delete_workspace'), path('', pdf_views.Overview.as_view(), name='workspace_overview'), # needed for base views working path('collection/create', collection_views.Create.as_view(), name='create_collection'), + path('collection/details/', workspace_views.CollectionDetails.as_view(), name='collection_details'), + path('collection/edit//', collection_views.Edit.as_view(), name='edit_collection'), + path('collection/delete/', workspace_views.Delete.as_view(), name='delete_collection'), path('', pdf_views.Overview.as_view(), name='collection_overview'), # needed for base views working ] diff --git a/pdfding/pdf/views/collection_views.py b/pdfding/pdf/views/collection_views.py index 81ff0902..8bcc8cdd 100644 --- a/pdfding/pdf/views/collection_views.py +++ b/pdfding/pdf/views/collection_views.py @@ -1,6 +1,10 @@ from base import base_views +from django.contrib import messages from django.http import HttpRequest -from pdf.forms import CollectionForm +from pdf.forms import CollectionDescriptionForm, CollectionForm, CollectionNameForm +from pdf.models.collection_models import Collection +from pdf.services.collection_services import move_collection +from pdf.services.pdf_services import check_object_access_allowed from pdf.services.workspace_services import create_collection @@ -30,5 +34,66 @@ def obj_save(cls, form: CollectionForm, request: HttpRequest, _): create_collection(workspace=workspace, collection_name=form.data['name'], description=form.data['description']) +class CollectionMixin(BaseCollectionMixin): + @staticmethod + @check_object_access_allowed + def get_object(request: HttpRequest, collection_id: str) -> Collection: + """Get the current collection.""" + + user_profile = request.user.profile + collection = user_profile.collections.get(id=collection_id) + + return collection + + +class EditCollectionMixin(CollectionMixin): + fields_requiring_extra_processing = ['name'] + + @staticmethod + def get_edit_form_dict(): + """Get the forms of the fields that can be edited as a dict.""" + + form_dict = { + 'description': CollectionDescriptionForm, + 'name': CollectionNameForm, + } + + return form_dict + + def get_edit_form_get(self, field_name: str, collection: Collection): + """Get the form belonging to the specified field.""" + + form_dict = self.get_edit_form_dict() + + initial_dict = { + 'name': {'name': collection.name}, + 'description': {'description': collection.description}, + } + + form = form_dict[field_name](initial=initial_dict[field_name]) + + return form + + @classmethod + def process_field(cls, field_name: str, collection: Collection, request: HttpRequest, form_data: dict): + """Process fields that are not covered in the base edit view.""" + + if field_name == 'name': + profile = request.user.profile + existing_collection = profile.collections.filter(name__iexact=form_data.get('name').strip()).first() + + if existing_collection and str(existing_collection.id) != str(collection.id): + messages.warning(request, 'This name is already used by another collection in this workspace!') + else: + move_collection(collection) + + class Create(CreateCollectionMixin, base_views.BaseAdd): """View for creating new collections.""" + + +class Edit(EditCollectionMixin, base_views.BaseDetailsEdit): + """ + The view for editing a collection's name and description. The field, that is to be changed, is specified by the + 'field' argument. + """ diff --git a/pdfding/pdf/views/pdf_views.py b/pdfding/pdf/views/pdf_views.py index 83bd94bd..5019815f 100644 --- a/pdfding/pdf/views/pdf_views.py +++ b/pdfding/pdf/views/pdf_views.py @@ -222,6 +222,8 @@ def get_extra_context(request: HttpRequest) -> dict: 'special_pdf_selection': special_pdf_selection, 'tag_info_dict': TagServices.get_tag_info_dict(request.user.profile), 'tag_query': tag_query, + 'current_collection_id': request.user.profile.current_collection_id, + 'current_collection_name': request.user.profile.current_collection_name, } return extra_context diff --git a/pdfding/pdf/views/share_views.py b/pdfding/pdf/views/share_views.py index 9c8ca04f..3499f82e 100644 --- a/pdfding/pdf/views/share_views.py +++ b/pdfding/pdf/views/share_views.py @@ -133,10 +133,14 @@ def filter_objects(request: HttpRequest) -> QuerySet: return shared_pdfs @staticmethod - def get_extra_context(_) -> dict: # pragma: no cover + def get_extra_context(request: HttpRequest) -> dict: # pragma: no cover """get further information that needs to be passed to the template.""" - return {'page': 'shared_pdf_overview'} + return { + 'page': 'shared_pdf_overview', + 'current_collection_id': request.user.profile.current_collection_id, + 'current_collection_name': request.user.profile.current_collection_name, + } class SharedPdfMixin(BaseShareMixin): diff --git a/pdfding/pdf/views/workspace_views.py b/pdfding/pdf/views/workspace_views.py index f1d1b30f..b9fef682 100644 --- a/pdfding/pdf/views/workspace_views.py +++ b/pdfding/pdf/views/workspace_views.py @@ -2,7 +2,9 @@ from django.http import HttpRequest from django.shortcuts import redirect, render from pdf.forms import WorkspaceDescriptionForm, WorkspaceForm, WorkspaceNameForm +from pdf.models.collection_models import Collection from pdf.models.workspace_models import Workspace, WorkspaceError +from pdf.services.pdf_services import check_object_access_allowed from pdf.services.workspace_services import create_workspace @@ -39,6 +41,7 @@ def obj_save(cls, form: WorkspaceForm, request: HttpRequest, _): class WorkspaceMixin(BaseWorkspaceMixin): @staticmethod + @check_object_access_allowed def get_object(request: HttpRequest, ws_id: str) -> Workspace: """Get the current workspace.""" @@ -118,7 +121,7 @@ class Details(WorkspaceMixin, base_views.BaseDetails): """View for displaying the details page of a workspace.""" # returns the current workspace, needs to be adjusted in the future - def get(self, request: HttpRequest, identifier: str): # pragma: no cover + def get(self, request: HttpRequest, identifier: str | None = None): # pragma: no cover """Display the details page.""" obj = request.user.profile.current_workspace @@ -127,6 +130,31 @@ def get(self, request: HttpRequest, identifier: str): # pragma: no cover return render(request, 'workspace_details.html', context) +class CollectionDetails(WorkspaceMixin, base_views.BaseDetails): + """View for displaying the collections page of a workspace.""" + + def get(self, request: HttpRequest, identifier: str): + """Display the collection page.""" + + workspace = request.user.profile.current_workspace + collection = self.get_collection(request, identifier=identifier) + context = { + 'workspace': workspace, + 'current_collection_id': identifier, + 'collection': collection, + 'current_collection_name': collection.name, + } + + return render(request, 'collection_details.html', context) + + @staticmethod + @check_object_access_allowed + def get_collection(request: HttpRequest, identifier: str) -> Collection: + collection = request.user.profile.collections.get(id=identifier) + + return collection + + class Edit(EditWorkspaceMixin, base_views.BaseDetailsEdit): """ The view for editing a workspace's name and description. The field, that is to be changed, is specified by the diff --git a/pdfding/users/models.py b/pdfding/users/models.py index df5fb0fa..ae27d7f3 100644 --- a/pdfding/users/models.py +++ b/pdfding/users/models.py @@ -149,6 +149,17 @@ def current_collection(self): return self.collections.get(id=self.current_collection_id) + @property + def current_collection_name(self): + """Return the name of the current collection""" + + if self.current_collection_id == 'all': + current_collection_name = 'All' + else: + current_collection_name = self.current_collection.name + + return current_collection_name + @property def all_pdfs(self) -> QuerySet: """Return all PDFs of all workspaces the user has access to.""" diff --git a/pdfding/users/templates/account_settings.html b/pdfding/users/templates/account_settings.html index 907d25fb..c2788975 100644 --- a/pdfding/users/templates/account_settings.html +++ b/pdfding/users/templates/account_settings.html @@ -5,7 +5,7 @@
{% include 'includes/settings_sidebar.html' with page='account_settings' %}
-
+
diff --git a/pdfding/users/templates/danger_settings.html b/pdfding/users/templates/danger_settings.html index 301a10d2..6ecb9269 100644 --- a/pdfding/users/templates/danger_settings.html +++ b/pdfding/users/templates/danger_settings.html @@ -5,7 +5,7 @@
{% include 'includes/settings_sidebar.html' with page='danger_settings' %}
-
+
diff --git a/pdfding/users/templates/ui_settings.html b/pdfding/users/templates/ui_settings.html index aeb20f29..a93e1357 100644 --- a/pdfding/users/templates/ui_settings.html +++ b/pdfding/users/templates/ui_settings.html @@ -5,7 +5,7 @@
{% include 'includes/settings_sidebar.html' with page='ui_settings' %}
-
+
diff --git a/pdfding/users/templates/viewer_settings.html b/pdfding/users/templates/viewer_settings.html index dab35428..bb93b789 100644 --- a/pdfding/users/templates/viewer_settings.html +++ b/pdfding/users/templates/viewer_settings.html @@ -5,7 +5,7 @@
{% include 'includes/settings_sidebar.html' with page='viewer_settings' %}
-
+
diff --git a/pdfding/users/tests/test_models.py b/pdfding/users/tests/test_models.py index f98eddda..c3a226d6 100644 --- a/pdfding/users/tests/test_models.py +++ b/pdfding/users/tests/test_models.py @@ -189,14 +189,25 @@ def test_current_workspace_property(self): self.assertEqual(self.user.profile.current_workspace, other_workspace) def test_current_collection_property(self): - self.assertEqual(self.user.profile.current_collection.id, str(self.user.id)) - other_collection = create_collection(self.user.profile.current_workspace, 'other') self.user.profile.current_collection_id = other_collection.id self.user.profile.save() self.assertEqual(other_collection, self.user.profile.current_collection) + def test_current_collection_name_property(self): + other_collection = create_collection(self.user.profile.current_workspace, 'other') + self.user.profile.current_collection_id = other_collection.id + self.user.profile.save() + + self.assertEqual('other', self.user.profile.current_collection_name) + + def test_current_collection_name_all_property(self): + self.user.profile.current_collection_id = 'all' + self.user.profile.save() + + self.assertEqual('All', self.user.profile.current_collection_name) + def test_has_access_to_workspace(self): profile = self.user.profile other_workspace = create_workspace('other_workspace', self.user)