Skip to content

Commit

Permalink
Finish update task view with sub task update prototype (#611)
Browse files Browse the repository at this point in the history
This prototype adds the finished prototype for the task update
interface. The task update prototype lets you update sub tasks as well.
You can add new sub tasks as well, or delete unneeded sub tasks.
  • Loading branch information
justuswilhelm authored Jan 20, 2025
2 parents 03b77a7 + f592e90 commit 9264405
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 43 deletions.
43 changes: 31 additions & 12 deletions backend/projectify/workspace/templates/workspace/task_update.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
method="post"
class="min-w-0 grow"
role="presentation">
{{ form.errors }}
<div class="flex h-full flex-col p-4 pt-0">
<div class="sticky top-0 flex flex-row flex-wrap items-center justify-between gap-x-2 gap-y-4 bg-foreground pb-8 pt-4 lg:flex-nowrap">
<div class="flex shrink flex-row items-center gap-6">
Expand Down Expand Up @@ -91,18 +92,10 @@
<div class="flex flex-row items-center gap-4">
<h4 class="text-xl font-bold">Sub tasks</h4>
</div>
<input type="hidden"
name="form-TOTAL_FORMS"
value="{{ formset_total }}"
id="total-subtasks">
<div id="add-subtask" class="flex flex-row items-center gap-6">
<button type="button"
hx-select-oob="#total-subtasks,#add-subtask"
hx-trigger="click"
hx-target="#subtasks"
hx-select="[data-formset]"
hx-swap="beforeend"
hx-swap="outerHTML"
<button type="submit"
name="add_sub_task"
value="add_sub_task"
class="w-full text-tertiary-content hover:text-tertiary-content-hover active:bg-tertiary-pressed active:text-tertiary-content-hover text-base flex min-w-max flex-row justify-center gap-2 rounded-lg px-4 py-2 font-bold disabled:bg-transparent disabled:text-disabled-content">
<svg fill="none"
viewBox="0 0 24 24"
Expand All @@ -118,7 +111,33 @@ <h4 class="text-xl font-bold">Sub tasks</h4>
</button>
</div>
</div>
<div id="subtasks">{% include "workspace/task_update/sub_task_update.html" with formset=formset %}</div>
<div id="subtasks">
{{ formset.errors }}
{% for form in formset %}
<div data-formset
class="flex w-full flex-row items-center justify-between gap-2 px-2 py-1">
<div class="flex grow flex-row items-center gap-2">
<label for="{{ form.done.id_for_label }}" class="sr-only">{{ form.done.label }}</label>
{{ form.done }}
<div class="grow">
<label for="{{ form.title.id_for_label }}" class="sr-only">{{ form.title.label }}</label>
<div class="flex flex-col items-start gap-2">
<div class="flex w-full flex-row items-center gap-2">{{ form.title }}</div>
</div>
</div>
{% if form.uuid.value %}
<input type="hidden"
name="{{ form.uuid.html_name }}"
value="{{ form.uuid.value }}">
{% endif %}
<div>
{{ form.delete }}
<label for="{{ form.delete.id_for_label }}">{{ form.delete.label }}</label>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{{ formset.management_form }}
{% csrf_token %}
Expand Down

This file was deleted.

66 changes: 59 additions & 7 deletions backend/projectify/workspace/views/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
# SPDX-FileCopyrightText: 2023-2024 JWP Consulting GK
"""Task CRUD views."""

from typing import Any, Literal, Union
import logging
from typing import Any, Literal, Optional, Union
from uuid import UUID

from django import forms
from django.forms.formsets import TOTAL_FORM_COUNT
from django.http import HttpResponse
from django.http.response import Http404
from django.shortcuts import redirect, render
Expand Down Expand Up @@ -54,6 +56,8 @@
task_update_nested,
)

logger = logging.getLogger(__name__)


