Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion pdfding/e2e/test_workspace_e2e.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.contrib.auth.models import User
from django.urls import reverse
from helpers import PdfDingE2ETestCase
from playwright.sync_api import sync_playwright
from playwright.sync_api import expect, sync_playwright


class TestWorkspaceE2ETestCase(PdfDingE2ETestCase):
Expand All @@ -19,3 +19,44 @@ def test_create_workspace(self):
self.assertEqual(created_ws.name, 'some_ws')
self.assertEqual(created_ws.description, '')
self.assertFalse(created_ws.personal_workspace)

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)

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.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_cancel_change_details(self):
ws = self.user.profile.current_workspace

with sync_playwright() as p:
self.open(reverse('workspace_details', kwargs={'identifier': ws.id}), p)

for edit_name in ['#name-edit', '#description-edit']:
expect(self.page.locator(edit_name)).to_contain_text("Edit")
self.page.locator(edit_name).click()
expect(self.page.locator(edit_name)).to_contain_text("Cancel")
self.page.locator(edit_name).click()
expect(self.page.locator(edit_name)).to_contain_text("Edit")
52 changes: 44 additions & 8 deletions pdfding/pdf/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.core.files import File
from pdf.models.pdf_models import Pdf
from pdf.models.shared_pdf_models import SharedPdf
from pdf.models.workspace_models import Workspace
from pdf.services.workspace_services import check_if_pdf_with_name_exists, get_shared_pdfs_of_workspace


Expand Down Expand Up @@ -455,24 +456,41 @@ def __init__(self, *args, **kwargs):

super(WorkspaceForm, self).__init__(*args, **kwargs)

def clean_name(self) -> str:
def clean_name(self) -> str: # pragma: no cover
"""
Clean the submitted workspace name. Removes trailing and multiple whitespaces. Checks that only
numbers, letters, '_' and '-' are used.
"""

ws_name = CleanHelpers.clean_name(self.cleaned_data['name'])
ws_name = CleanHelpers.clean_workspace_name(self.cleaned_data['name'])

return ws_name

if ws_name in ['_', '-']:
raise forms.ValidationError('"_" or "-" are not valid workspace names!')
elif ws_name and not re.match(r'^[A-Za-z0-9-_]*$', ws_name):
raise forms.ValidationError('Only "-", "_", numbers or letters are allowed!')
if len(ws_name) > 50:
raise forms.ValidationError('Maximum number of characters for a workspace name is 50!')

class WorkspaceNameForm(forms.ModelForm):
"""Form for changing the name of a workspace."""

class Meta:
model = Workspace
fields = ['name']

def clean_name(self) -> str: # pragma: no cover
"""Clean the submitted workspace name. Removes trailing and multiple whitespaces."""

ws_name = CleanHelpers.clean_workspace_name(self.cleaned_data['name'])

return ws_name


class WorkspaceDescriptionForm(forms.ModelForm):
"""Form for changing the description of a Workspace."""

class Meta:
model = Workspace
widgets = {'description': forms.Textarea(attrs={'rows': 3})}
fields = ['description']


class CleanHelpers:
@staticmethod
def clean_file(file: File) -> File:
Expand Down Expand Up @@ -565,3 +583,21 @@ def clean_tag_string_file_directory(input_string: str):
raise forms.ValidationError('Not allowed to contain consecutive "/" characters!')

return input_string

@staticmethod
def clean_workspace_name(ws_name: str) -> str:
"""
Clean the submitted workspace name. Removes trailing and multiple whitespaces. Checks that only
numbers, letters, '_' and '-' are used.
"""

ws_name = ws_name.strip()

if ws_name in ['_', '-']:
raise forms.ValidationError('"_" or "-" are not valid workspace names!')
elif ws_name and not re.match(r'^[A-Za-z0-9-_]*$', ws_name):
raise forms.ValidationError('Only "-", "_", numbers or letters are allowed!')
if len(ws_name) > 50:
raise forms.ValidationError('Maximum number of characters for a workspace name is 50!')

return ws_name
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.2.8 on 2025-12-20 14:21

import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pdf', '0024_remove_unneeded_null_true'),
]

