Skip to content
4 changes: 2 additions & 2 deletions canvas_editor/editor/views/editor_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.utils import timezone
from django.views.generic import TemplateView

from project_management.forms.project_form import ProjectForm
from project_management.forms.create_project_form import CreateProjectForm
from project_management.models import Project


Expand All @@ -22,7 +22,7 @@ def get_context_data(self, **kwargs):
project.last_edited = timezone.now()
project.save()

create_new_project_form = ProjectForm()
create_new_project_form = CreateProjectForm(user=self.request.user)
all_projects = Project.objects.filter(owner=request.user).order_by("-last_edited")

context.update(
Expand Down
90 changes: 90 additions & 0 deletions canvas_editor/project_management/forms/create_project_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from re import sub

from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.utils import timezone

from canvas import message_dict
from hdf5_management.hdf5_manager import HDF5Manager
from project_management.forms.utils import validate_symbols
from project_management.models import Project
from project_management.views.utils import is_name_unique


class CreateProjectForm(forms.ModelForm):
"""Form to create or edit a project."""

file = forms.FileField(
required=False,
widget=forms.ClearableFileInput(attrs={"class": "form-control", "accept": ".h5"}),
)

class Meta:
"""Meta information for this model form."""

model = Project
fields = ["name", "description"]

def clean_name(self):
"""Validate the given project name.

Because white space and special characters break the CSS selectors, special characters are prohibited and all white space is replaced with _.
"""
# Replace any white space with '_'
project_name = sub(r"\s", "_", str(self.cleaned_data.get("name")).strip())

if not is_name_unique(self.user, project_name):
raise ValidationError(message_dict.project_name_must_be_unique)

return validate_symbols(project_name)

def clean_file(self):
"""Validate the uploaded file."""
# Check if a file is uploaded
file = self.cleaned_data.get("file")
if not file:
return file

# Check file extension (only allow .h5 files)
if not file.name.endswith(".h5"):
raise ValidationError("Only HDF5 (.h5) files are allowed.")

# Check file size
max_size = 1024 * 1024 * 1024 # 1GB
if file.size > max_size:
raise ValidationError("File size should not exceed 1GB.")

return file

def save(self, commit=True):
"""Expand the default save method.

Adds missing owner and last edited field values.
Imports the data if a file is given.
"""
new_project = super().save(commit=False)
new_project.last_edited = timezone.now()
new_project.owner = self.user

if commit:
new_project.save()

file = self.cleaned_data.get("file")
if file is not None:
hdf5_manager = HDF5Manager()
hdf5_manager.create_project_from_hdf5_file(file, new_project)

return new_project

def __init__(
self,
user: User,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self.user = user
self.fields["name"].widget.attrs.update({"class": "form-control"})
self.fields["description"].widget.attrs.update({"class": "form-control"})
self.fields["file"].widget.attrs.update({"class": "form-control"})
23 changes: 16 additions & 7 deletions canvas_editor/project_management/forms/update_project_form.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django import forms
from django.forms import ModelForm
from re import sub

from django.forms import ModelForm, ValidationError

from canvas import message_dict
from project_management.forms.utils import validate_symbols
from project_management.models import Project
from project_management.views.utils import is_name_unique


class UpdateProjectForm(ModelForm):
Expand All @@ -14,12 +17,18 @@ class Meta:
model = Project
fields = ["name", "description"]

name = forms.CharField(
max_length=100,
validators=[validate_symbols],
)
def clean_name(self):
"""Validate the given project name.

Because white space and special characters break the CSS selectors, special characters are prohibited and all white space is replaced with _.
"""
# Replace any white space with '_'
project_name = sub(r"\s", "_", str(self.cleaned_data.get("name")).strip())

if (project_name != self.instance.name) and not is_name_unique(self.instance.owner, project_name):
raise ValidationError(message_dict.project_name_must_be_unique)

description = forms.CharField(max_length=500, required=False, widget=forms.TextInput())
return validate_symbols(project_name)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="modal fade"
id="updateModal-{{ project.name }}"
tabindex="-1"
aria-labelledby="updateModal"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteModalHeader">Edit Project</h1>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="font-italic">
Projects will only be
updated if they have a
unique name!
</p>
<form action="{% url 'updateProject' project_name=project.name %}"
method="POST"
class="d-flex flex-column gap-2">
{% csrf_token %}
{{ project.update_form }}
<button class="btn btn-primary rounded-3" type="submit">Save</button>
</form>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,17 @@
<div class="d-flex">
<!-- favorites-->
<div class="position-absolute end-0 top-0">
{% if project.favorite %}
<div class="btn btn-primary custom-btn shadow favoriteButton"
style="border-radius: 0%"
data-is-favorite="{{ project.favorite }}"
data-project-name="{{ project.name }}"
title="Mark/Unmark as favorite">
<div class="btn btn-primary custom-btn shadow favoriteButton"
style="border-radius: 0%"
data-is-favorite="{{ project.favorite }}"
data-project-name="{{ project.name }}"
title="Mark/Unmark as favorite">
{% if project.favorite %}
<i class="bi bi-star-fill text-warning"></i>
</div>
{% else %}
<div class="btn btn-primary custom-btn shadow favoriteButton"
style="border-radius: 0%"
data-is-favorite="{{ project.favorite }}"
data-project-name="{{ project.name }}"
title="Mark/Unmark as favorite">
{% else %}
<i class="bi bi-star"></i>
</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="col-md-8">
<div class="card-body">
Expand All @@ -60,16 +54,16 @@ <h1 class="fw-bolder">{{ project.name | truncate_with_end:100 }}</h1>
<a role="button"
href="{% url 'editor' project_name=project.name %}"
class="btn btn-primary">Open</a>
<!-- Edit Button -->
<!-- Update Button -->
<button type="button"
class="btn btn-primary custom-btn shadow"
data-bs-toggle="modal"
data-bs-target="#updateModal-{{ project.name }}"
title="Edit project name or description">
<i class="bi bi-pencil-square"></i>
</button>
<!--create project modal-->
{% include "project_management/modals/editProjectModal.html" %}
<!--update project modal-->
{% include "project_management/modals/updateProjectModal.html" %}
<!-- Duplicate Button -->
<form action="{% url 'duplicateProject' project_name=project.name %}"
method="post">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from canvas.test_constants import (
COPY_SUFFIX,
DESCRIPTION_FIELD,
EMPTY_FIELD,
FILE_FIELD,
NAME_FIELD,
OWNER_FIELD,
Expand Down Expand Up @@ -220,21 +219,6 @@ def test_update_project_post_name_description_changed(self):
self.assertEqual(Project.objects.first().description, UPDATED_DESCRIPTION)
self.assertEqual(Project.objects.count(), 1)

def test_update_project_post_name_description_changed_description_is_empty(self):
"""Test updating a project via POST request with changed name and empty description."""
response = self.client.post(
self.update_project_url,
{
NAME_FIELD: UPDATED_PROJECT_NAME,
OWNER_FIELD: self.user.id,
},
)

self.assertEqual(response.status_code, 302)
self.assertEqual(Project.objects.first().name, UPDATED_PROJECT_NAME)
self.assertEqual(Project.objects.first().description, EMPTY_FIELD)
self.assertEqual(Project.objects.count(), 1)

def test_update_project_post_name_not_changed(self):
"""Test updating a project via POST request with unchanged name."""
response = self.client.post(
Expand Down
45 changes: 10 additions & 35 deletions canvas_editor/project_management/views/projects_view.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.shortcuts import redirect, render
from django.utils import timezone
from django.utils.http import urlsafe_base64_encode
from django.views.generic import ListView

from canvas import message_dict
from hdf5_management.hdf5_manager import HDF5Manager
from project_management.forms.project_form import ProjectForm
from project_management.forms.create_project_form import CreateProjectForm
from project_management.forms.update_project_form import UpdateProjectForm
from project_management.models import Project
from project_management.views.utils import is_name_unique


class ProjectsView(LoginRequiredMixin, ListView):
Expand All @@ -22,25 +17,14 @@ class ProjectsView(LoginRequiredMixin, ListView):

@staticmethod
def _generate_uid(request):
"""Generate an url safe encoding of the user id."""
return urlsafe_base64_encode(str(request.user.id).encode())

@staticmethod
def _generate_token(project_name):
"""Generate an url safe encoding of the project name."""
return urlsafe_base64_encode(str(project_name).encode())

@staticmethod
def _create_project(user: User, project_name: str, project_description: str, project_file):
new_project = Project(
name=project_name,
description=project_description,
owner=user,
last_edited=timezone.now(),
)
new_project.save()

if project_file is not None:
HDF5Manager.create_project_from_hdf5_file(project_file, new_project)

def get_queryset(self):
"""Get a list of all projects of this user.

Expand All @@ -50,35 +34,26 @@ def get_queryset(self):
for project in queryset:
project.uid = self._generate_uid(self.request)
project.token = self._generate_token(project.name)
project.update_form = UpdateProjectForm(instance=project)
return queryset

def get_context_data(self, **kwargs):
"""Add the ProjectForm to the context."""
context = super().get_context_data(**kwargs)
context["create_new_project_form"] = ProjectForm()
context["create_new_project_form"] = CreateProjectForm(user=self.request.user)
return context

def post(self, request):
"""Create a new project if the form is valid and the name is unique."""
# Initialize the form with POST and FILE data
form = ProjectForm(request.POST, request.FILES)
form = CreateProjectForm(request.user, request.POST, request.FILES)

# Check if form is valid before proceeding
if form.is_valid() and is_name_unique(request.user, form.cleaned_data["name"].strip().replace(" ", "_")):
project_name = form.cleaned_data["name"].strip().replace(" ", "_")
project_file = request.FILES.get("file")
project_description = form.cleaned_data.get("description", "").strip()

self._create_project(request.user, project_name, project_description, project_file)

return redirect("editor", project_name=project_name)
if form.is_valid():
project = form.save()
return redirect("editor", project_name=project.name)

else:
messages.error(request, message_dict.project_name_must_be_unique)
for field in form:
for error in field.errors:
messages.error(request, f"Error in {field.label}: {error}")

context = self.get_context_data(object_list=self.get_queryset())
context["create_new_project_form"] = form
return render(request, "project_management/projects.html", context)
Loading