def get_object(
request: Union[Request, AuthenticatedHttpRequest], task_uuid: UUID
Expand Down Expand Up @@ -114,7 +118,7 @@ class TaskUpdateSubTaskForm(forms.Form):

title = forms.CharField()
done = forms.BooleanField(required=False)
uuid = forms.UUIDField(widget=forms.HiddenInput)
uuid = forms.UUIDField(required=False, widget=forms.HiddenInput)
delete = forms.BooleanField(
required=False,
label=_("Delete task"),
Expand Down Expand Up @@ -227,6 +231,7 @@ class TaskUpdateForm(forms.Form):
)
submit = forms.CharField(required=False)
submit_stay = forms.CharField(required=False)
add_sub_task = forms.CharField(required=False)

def __init__(self, *args: Any, workspace: Workspace, **kwargs: Any):
"""Populate available assignees."""
Expand All @@ -237,6 +242,21 @@ def __init__(self, *args: Any, workspace: Workspace, **kwargs: Any):
self.fields["labels"].queryset = workspace.label_set.all()


def determine_action(
request: AuthenticatedHttpRequest,
) -> Optional[Literal["get", "submit", "submit_stay", "add_sub_task"]]:
"""Determine what update view action should be taken."""
if request.method == "GET":
return "get"
if "submit" in request.POST:
return "submit"
elif "submit_stay" in request.POST:
return "submit_stay"
elif "add_sub_task" in request.POST:
return "add_sub_task"
return None


@platform_view
@require_http_methods(["GET", "POST"])
def task_update_view(
Expand All @@ -250,6 +270,26 @@ def task_update_view(
raise Http404(_(f"Could not find task with uuid {task_uuid}"))
project = task.section.project
workspace = project.workspace

action = determine_action(request)

if action == "add_sub_task":
post = request.POST.copy()
sub_task_count_raw: str = post.get("form-" + TOTAL_FORM_COUNT, "0")
try:
sub_task_count = int(sub_task_count_raw)
except ValueError as e:
logger.error(
"Unexpected error when getting total form count", exc_info=e
)
sub_task_count = 0
post["form-TOTAL_FORMS"] = str(sub_task_count + 1)
logger.info("Adding sub task")
form = TaskUpdateForm(data=post, workspace=workspace)
formset = TaskUpdateSubTaskForms(data=post)
context = {"form": form, "task": task, "formset": formset}
return render(request, "workspace/task_update.html", context)

task_initial = {
"title": task.title,
"assignee": task.assignee,
Expand All @@ -262,7 +302,7 @@ def task_update_view(
{"title": sub_task.title, "done": sub_task.done, "uuid": sub_task.uuid}
for sub_task in sub_tasks
]
if request.method == "GET":
if action == "get":
form = TaskUpdateForm(initial=task_initial, workspace=workspace)
formset = TaskUpdateSubTaskForms(initial=sub_tasks_initial)
context = {"form": form, "task": task, "formset": formset}
Expand All @@ -282,15 +322,25 @@ def task_update_view(
return render(request, "workspace/task_update.html", context)

cleaned_data = form.cleaned_data
formset_cleaned_data = formset.cleaned_data
update_sub_tasks: list[ValidatedDatumWithUuid] = [
{
"title": f["title"],
"done": f["done"],
"_order": i,
"uuid": f["uuid"],
}
for i, f in enumerate(formset.cleaned_data)
if not f["delete"]
for i, f in enumerate(formset_cleaned_data)
if f and f["uuid"] and not f["delete"]
]
create_sub_tasks: list[ValidatedDatum] = [
{
"title": f["title"],
"done": f["done"],
"_order": i,
}
for i, f in enumerate(formset_cleaned_data)
if f and not f["uuid"] and not f["delete"]
]
task_update_nested(
who=request.user,
Expand All @@ -302,11 +352,13 @@ def task_update_view(
labels=[],
sub_tasks={
"update_sub_tasks": update_sub_tasks,
"create_sub_tasks": [],
"create_sub_tasks": create_sub_tasks,
},
)
if cleaned_data["submit_stay"]:
if action == "submit_stay":
n = reverse("dashboard:tasks:detail", args=(task.uuid,))
elif action == "submit":
n = reverse("dashboard:projects:detail", args=(project.uuid,))
else:
n = reverse("dashboard:projects:detail", args=(project.uuid,))
return redirect(n)
Expand Down
3 changes: 3 additions & 0 deletions backend/projectify/workspace/views/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from projectify.lib.error_schema import DeriveSchema
from projectify.lib.schema import extend_schema
from projectify.lib.types import AuthenticatedHttpRequest
from projectify.lib.views import platform_view
from projectify.workspace.selectors.project import (
project_find_by_workspace_uuid,
)
Expand All @@ -46,13 +47,15 @@


# HTML
@platform_view
def workspace_list_view(request: AuthenticatedHttpRequest) -> HttpResponse:
"""Show all workspaces."""
workspaces = workspace_find_for_user(who=request.user)
context = {"workspaces": workspaces}
return render(request, "workspace/workspace_list.html", context)


@platform_view
def workspace_view(
request: AuthenticatedHttpRequest, workspace_uuid: UUID
) -> HttpResponse:
Expand Down
57 changes: 57 additions & 0 deletions docs/remove-fe-worklog.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,60 @@ and make the template for the form a bit more maintainable. This is the way.
## 2025-01-11

I've now added a checkbox that lets you delete an existing sub task

# 2025-01-12

I realize that modifying forms can be done server-side as well. Here's the
use case: While adding or updating a task, I'd like to add a sub task.
Perhaps I've already entered a few sub tasks when editing an existing task.
Or I am adding a new task and I want to add my first sub task row.

The solution so far was the following: Since Projectify had a JavaScript
frontend, I could just manage the amount of sub tasks I want to add in the
frontend state. With Svelte, you'd use stores for this. The same applies to
ordering both new or existing sub tasks.

If I want to do this without a JavasScript frontend, I need to find other means
of managing the state of a task without having to store an intermediate
state of a task somewhere in the backend or database.

The solution is to submit a draft form to the server and let it know in the
POST request that this is a draft, not a final *save* request. A form can
have several *submit* buttons, not only one. If each submit button has a
different `name=` attribute, you can let the backend know which button was
pressed in the browser.

Let's say one of these buttons has the name `add-new-sub-task`. The POST
request will then let the backend know that this button has been pressed. The
backend then knows that this isn't a request to update a task based on the form
data. Instead, it will render a new form using all the task draft information,
with a new sub tasks row added at the end.

# 2025-01-15

Yesterday, I found a solution to make adding new sub task fields work. The
idea is to increase the total formset count by manually updating the POST
data. This is the relevant code:

```python
def view(request: HttpRequest) -> HttpResponse:
post = request.POST.copy()
sub_task_count_raw: str = post.get("form-" + TOTAL_FORM_COUNT, "0")
try:
sub_task_count = int(sub_task_count_raw)
except ValueError as e:
logger.error(
"Unexpected error when getting total form count", exc_info=e
)
sub_task_count = 0
post["form-TOTAL_FORMS"] = str(sub_task_count + 1)
logger.info("Adding sub task")
form = TaskUpdateForm(data=post, workspace=workspace)
formset = TaskUpdateSubTaskForms(data=post)
context = {"form": form, "task": task, "formset": formset}
return render(request, "workspace/task_update.html", context)
```

Furthermore, I've decided to not implement sub task reordering. I question
the utility of sub task reordering in general, so I want to hold off on that
feature for now.

0 comments on commit 9264405

Please sign in to comment.