operations = [
migrations.AddField(
model_name='collection',
name='creation_date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='workspace',
name='creation_date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]
1 change: 1 addition & 0 deletions pdfding/pdf/models/collection_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Collection(models.Model):
"""The model for the collections used for organizing PDF files."""

id = models.CharField(primary_key=True, default=get_uuid4_str, max_length=36, editable=False, blank=False)
creation_date = models.DateTimeField(blank=False, editable=False, auto_now_add=True)
description = models.TextField(default='', blank=True)
name = models.CharField(max_length=50, blank=False)
workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, blank=False)
Expand Down
1 change: 1 addition & 0 deletions pdfding/pdf/models/workspace_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Workspace(models.Model):
"""The workspace model. Workspaces are the top level hierarchy."""

id = models.CharField(primary_key=True, default=get_uuid4_str, max_length=36, editable=False, blank=False)
creation_date = models.DateTimeField(blank=False, editable=False, auto_now_add=True)
description = models.TextField(default='', blank=True)
name = models.CharField(max_length=50, blank=False)
personal_workspace = models.BooleanField(blank=False, editable=False)
Expand Down
21 changes: 9 additions & 12 deletions pdfding/pdf/services/workspace_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,15 @@ def create_personal_workspace(creator: User) -> Workspace:
def create_workspace(name: str, creator: User, description: str = '') -> Workspace:
"""Create a non personal workspace for a user including the workspace user and the default collection"""

if creator.profile.workspaces.filter(name=name).count():
raise WorkspaceError(f'There is already a workspace named {name}!')
else:
workspace = Workspace.objects.create(name=name, personal_workspace=False, description=description)
WorkspaceUser.objects.create(workspace=workspace, user=creator, role=WorkspaceRoles.OWNER)
Collection.objects.create(
id=workspace.id,
name='Default',
workspace=workspace,
default_collection=True,
description='Default Collection',
)
workspace = Workspace.objects.create(name=name, personal_workspace=False, description=description)
WorkspaceUser.objects.create(workspace=workspace, user=creator, role=WorkspaceRoles.OWNER)
Collection.objects.create(
id=workspace.id,
name='Default',
workspace=workspace,
default_collection=True,
description='Default Collection',
)

return workspace

Expand Down
36 changes: 36 additions & 0 deletions pdfding/pdf/templates/includes/workspace_sidebar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% extends 'layouts/base_sidebar.html' %}
{% block sidebar_content %}
<div class="flex flex-row w-full justify-between! md:flex-col gap-y-1
[&>*]:font-semibold [&>div]:px-3 md:[&>div]:px-2 [&>div]:py-2 md:[&>div]:py-1 [&>div]:rounded-md
[&>div>a]:flex [&>div>a]:flex-row [&>div>a]:items-center [&>div>a]:gap-x-2
[&>div]:hover:bg-slate-200 dark:[&>div]:hover:bg-slate-700 creme:[&>div]:hover:bg-creme-dark">
<div>
<a href="{% url 'pdf_overview' %}">
<svg class="w-5 h-5" fill="currentColor" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400.004 400.004" xml:space="preserve">
<!-- source: https://www.svgrepo.com/svg/97894/left-arrow -->
<!-- license: CC0 License-->
<path d="M382.688,182.686H59.116l77.209-77.214c6.764-6.76,6.764-17.726,0-24.485c-6.764-6.764-17.73-6.764-24.484,0L5.073,187.757
c-6.764,6.76-6.764,17.727,0,24.485l106.768,106.775c3.381,3.383,7.812,5.072,12.242,5.072c4.43,0,8.861-1.689,12.242-5.072
c6.764-6.76,6.764-17.726,0-24.484l-77.209-77.218h323.572c9.562,0,17.316-7.753,17.316-17.315
C400.004,190.438,392.251,182.686,382.688,182.686z"/>
</svg>
<span class="hidden md:block">Back to App</span>
</a>
</div>
<div {% if page == 'workspace_details' %} class="bg-slate-200 md:bg-slate-100 dark:bg-slate-800 creme:bg-creme-dark-light" {% endif %}>
<a href="{% url 'workspace_details' workspace.id %}">
<svg fill="currentColor" class="w-5 h-5" version="1.1" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 330 330" xml:space="preserve">
<!-- source: https://www.svgrepo.com/svg/65094/information -->
<!-- license: CC0 License-->
<g>
<path d="M165,0C74.019,0,0,74.02,0,165.001C0,255.982,74.019,330,165,330s165-74.018,165-164.999C330,74.02,255.981,0,165,0z M165,300c-74.44,0-135-60.56-135-134.999C30,90.562,90.56,30,165,30s135,60.562,135,135.001C300,239.44,239.439,300,165,300z"/>
<path d="M164.998,70c-11.026,0-19.996,8.976-19.996,20.009c0,11.023,8.97,19.991,19.996,19.991 c11.026,0,19.996-8.968,19.996-19.991C184.994,78.976,176.024,70,164.998,70z"/>
<path d="M165,140c-8.284,0-15,6.716-15,15v90c0,8.284,6.716,15,15,15c8.284,0,15-6.716,15-15v-90C180,146.716,173.284,140,165,140z"/>
</g>
</svg>
<span class="hidden md:block">Details</span>
</a>
</div>
</div>
{% endblock %}
2 changes: 1 addition & 1 deletion pdfding/pdf/templates/partials/workspace_dropdown.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
</svg>
<span class="">Create Workspace</span>
</a>
<a class="ws-modal-child items-center gap-x-2">
<a href="{% url 'workspace_details' request.user.profile.current_workspace_id %}" class="ws-modal-child items-center gap-x-2">
<svg class="-ml-1! w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<!-- source: https://www.svgrepo.com/svg/304474/settings -->
<!-- license: PD License-->
Expand Down
82 changes: 82 additions & 0 deletions pdfding/pdf/templates/workspace_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{% extends 'layouts/blank.html' %}

