Basic task category/tag implementation
This commit is contained in:
parent
875c5f7f06
commit
c7f6f3f4f1
9 changed files with 317 additions and 21 deletions
43
src/migrations/versions/8576b056149e_add_category_to_task.py
Normal file
43
src/migrations/versions/8576b056149e_add_category_to_task.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""Add category to task
|
||||||
|
|
||||||
|
Revision ID: 8576b056149e
|
||||||
|
Revises: 067ee615c967
|
||||||
|
Create Date: 2025-12-03 12:45:29.641119
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8576b056149e'
|
||||||
|
down_revision = '067ee615c967'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('cat_to_task')
|
||||||
|
with op.batch_alter_table('task', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('category', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key('fk_task_cat', 'category', ['category'], ['id'], ondelete='SET NULL')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('task', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('fk_task_cat', type_='foreignkey')
|
||||||
|
batch_op.drop_column('category')
|
||||||
|
|
||||||
|
op.create_table('cat_to_task',
|
||||||
|
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('cat', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('task', sa.INTEGER(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['cat'], ['category.id'], name=op.f('fk_c2t_cat'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['task'], ['task.id'], name=op.f('fk_c2t_task'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -67,13 +67,6 @@ class TaskCategory(db.Model):
|
||||||
name : Mapped[str] = mapped_column(String(TAG_FIELD_NAME_MAX_LEN))
|
name : Mapped[str] = mapped_column(String(TAG_FIELD_NAME_MAX_LEN))
|
||||||
namespace : Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE', name='fk_cat_ns'))
|
namespace : Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE', name='fk_cat_ns'))
|
||||||
|
|
||||||
class CatToTask(db.Model):
|
|
||||||
__tablename__: str = 'cat_to_task'
|
|
||||||
|
|
||||||
id : Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
||||||
cat : Mapped[int] = mapped_column(Integer, ForeignKey('category.id', ondelete='CASCADE', name='fk_c2t_cat'))
|
|
||||||
task : Mapped[int] = mapped_column(Integer, ForeignKey('task.id', ondelete='CASCADE', name='fk_c2t_task'))
|
|
||||||
|
|
||||||
|
|
||||||
class Field(db.Model):
|
class Field(db.Model):
|
||||||
__tablename__: str = 'field'
|
__tablename__: str = 'field'
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class Task(db.Model):
|
||||||
id : Mapped[int] = mapped_column(Integer, primary_key=True)
|
id : Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
namespace : Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE'))
|
namespace : Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE'))
|
||||||
parent : Mapped[int|None] = mapped_column(Integer, ForeignKey('task.id', name='fk_task_parent'), nullable=True)
|
parent : Mapped[int|None] = mapped_column(Integer, ForeignKey('task.id', name='fk_task_parent'), nullable=True)
|
||||||
|
category : Mapped[int|None] = mapped_column(Integer, ForeignKey('category.id', ondelete='SET NULL', name='fk_task_cat'), nullable=True)
|
||||||
|
|
||||||
name : Mapped[str] = mapped_column(String(64))
|
name : Mapped[str] = mapped_column(String(64))
|
||||||
description : Mapped[str] = mapped_column(String)
|
description : Mapped[str] = mapped_column(String)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
from typing import override
|
from typing import final, override
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from wtforms import Field, Form, SelectField, StringField, TextAreaField, ValidationError, validators
|
from wtforms import Field, Form, SelectField, SelectMultipleField, StringField, TextAreaField, ValidationError, validators
|
||||||
|
|
||||||
from taskflower.auth.permission import NamespacePermissionType
|
from taskflower.auth.permission import NamespacePermissionType
|
||||||
from taskflower.auth.permission.checks import assert_user_perms_on_namespace, assert_user_perms_on_task
|
from taskflower.auth.permission.checks import assert_user_perms_on_namespace, assert_user_perms_on_task
|
||||||
from taskflower.auth.permission.lookups import namespaces_where_user_can
|
from taskflower.auth.permission.lookups import namespaces_where_user_can
|
||||||
from taskflower.db import db
|
from taskflower.db import db, db_fetch_by_id
|
||||||
from taskflower.db.model.namespace import Namespace
|
from taskflower.db.model.namespace import Namespace
|
||||||
|
from taskflower.db.model.tag import Tag, TagToTask, TaskCategory
|
||||||
from taskflower.db.model.task import Task
|
from taskflower.db.model.task import Task
|
||||||
from taskflower.db.model.user import User
|
from taskflower.db.model.user import User
|
||||||
from taskflower.form import FormCreatesObjectWithUser, FormEditsObjectWithUser
|
from taskflower.form import FormCreatesObjectWithUser, FormEditsObjectWithUser
|
||||||
from taskflower.types import ann
|
from taskflower.types import AssertType, CheckType, ann
|
||||||
from taskflower.types.either import Either, Left, Right
|
from taskflower.types.either import Either, Left, Right, reduce_either
|
||||||
from taskflower.types.option import Option
|
from taskflower.types.option import Option
|
||||||
from taskflower.util.time import from_sophont_provided_data
|
from taskflower.util.time import from_sophont_provided_data
|
||||||
|
|
||||||
|
|
@ -44,9 +45,99 @@ def is_valid_timestamp(form: Form, field: Field):
|
||||||
if isinstance(res, Left):
|
if isinstance(res, Left):
|
||||||
raise ValidationError(f'Parse failure: {res.val!s}')
|
raise ValidationError(f'Parse failure: {res.val!s}')
|
||||||
|
|
||||||
|
@final
|
||||||
|
class ValidCategory:
|
||||||
|
def __init__(self, ns: Namespace) -> None:
|
||||||
|
self._ns = ns
|
||||||
|
|
||||||
|
def __call__(self, _: Form, fld: Field) -> None:
|
||||||
|
if fld.data == -1: # pyright:ignore[reportAny]
|
||||||
|
return
|
||||||
|
res = CheckType(int)(
|
||||||
|
fld.data # pyright:ignore[reportAny]
|
||||||
|
).flat_map(
|
||||||
|
lambda cid: db_fetch_by_id(
|
||||||
|
TaskCategory,
|
||||||
|
cid,
|
||||||
|
db
|
||||||
|
).flat_map(
|
||||||
|
lambda cat: Either.do_assert(
|
||||||
|
cat.namespace == self._ns.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(res, Left):
|
||||||
|
raise ValidationError('Invalid category!')
|
||||||
|
|
||||||
|
@final
|
||||||
|
class ValidTag:
|
||||||
|
def __init__(self, ns: Namespace) -> None:
|
||||||
|
self._ns = ns
|
||||||
|
|
||||||
|
def __call__(self, _: Form, fraw: Field) -> None:
|
||||||
|
res = CheckType(list[int])(
|
||||||
|
fraw.data # pyright:ignore[reportAny]
|
||||||
|
).flat_map(
|
||||||
|
lambda tag_ids: reduce_either([
|
||||||
|
CheckType(int)(
|
||||||
|
tag_id
|
||||||
|
).flat_map(
|
||||||
|
lambda tid: db_fetch_by_id(
|
||||||
|
Tag,
|
||||||
|
tid,
|
||||||
|
db
|
||||||
|
)
|
||||||
|
).flat_map(
|
||||||
|
lambda tag: Either.do_assert(
|
||||||
|
tag.namespace == self._ns.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for tag_id in tag_ids
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(res, Left):
|
||||||
|
raise ValidationError('Invalid tag!')
|
||||||
|
|
||||||
|
|
||||||
def task_edit_form_for_task(
|
def task_edit_form_for_task(
|
||||||
t: Task
|
t: Task,
|
||||||
|
ns: Namespace
|
||||||
) -> type[FormEditsObjectWithUser[Task]]:
|
) -> type[FormEditsObjectWithUser[Task]]:
|
||||||
|
cat_choices = [(-1, 'None')] + [
|
||||||
|
(cat.id, cat.name)
|
||||||
|
for cat in db.session.query(
|
||||||
|
TaskCategory
|
||||||
|
).filter(
|
||||||
|
TaskCategory.namespace == t.namespace
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
tag_choices = [
|
||||||
|
(tag.id, tag.name)
|
||||||
|
for tag in db.session.query(
|
||||||
|
Tag
|
||||||
|
).filter(
|
||||||
|
Tag.namespace == t.namespace
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
cur_tags = db.session.query(
|
||||||
|
Tag
|
||||||
|
).filter(
|
||||||
|
Tag.namespace == t.namespace
|
||||||
|
).join(
|
||||||
|
TagToTask,
|
||||||
|
TagToTask.tag == Tag.id
|
||||||
|
).filter(
|
||||||
|
TagToTask.task == t.id
|
||||||
|
).all()
|
||||||
|
tag_defaults = [
|
||||||
|
tg.id
|
||||||
|
for tg in cur_tags
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TaskEditForm(FormEditsObjectWithUser[Task]):
|
class TaskEditForm(FormEditsObjectWithUser[Task]):
|
||||||
name: StringField = StringField(
|
name: StringField = StringField(
|
||||||
'Task Name',
|
'Task Name',
|
||||||
|
|
@ -59,6 +150,28 @@ def task_edit_form_for_task(
|
||||||
],
|
],
|
||||||
default=t.name
|
default=t.name
|
||||||
)
|
)
|
||||||
|
category: SelectField = SelectField(
|
||||||
|
'Category',
|
||||||
|
[
|
||||||
|
ValidCategory(ns)
|
||||||
|
],
|
||||||
|
coerce=int,
|
||||||
|
choices=cat_choices,
|
||||||
|
default=(
|
||||||
|
t.category
|
||||||
|
if t.category
|
||||||
|
else -1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tags: SelectMultipleField = SelectMultipleField(
|
||||||
|
'Tags',
|
||||||
|
[
|
||||||
|
ValidTag(ns)
|
||||||
|
],
|
||||||
|
coerce=int,
|
||||||
|
choices=tag_choices,
|
||||||
|
default=tag_defaults
|
||||||
|
)
|
||||||
due: StringField = StringField(
|
due: StringField = StringField(
|
||||||
'Due Date',
|
'Due Date',
|
||||||
[
|
[
|
||||||
|
|
@ -87,6 +200,48 @@ def task_edit_form_for_task(
|
||||||
def _do_edit(tsk: Task) -> Either[Exception, Task]:
|
def _do_edit(tsk: Task) -> Either[Exception, Task]:
|
||||||
tsk.name = ann(self.name.data)
|
tsk.name = ann(self.name.data)
|
||||||
|
|
||||||
|
if self.category.data: # pyright:ignore[reportAny]
|
||||||
|
cat_id = AssertType(int)(
|
||||||
|
self.category.data # pyright:ignore[reportAny]
|
||||||
|
)
|
||||||
|
|
||||||
|
tsk.category = (
|
||||||
|
None if cat_id == -1 else cat_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.tags.data:
|
||||||
|
res = CheckType(list[int])(
|
||||||
|
self.tags.data
|
||||||
|
).flat_map(
|
||||||
|
lambda tids: reduce_either([
|
||||||
|
db_fetch_by_id(
|
||||||
|
Tag,
|
||||||
|
tid,
|
||||||
|
db
|
||||||
|
)
|
||||||
|
for tid in tids
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(res, Right):
|
||||||
|
tags_to_apply = res.val
|
||||||
|
_ = db.session.query(
|
||||||
|
TagToTask
|
||||||
|
).filter(
|
||||||
|
TagToTask.task == tsk.id
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
for tag in tags_to_apply:
|
||||||
|
t2t = TagToTask(
|
||||||
|
tag=tag.id, # pyright:ignore[reportCallIssue]
|
||||||
|
task=tsk.id # pyright:ignore[reportCallIssue]
|
||||||
|
)
|
||||||
|
print('Added tag')
|
||||||
|
# Queue the tag associations, but don't commit them
|
||||||
|
# yet - let the caller decide whether to commit
|
||||||
|
# along with the task edits.
|
||||||
|
db.session.add(t2t)
|
||||||
|
|
||||||
if self.due.data:
|
if self.due.data:
|
||||||
# We already check the validity during validation,
|
# We already check the validity during validation,
|
||||||
# so this call should always be Right
|
# so this call should always be Right
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ import humanize
|
||||||
from taskflower.auth.permission import NPT, NamespacePermissionType
|
from taskflower.auth.permission import NPT, NamespacePermissionType
|
||||||
from taskflower.auth.permission.checks import assert_user_perms_on_task
|
from taskflower.auth.permission.checks import assert_user_perms_on_task
|
||||||
from taskflower.auth.permission.lookups import get_user_perms_on_task
|
from taskflower.auth.permission.lookups import get_user_perms_on_task
|
||||||
|
from taskflower.db import db, db_fetch_by_id
|
||||||
|
from taskflower.db.model.tag import Tag, TagToTask, TaskCategory
|
||||||
from taskflower.db.model.task import Task
|
from taskflower.db.model.task import Task
|
||||||
from taskflower.db.model.user import User
|
from taskflower.db.model.user import User
|
||||||
from taskflower.types.either import Either
|
from taskflower.types.either import Either, gather_successes
|
||||||
from taskflower.util.time import ensure_timezone_aware, now
|
from taskflower.util.time import ensure_timezone_aware, now
|
||||||
|
|
||||||
def _due_str(due: datetime) -> str:
|
def _due_str(due: datetime) -> str:
|
||||||
|
|
@ -37,6 +39,8 @@ class TaskForUser:
|
||||||
completed: datetime|None
|
completed: datetime|None
|
||||||
overdue: bool
|
overdue: bool
|
||||||
namespace_id: int
|
namespace_id: int
|
||||||
|
category: str|None
|
||||||
|
tags: list[str]
|
||||||
|
|
||||||
can_edit: bool
|
can_edit: bool
|
||||||
can_delete: bool
|
can_delete: bool
|
||||||
|
|
@ -85,6 +89,42 @@ class TaskForUser:
|
||||||
in an ``lside_effect()``)
|
in an ``lside_effect()``)
|
||||||
'''
|
'''
|
||||||
perms = get_user_perms_on_task(usr, tsk)
|
perms = get_user_perms_on_task(usr, tsk)
|
||||||
|
cat = (
|
||||||
|
db_fetch_by_id(
|
||||||
|
TaskCategory,
|
||||||
|
tsk.category,
|
||||||
|
db
|
||||||
|
).flat_map(
|
||||||
|
lambda ct: Either.do_assert(
|
||||||
|
ct.namespace == tsk.namespace
|
||||||
|
).map(lambda _: ct)
|
||||||
|
).and_then(
|
||||||
|
lambda cat: cat.name,
|
||||||
|
lambda exc: None
|
||||||
|
)
|
||||||
|
if tsk.category is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = gather_successes([
|
||||||
|
db_fetch_by_id(
|
||||||
|
Tag,
|
||||||
|
t2t.tag,
|
||||||
|
db
|
||||||
|
).flat_map(
|
||||||
|
lambda tag: Either.do_assert(
|
||||||
|
tag.namespace == tsk.namespace
|
||||||
|
).map(lambda _: tag)
|
||||||
|
).map(
|
||||||
|
lambda tag: tag.name
|
||||||
|
)
|
||||||
|
for t2t in db.session.query(
|
||||||
|
TagToTask
|
||||||
|
).filter(
|
||||||
|
TagToTask.task == tsk.id
|
||||||
|
).all()
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
return assert_user_perms_on_task(
|
return assert_user_perms_on_task(
|
||||||
usr,
|
usr,
|
||||||
|
|
@ -107,6 +147,8 @@ class TaskForUser:
|
||||||
),
|
),
|
||||||
tsk.due.replace(tzinfo=timezone.utc) < now(),
|
tsk.due.replace(tzinfo=timezone.utc) < now(),
|
||||||
tsk.namespace,
|
tsk.namespace,
|
||||||
|
cat,
|
||||||
|
tags,
|
||||||
NPT.EDIT_ALL_TASKS in perms,
|
NPT.EDIT_ALL_TASKS in perms,
|
||||||
NPT.DELETE_ALL_TASKS in perms,
|
NPT.DELETE_ALL_TASKS in perms,
|
||||||
NPT.COMPLETE_ALL_TASKS in perms,
|
NPT.COMPLETE_ALL_TASKS in perms,
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,19 @@
|
||||||
&.pad-even {
|
&.pad-even {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.task-name{
|
||||||
|
max-width: 10rem;
|
||||||
|
|
||||||
|
.tag-tray {
|
||||||
|
max-width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
@ -180,6 +193,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-tray {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
p.tag {
|
||||||
|
background-color: #d6a5ff;
|
||||||
|
color: #000;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
margin: 0.1rem 0.2rem;
|
||||||
|
font-size: small;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.detail-view-elem {
|
.detail-view-elem {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,16 @@
|
||||||
class="task-name" id="{{ tlist_cid('task-name', task.id, list_id) }}"
|
class="task-name" id="{{ tlist_cid('task-name', task.id, list_id) }}"
|
||||||
onclick="set_active('{{ tlist_cid('task-detail', task.id, list_id) }}', {{list_id}})"
|
onclick="set_active('{{ tlist_cid('task-detail', task.id, list_id) }}', {{list_id}})"
|
||||||
>
|
>
|
||||||
<label for="{{ tlist_cid('check-inner', task.id, list_id) }}">{{ task.name }}</label>
|
<div style="display: flex;">
|
||||||
|
<label style="flex: 1 0;" for="{{ tlist_cid('check-inner', task.id, list_id) }}">{{ task.name }}</label>
|
||||||
|
<div class="tag-tray">
|
||||||
|
{% if task.tags %}
|
||||||
|
{% for tag in task.tags %}
|
||||||
|
<p class="tag">{{ tag }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="task-due" id="{{ tlist_cid('task-due', task.id, list_id) }}">
|
<td class="task-due" id="{{ tlist_cid('task-due', task.id, list_id) }}">
|
||||||
<p>{{ reltime(task.due)|safe }}</p>
|
<p>{{ reltime(task.due)|safe }}</p>
|
||||||
|
|
@ -51,6 +60,17 @@
|
||||||
<div class="detail-view-header">
|
<div class="detail-view-header">
|
||||||
<h1>Task Details: {{ task.name }}</h1>
|
<h1>Task Details: {{ task.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{% if task.category %}
|
||||||
|
<p class="category">Category: {{ task.category }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if task.tags %}
|
||||||
|
<div class="tag-tray">
|
||||||
|
<p class="tag-header">Tags: </p>
|
||||||
|
{% for tag in task.tags %}
|
||||||
|
<p class="tag">{{ tag }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="link-tray">
|
<div class="link-tray">
|
||||||
{% if task.can_edit %}
|
{% if task.can_edit %}
|
||||||
<a class="link-btn icon-btn" href="{{url_for('web.task.edit', id=task.id, next=request.path)}}">{{icon('edit')|safe}}<span>Edit Task</span></a>
|
<a class="link-btn icon-btn" href="{{url_for('web.task.edit', id=task.id, next=request.path)}}">{{icon('edit')|safe}}<span>Edit Task</span></a>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
<h1>Edit Task</h1>
|
<h1>Edit Task</h1>
|
||||||
<dl>
|
<dl>
|
||||||
{{ render_field(form.name) }}
|
{{ render_field(form.name) }}
|
||||||
|
{{ render_field(form.category) }}
|
||||||
|
{{ render_field(form.tags) }}
|
||||||
{{ render_field(form.due) }}
|
{{ render_field(form.due) }}
|
||||||
{{ render_field(form.description) }}
|
{{ render_field(form.description) }}
|
||||||
<div id="tz-container">{{ render_field(form.timezone) }}</div>
|
<div id="tz-container">{{ render_field(form.timezone) }}</div>
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ from taskflower.auth.permission import NamespacePermissionType
|
||||||
from taskflower.auth.permission.checks import assert_user_perms_on_task
|
from taskflower.auth.permission.checks import assert_user_perms_on_task
|
||||||
from taskflower.auth.permission.lookups import tasks_where_user_can
|
from taskflower.auth.permission.lookups import tasks_where_user_can
|
||||||
from taskflower.auth.violations import check_for_auth_err_and_report
|
from taskflower.auth.violations import check_for_auth_err_and_report
|
||||||
from taskflower.db import commit_update, db, do_delete
|
from taskflower.db import commit_update, db, db_fetch_by_id, do_delete
|
||||||
from taskflower.db.helpers import add_to_db
|
from taskflower.db.helpers import add_to_db
|
||||||
|
from taskflower.db.model.namespace import Namespace
|
||||||
from taskflower.db.model.task import Task
|
from taskflower.db.model.task import Task
|
||||||
from taskflower.db.model.user import User
|
from taskflower.db.model.user import User
|
||||||
from taskflower.form import FormEditsObjectWithUser
|
|
||||||
from taskflower.form.task import task_edit_form_for_task, task_form_for_user
|
from taskflower.form.task import task_edit_form_for_task, task_form_for_user
|
||||||
from taskflower.sanitize.task import TaskForUser
|
from taskflower.sanitize.task import TaskForUser
|
||||||
from taskflower.types.either import Either, Left, Right, reduce_either
|
from taskflower.types.either import Either, Left, Right, reduce_either
|
||||||
|
|
@ -246,10 +246,19 @@ def edit(id: int):
|
||||||
).lside_effect(
|
).lside_effect(
|
||||||
check_for_auth_err_and_report
|
check_for_auth_err_and_report
|
||||||
).flat_map(
|
).flat_map(
|
||||||
lambda tsk: Right[Exception, FormEditsObjectWithUser[Task]](
|
lambda tsk: db_fetch_by_id(
|
||||||
task_edit_form_for_task(
|
Namespace,
|
||||||
tsk
|
tsk.namespace,
|
||||||
)(request.form)
|
db
|
||||||
|
).map(
|
||||||
|
lambda ns: task_edit_form_for_task(
|
||||||
|
tsk,
|
||||||
|
ns
|
||||||
|
)(
|
||||||
|
request.form
|
||||||
|
if request.method == 'POST'
|
||||||
|
else None
|
||||||
|
)
|
||||||
).map(
|
).map(
|
||||||
lambda form: (tsk, form)
|
lambda form: (tsk, form)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue