Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
a8a1ba9
BE: Update pyright
justuswilhelm Jan 26, 2026
0515a79
BE: Use inspect.iscoroutinefunction
justuswilhelm Jan 26, 2026
726221d
BE: Ignore .venv in pyright
justuswilhelm Jan 26, 2026
d1fae19
BE: Ignore .venv in vulture
justuswilhelm Jan 26, 2026
ad0091e
BE: Add submit widget template
justuswilhelm Dec 15, 2025
971ce6d
BE: Add missing projects ctx var
justuswilhelm Dec 15, 2025
ba62e4a
BE: Add link to create project page
justuswilhelm Dec 15, 2025
1b2491e
BE: Show active project in task detail
justuswilhelm Dec 15, 2025
88f4e44
BE: Finish create section form
justuswilhelm Dec 15, 2025
3833bba
Docs: Update DONE status
justuswilhelm Dec 15, 2025
482302c
BE: Improve project update page
justuswilhelm Dec 15, 2025
b640db2
BE: Finish update section page
justuswilhelm Dec 15, 2025
a72a334
Docs: Update implementation status
justuswilhelm Dec 15, 2025
a892c57
BE: Add features to task actions
justuswilhelm Dec 15, 2025
355cc78
BE: Improve task creation
justuswilhelm Dec 15, 2025
182bce6
BE: Improve task update
justuswilhelm Dec 15, 2025
ac1b1db
BE: Improve task actions
justuswilhelm Dec 15, 2025
5213e0d
BE: Improve task actions
justuswilhelm Dec 15, 2025
5d44284
BE: Implement basic team member task filter
justuswilhelm Dec 22, 2025
7269469
BE: Annotate users based on filter
justuswilhelm Dec 24, 2025
e238e52
BE: Optimize project detail query
justuswilhelm Dec 24, 2025
377860a
BE: Optimize query counts one more time
justuswilhelm Dec 24, 2025
c5ea275
BE: Support filtering by unassigned tasks
justuswilhelm Dec 24, 2025
485bd48
BE: Add mock label filter
justuswilhelm Dec 24, 2025
83fb18e
BE: Implement basic label filtering
justuswilhelm Dec 24, 2025
a2cf0db
BE: Fix label/member filter combination
justuswilhelm Dec 24, 2025
9a13434
BE: Wrap up filtering
justuswilhelm Dec 24, 2025
7d69871
BE: Bake filter team members into sidebar
justuswilhelm Dec 25, 2025
181a0b7
BE: Bake filter labels into proj. details template
justuswilhelm Dec 25, 2025
5cd8f48
BE: Introduce form for project filtering
justuswilhelm Dec 25, 2025
818eee6
BE: Fix filter by team member
justuswilhelm Jan 19, 2026
a26f8b8
BE: Fix form name in include
justuswilhelm Jan 20, 2026
ff6db42
BE: Annotate task counts for label/member
justuswilhelm Jan 20, 2026
f8ae033
BE: Update side bar stuff
justuswilhelm Jan 20, 2026
a1f7113
BE: Refine logic in project selector
justuswilhelm Jan 21, 2026
077a39f
BE: Add label edit pages
justuswilhelm Jan 21, 2026
8af05a8
BE: Add back link anchor in label edit
justuswilhelm Jan 21, 2026
c397895
BE: Add label delete
justuswilhelm Jan 26, 2026
c5c0245
BE: Add macOS postgresql db url example
justuswilhelm Jan 26, 2026
b37deeb
BE: Add label creation screen
justuswilhelm Jan 26, 2026
e5a9a66
BE: Validate label numbers
justuswilhelm Jan 26, 2026
37ed953
BE: Annotate labels with colors in settings
justuswilhelm Jan 26, 2026
19b5a23
BE: Add proper label radio buttons
justuswilhelm Jan 26, 2026
e09131f
BE: Show labels in task dashboard
justuswilhelm Jan 26, 2026
e365ddf
BE: Improve label appearance
justuswilhelm Jan 26, 2026
c369524
BE: Introduce action_button templatetag
justuswilhelm Jan 27, 2026
0b1bbca
BE: Use action_button in ws label list
justuswilhelm Jan 27, 2026
de3d5b8
BE: Use new buttons in ws project settings
justuswilhelm Jan 27, 2026
7b8a74c
BE: Use new buttons in section update
justuswilhelm Jan 27, 2026
e84629b
BE: Refactor settings tabs
justuswilhelm Jan 27, 2026
706b55d
BE: Remove unused argument
justuswilhelm Jan 27, 2026
748f842
BE: Refactor workspace context
justuswilhelm Jan 27, 2026
63a006f
BE: Refactor workspace settings context
justuswilhelm Jan 27, 2026
a32c16c
BE: Refactor buttons in team member setttings
justuswilhelm Jan 27, 2026
712ff00
BE: Refactor button usage in billing settings
justuswilhelm Jan 27, 2026
9196c9e
BE: Refactor submit button in general ws settings
justuswilhelm Jan 27, 2026
f0dd325
BE: Clean up task SVGs
justuswilhelm Jan 27, 2026
bc6fe32
BE: Mark task detail strings for translations
justuswilhelm Jan 27, 2026
17382fd
BE: Use user avatar in task details
justuswilhelm Jan 27, 2026
fcafa56
BE: Apply correct label colors in task detail
justuswilhelm Jan 27, 2026
1b60853
BE: Implement task update field autofocus
justuswilhelm Jan 27, 2026
7fcb7ef
BE: Add missing SPDX header
justuswilhelm Jan 27, 2026
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
6 changes: 5 additions & 1 deletion backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
# SPDX-FileCopyrightText: 2024 JWP Consulting GK
DJANGO_SETTINGS_MODULE = projectify.settings.development
DJANGO_CONFIGURATION = Development
# The following assumes that you've chosen projectify as the database name
# and use PostgreSQL
DATABASE_URL=postgres://$USER@localhost/projectify
# If unix socket, choose something like this:
# DATABASE_URL = postgres://%2Fvar%2Flib%2Fpostgresql/dbname
# DATABASE_URL = postgres://%2Fvar%2Flib%2Fpostgresql/projectify
# On macOS, the PostgreSQL domain socket is here:
# DATABASE_URL = postgres://%2tmp/projectify
# If you need to use Redis for your celery worker, add the following:
# REDIS_TLS_URL = redis://localhost:6379/0
8 changes: 4 additions & 4 deletions backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions backend/projectify/lib/htmx.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@
# ruff: noqa: D101, D102, D105, D107
import json
from collections.abc import Awaitable
from inspect import iscoroutinefunction, markcoroutinefunction
from typing import Any, Callable
from urllib.parse import unquote, urlsplit, urlunsplit