{% block content %}
<div class="flex flex-col md:flex-row md:justify-end">
<div class="w-full! md:w-72! lg:w-72! px-4 pt-2">
{% include 'includes/workspace_sidebar.html' with page='workspace_details' %}
</div>
<div class="flex w-full justify-start items-center py-2 px-3 md:px-8">
<div class="rounded-md w-full min-[1200px]:w-3xl! md:ml-10 min-[1600px]:ml-40! px-4 py-4 md:pb-4 border
bg-slate-100 dark:bg-slate-800 creme:bg-creme-dark-light
border-slate-300 dark:border-slate-700 creme:border-creme-dark">
<div>
<div class="text-2xl font-bold">
Workspace Details
</div>
<div class="pt-4">
<span class="text-lg font-bold">Name</span>
</div>
<div class="flex justify-between text-slate-600 dark:text-slate-400 creme:text-stone-500">
<div class="w-[86%] truncate">
<span id="name">
{{ workspace.name }}
</span>
</div>
<div class="pr-0 md:pr-4">
<a id="name-edit" class="cursor-pointer text-primary hover:text-secondary"
hx-get="{% url 'edit_workspace' identifier=workspace.id field_name='name' %}"
hx-target="#name"
hx-swap="innerHTML">
Edit
</a>
</div>
</div>
<div class="pt-2">
<span class="text-lg font-bold">Personal Workspace</span>
</div>
<div class="flex justify-between text-slate-600 dark:text-slate-400 creme:text-stone-500">
<div class="w-[86%]">
<span id="personal_workspace">
{% if workspace.personal_workspace %}
Yes
{% else %}
No
{% endif%}
</span>
</div>
</div>
<div class="pt-2">
<span class="text-lg font-bold">Description</span>
</div>
<div class="flex justify-between text-slate-600 dark:text-slate-400 creme:text-stone-500">
<div class="w-[86%] text-sm">
<span id="description">
{% if workspace.description %}
{{ workspace.description }}
{% else %}
no description available
{% endif%}
</span>
</div>
<div class="pr-0 md:pr-4">
<a id="description-edit" class="cursor-pointer text-primary hover:text-secondary"
hx-get="{% url 'edit_workspace' identifier=workspace.id field_name='description' %}"
hx-target="#description"
hx-swap="innerHTML">
Edit
</a>
</div>
</div>
<div class="pt-2">
<span class="text-lg font-bold">Date added</span>
</div>
<div class="flex justify-between text-slate-600 dark:text-slate-400 creme:text-stone-500">
<div class="w-[86%] text-sm">
<span id="creation_date">{{ workspace.creation_date }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Loading