from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseBase, HttpResponseRedirectBase
from django.utils.functional import cached_property

from asgiref.sync import iscoroutinefunction, markcoroutinefunction


class HtmxMiddleware:
sync_capable = True
Expand Down
4 changes: 3 additions & 1 deletion backend/projectify/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
{% if debug %}
{% include "common/navigation/header/development_view_bar.html" %}
{% endif %}
<div class="flex min-h-screen flex-col" id="svelte" role="presentation">
<div class="flex min-h-screen flex-col"
id="app-content"
role="presentation">
{% block content %}{% endblock %}
</div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
name="{{ widget.name }}"
{% if widget.value != None %}value="{{ widget.value|stringformat:'s' }}"{% endif %}
{% include "django/forms/widgets/attrs.html" %}>
<span class="hidden absolute peer-checked:block">{% include "heroicons/check.svg" %}</span>
<span class="hidden absolute pointer-events-none peer-checked:block">{% include "heroicons/check.svg" %}</span>
</div>
7 changes: 7 additions & 0 deletions backend/projectify/templates/projectify/forms/submit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{# SPDX-FileCopyrightText: 2025 JWP Consulting GK #}
<button type="submit"
class="w-full bg-primary text-primary-content hover:bg-primary-hover active:bg-primary-pressed text-base flex min-w-max flex-row justify-center gap-2 rounded-lg px-4 py-2 font-bold disabled:bg-disabled disabled:text-disabled-primary-content"
{% if name %}name="{{ name }}"{% endif %}
{% if value %}value="{{ value }}"{% endif %}
{% if form_name %}form="{{ form_name }}"{% endif %}>{{ text }}</button>
59 changes: 54 additions & 5 deletions backend/projectify/templatetags/projectify.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2024 JWP Consulting GK
"""Shared template tags for Projectify."""

from typing import Optional, Union
from typing import Any, Literal, Optional, Union

from django import template
from django.template.loader import render_to_string
Expand All @@ -26,7 +26,9 @@ def percent(value: Optional[float]) -> Optional[str]:


@register.simple_tag
def anchor(href: str, label: str, external: bool = False) -> SafeText:
def anchor(
href: str, label: str, external: bool = False, *args: Any, **kwargs: Any
) -> SafeText:
"""
Render a fully styled HTML anchor.

Expand All @@ -39,9 +41,13 @@ def anchor(href: str, label: str, external: bool = False) -> SafeText:
if href == "":
raise ValueError("Empty href supplied")
try:
url = reverse(href)
url = reverse(href, args=args, kwargs=kwargs)
except NoReverseMatch:
url = href
# TODO, if we have a reverse match, we don't have external URLs
# We could switch all callers of the anchor function to use the route name
# and implicitly switch on external for all other URLs. After all, if it's
# an internal resource, we'd have a named route for that resource.
if external:
a_extra = mark_safe(' target="_blank"')
extra = format_html(
Expand All @@ -62,18 +68,61 @@ def anchor(href: str, label: str, external: bool = False) -> SafeText:


@register.simple_tag
def user_avatar(user: User) -> SafeText:
def action_button(
text: str,
icon: Optional[str] = None,
style: Literal["secondary", "destructive"] = "secondary",
value: Optional[str] = None,
name: Optional[str] = None,
grow: bool = True,
) -> SafeText:
"""Render a styled action button with icon."""
# Source: frontend/src/lib/funabashi/buttons/Button.svelte
color_classes = {
"secondary": "text-secondary-content hover:bg-secondary-hover hover:text-secondary-content-hover active:bg-secondary-pressed active:text-secondary-content-hover",
"destructive": "text-destructive hover:bg-destructive-secondary-hover hover:text-destructive-hover active:bg-destructive-secondary-pressed active:text-destructive-pressed",
}

return format_html(
'<button type="submit" '
'class="{width_class} {color_classes} flex min-w-max flex-row justify-center gap-2 rounded-lg px-4 py-2 font-bold"'
"{value}{name}>"
"{icon}"
"{text}"
"</button>",
width_class="w-full" if grow else "min-w-max",
color_classes=color_classes[style],
icon=format_html(
'<div class="w-6 h-6">{icon}</div>',
icon=render_to_string(f"heroicons/{icon}.svg"),
)
if icon
else "",
text=text,
value=format_html(' value="{value}"', value=value) if value else "",
name=format_html(' name="{name}"', name=name) if name else "",
)


@register.simple_tag
def user_avatar(user: Optional[User]) -> SafeText:
"""
Render a user avatar image.

Takes a user object as parameter.
"""
if user.profile_picture:
if user and user.profile_picture:
return format_html(
'<div class="shrink-0 flex flex-row h-6 w-6 items-center rounded-full border border-primary"><img src="{src}" alt="{alt}" height="24" width="24" class="h-full w-full overflow-x-auto rounded-full object-cover object-center"></div>',
src=user.profile_picture.url,
alt=str(user),
)
# TODO improve appearance
elif user:
return format_html(
'<div class="shrink-0 flex flex-row h-6 w-6 items-center rounded-full border border-primary" aria-label="{alt}"></div>',
alt=str(user),
)
else:
return mark_safe(
'<div class="shrink-0 flex flex-row h-6 w-6 items-center rounded-full border border-primary bg-base-200"></div>'
Expand Down
1 change: 1 addition & 0 deletions backend/projectify/theme/static_src/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const config = {
content: [
// Apps
"../../../projectify/*/templates/**/*.html",
"../../../projectify/**/const.py",
// Shared templates
"../../../projectify/templates/**/*.html",
// Heroicons
Expand Down
26 changes: 26 additions & 0 deletions backend/projectify/workspace/dashboard_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,20 @@
task_actions,
task_create,
task_create_sub_task_form,
task_delete_view,
task_detail,
task_move,
task_move_to_section,
task_update_view,
)
from projectify.workspace.views.workspace import (
workspace_list_view,
workspace_settings_billing,
workspace_settings_billing_edit,
workspace_settings_edit_label,
workspace_settings_general,
workspace_settings_label,
workspace_settings_new_label,
workspace_settings_projects,
workspace_settings_quota,
workspace_settings_team_member_remove,
Expand Down Expand Up @@ -71,6 +76,21 @@
workspace_settings_projects,
name="projects",
),
path(
"<uuid:workspace_uuid>/labels",
workspace_settings_label,
name="labels",
),
path(
"<uuid:workspace_uuid>/labels/create",
workspace_settings_new_label,
name="create-label",
),
path(
"<uuid:workspace_uuid>/labels/<uuid:label_uuid>",
workspace_settings_edit_label,
name="edit-label",
),
path(
"<uuid:workspace_uuid>/settings/team-members",
workspace_settings_team_members,
Expand Down Expand Up @@ -153,8 +173,14 @@
path("<uuid:task_uuid>/update", task_update_view, name="update"),
# Move/delete actions menu
path("<uuid:task_uuid>/actions", task_actions, name="actions"),
path("<uuid:task_uuid>/delete", task_delete_view, name="delete"),
# Form
path("<uuid:task_uuid>/move", task_move, name="move"),
path(
"<uuid:task_uuid>/move-to-section",
task_move_to_section,
name="move-to-section",
),
path(
"sub-task/<int:sub_tasks>",
task_create_sub_task_form,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2025 JWP Consulting GK
# Generated by Django 5.2.9 on 2026-01-26 07:17
"""Validate label color range."""

from django.db import migrations, models


class Migration(migrations.Migration):
"""The migration."""

dependencies = [
("workspace", "0065_workspace_title"),
]

operations = [
migrations.AddConstraint(
model_name="label",
constraint=models.CheckConstraint(
check=models.Q(("color__gte", 0), ("color__lte", 7)),
name="label_color_range",
violation_error_message="Color must be between 0 and 7",
),
),
]
48 changes: 47 additions & 1 deletion backend/projectify/workspace/models/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 JWP Consulting GK
# SPDX-FileCopyrightText: 2023,2025 JWP Consulting GK
"""Contains enums and other constant values."""

from django.db import models
Expand All @@ -14,3 +14,49 @@ class TeamMemberRoles(models.TextChoices):
CONTRIBUTOR = "CONTRIBUTOR", _("Contributor")
MAINTAINER = "MAINTAINER", _("Maintainer")
OWNER = "OWNER", _("Owner")


COLOR_MAP = {
0: {
"name": _("Orange"),
"bg_class": "bg-label-orange",
"border_class": "border-label-text-orange",
"text_class": "text-label-text-orange",
},
1: {
"name": _("Pink"),
"bg_class": "bg-label-pink",
"border_class": "border-label-text-pink",
"text_class": "text-label-text-pink",
},
2: {
"name": _("Blue"),
"bg_class": "bg-label-blue",
"border_class": "border-label-text-blue",
"text_class": "text-label-text-blue",
},
3: {
"name": _("Purple"),
"bg_class": "bg-label-purple",
"border_class": "border-label-text-purple",
"text_class": "text-label-text-purple",
},
4: {
"name": _("Yellow"),
"bg_class": "bg-label-yellow",
"border_class": "border-label-text-yellow",
"text_class": "text-label-text-yellow",
},
5: {
"name": _("Red"),
"bg_class": "bg-label-red",
"border_class": "border-label-text-red",
"text_class": "text-label-text-red",
},
6: {
"name": _("Green"),
"bg_class": "bg-label-green",
"border_class": "border-label-text-green",
"text_class": "text-label-text-green",
},
}
19 changes: 18 additions & 1 deletion backend/projectify/workspace/models/label.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023-2024 JWP Consulting GK
# SPDX-FileCopyrightText: 2023-2025 JWP Consulting GK
"""Label manager and model."""

import uuid
from typing import TYPE_CHECKING, Any, ClassVar, Self, cast

from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from django.db.models import CheckConstraint, Q
from django.utils.translation import gettext_lazy as _

from projectify.lib.models import BaseModel
Expand Down Expand Up @@ -37,6 +38,15 @@ class Label(BaseModel):

# TODO It should be fine to just use TitleDescription here
name = models.CharField(max_length=255)
"""
0 -> orange
2 -> pink
3 -> blue
4 -> purple
5 -> yellow
6 -> red
7 -> green
"""
color = models.PositiveBigIntegerField(
help_text=_("Color index"),
)
Expand Down Expand Up @@ -68,3 +78,10 @@ class Meta:
# TODO remove this restriction, just let users do what they want to
unique_together = ("workspace", "name")
ordering = ("-modified",)
constraints = [
CheckConstraint(
check=Q(color__gte=0) & Q(color__lte=7),
name="label_color_range",
violation_error_message=_("Color must be between 0 and 7"),
),
]
Loading
Loading