Add markdown support (and sanitization) for task descriptions
This also includes some initial work toward implementing task editing and deleting. Closes #12 Related to #2
This commit is contained in:
parent
9bb625afe6
commit
47b1e6ca82
14 changed files with 373 additions and 14 deletions
|
|
@ -7,3 +7,7 @@
|
||||||
- psycopg2 (for postgresql)
|
- psycopg2 (for postgresql)
|
||||||
- pyargon2 (for HashV1)
|
- pyargon2 (for HashV1)
|
||||||
- humanize (for generating human-readable timedeltas)
|
- humanize (for generating human-readable timedeltas)
|
||||||
|
- nh3 (for markdown and general HTML sanitization)
|
||||||
|
|
||||||
|
## Already Included
|
||||||
|
- Certain tag definitions from bleach-allowlist, used in markdown sanitization.
|
||||||
|
|
@ -8,6 +8,7 @@ from taskflower.config import SignUpMode, config
|
||||||
from taskflower.db import db
|
from taskflower.db import db
|
||||||
from taskflower.api import APIBase
|
from taskflower.api import APIBase
|
||||||
from taskflower.db.model.user import User
|
from taskflower.db.model.user import User
|
||||||
|
from taskflower.sanitize.markdown import render_and_sanitize
|
||||||
from taskflower.web import web_base
|
from taskflower.web import web_base
|
||||||
|
|
||||||
from taskflower.tools.hibp import hibp_bp
|
from taskflower.tools.hibp import hibp_bp
|
||||||
|
|
@ -69,10 +70,14 @@ def template_utility_fns():
|
||||||
usr.enabled and usr.administrator
|
usr.enabled and usr.administrator
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def render_as_markdown(raw: str):
|
||||||
|
return render_and_sanitize(raw)
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
literal_call=literal_call,
|
literal_call=literal_call,
|
||||||
reltime=reltime,
|
reltime=reltime,
|
||||||
can_generate_sign_up_codes=can_generate_sign_up_codes
|
can_generate_sign_up_codes=can_generate_sign_up_codes,
|
||||||
|
render_as_markdown=render_as_markdown
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,21 @@ class FormCreatesObjectWithUser[T](Form):
|
||||||
create an object with the specified parameters.
|
create an object with the specified parameters.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class FormEditsObjectWithUser[T](Form):
|
||||||
|
''' Trait that indicates that this ``Form`` can be used to edit an object
|
||||||
|
if provided with a ``User`` object.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def edit_object(
|
||||||
|
self,
|
||||||
|
current_user: User, # pyright:ignore[reportUnusedParameter]
|
||||||
|
target_object: T # pyright:ignore[reportUnusedParameter]
|
||||||
|
) -> Either[Exception, T]:
|
||||||
|
''' Try to edit ``target_object`` on behalf of ``current_user``.
|
||||||
|
|
||||||
|
This function checks for authorization, and will return an
|
||||||
|
``AuthorizationError`` if ``current_user`` is not authorized to
|
||||||
|
edit an object with the specified parameters.
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
from wtforms import DateTimeLocalField, SelectField, StringField, ValidationError, validators
|
from wtforms import DateTimeLocalField, SelectField, 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
|
from taskflower.auth.permission.checks import assert_user_perms_on_namespace
|
||||||
|
|
@ -8,10 +8,35 @@ from taskflower.db import db
|
||||||
from taskflower.db.model.namespace import Namespace
|
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 FormCreatesObjectWithUser
|
from taskflower.form import FormCreatesObjectWithUser, FormEditsObjectWithUser
|
||||||
from taskflower.types.either import Either, Left, Right
|
from taskflower.types.either import Either, Left, Right
|
||||||
from taskflower.types.option import Option
|
from taskflower.types.option import Option
|
||||||
|
|
||||||
|
class TaskEditForm(FormEditsObjectWithUser[Task]):
|
||||||
|
name: StringField = StringField(
|
||||||
|
'Task Name',
|
||||||
|
[
|
||||||
|
validators.Length(
|
||||||
|
min=1,
|
||||||
|
max=64,
|
||||||
|
message='Task name must be between 1 and 64 characters!'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
due: DateTimeLocalField = DateTimeLocalField(
|
||||||
|
'Due Date',
|
||||||
|
[
|
||||||
|
validators.DataRequired()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
description: TextAreaField = TextAreaField(
|
||||||
|
'Description',
|
||||||
|
[
|
||||||
|
validators.Optional()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def task_form_for_user(
|
def task_form_for_user(
|
||||||
user: User
|
user: User
|
||||||
) -> type[FormCreatesObjectWithUser[Task]]:
|
) -> type[FormCreatesObjectWithUser[Task]]:
|
||||||
|
|
@ -35,7 +60,10 @@ def task_form_for_user(
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
due: DateTimeLocalField = DateTimeLocalField(
|
due: DateTimeLocalField = DateTimeLocalField(
|
||||||
'Due Date'
|
'Due Date',
|
||||||
|
[
|
||||||
|
validators.DataRequired()
|
||||||
|
]
|
||||||
)
|
)
|
||||||
namespace: SelectField = SelectField(
|
namespace: SelectField = SelectField(
|
||||||
'Select a namespace',
|
'Select a namespace',
|
||||||
|
|
@ -45,8 +73,8 @@ def task_form_for_user(
|
||||||
choices=namespace_choices,
|
choices=namespace_choices,
|
||||||
coerce=int
|
coerce=int
|
||||||
)
|
)
|
||||||
description: StringField = StringField(
|
description: TextAreaField = TextAreaField(
|
||||||
'Description',
|
'Description (supports markdown)',
|
||||||
[
|
[
|
||||||
validators.Optional()
|
validators.Optional()
|
||||||
]
|
]
|
||||||
|
|
|
||||||
50
src/taskflower/sanitize/bleach_allowlist_md.py
Normal file
50
src/taskflower/sanitize/bleach_allowlist_md.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
'''
|
||||||
|
These tag lists are adapted from the "Bleach-Allowlist" project, available at
|
||||||
|
https://github.com/yourcelf/bleach-allowlist.
|
||||||
|
|
||||||
|
They are incorporated here under the BSD 2-Clause Simplified license"
|
||||||
|
|
||||||
|
BSD 2-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2017, Charlie DeTar
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Tags suitable for rendering markdown
|
||||||
|
markdown_tags = {
|
||||||
|
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
|
"b", "i", "strong", "em", "tt",
|
||||||
|
"p", "br",
|
||||||
|
"span", "div", "blockquote", "code", "pre", "hr",
|
||||||
|
"ul", "ol", "li", "dd", "dt",
|
||||||
|
"img",
|
||||||
|
"a",
|
||||||
|
"sub", "sup",
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown_attrs = {
|
||||||
|
"*" : {"id"},
|
||||||
|
"img": {"src", "alt", "title"},
|
||||||
|
"a" : {"href", "alt", "title"},
|
||||||
|
}
|
||||||
34
src/taskflower/sanitize/markdown.py
Normal file
34
src/taskflower/sanitize/markdown.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import markdown
|
||||||
|
import nh3
|
||||||
|
|
||||||
|
from taskflower.sanitize.bleach_allowlist_md import markdown_attrs, markdown_tags
|
||||||
|
|
||||||
|
SafeHTML=str
|
||||||
|
|
||||||
|
def first_pass_escape(raw: str) -> str:
|
||||||
|
''' Perform basic HTML escaping by replacing < and > with < and &rt;,
|
||||||
|
respectively.
|
||||||
|
'''
|
||||||
|
return raw.replace(
|
||||||
|
'<',
|
||||||
|
'<'
|
||||||
|
).replace(
|
||||||
|
'>',
|
||||||
|
'>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def sanitize_markdown_render_result(render_res: str) -> SafeHTML:
|
||||||
|
safe: SafeHTML = nh3.clean(
|
||||||
|
render_res,
|
||||||
|
markdown_tags,
|
||||||
|
attributes=markdown_attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
return safe
|
||||||
|
|
||||||
|
def render_and_sanitize(raw: str) -> SafeHTML:
|
||||||
|
return sanitize_markdown_render_result(
|
||||||
|
markdown.markdown(
|
||||||
|
first_pass_escape(raw)
|
||||||
|
)
|
||||||
|
)
|
||||||
53
src/taskflower/static/delete.svg
Normal file
53
src/taskflower/static/delete.svg
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="32mm"
|
||||||
|
height="32mm"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4219"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||||
|
sodipodi:docname="delete.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview4221"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:pageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:snap-page="true"
|
||||||
|
inkscape:zoom="6.0007856"
|
||||||
|
inkscape:cx="60.492079"
|
||||||
|
inkscape:cy="59.992145"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1018"
|
||||||
|
inkscape:window-x="1600"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs4216" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 4.3442615,4.3442615 27.655737,27.655737"
|
||||||
|
id="path4313" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 27.655737,4.3442615 4.3442615,27.655737"
|
||||||
|
id="path4428" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
76
src/taskflower/static/edit.svg
Normal file
76
src/taskflower/static/edit.svg
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="32mm"
|
||||||
|
height="32mm"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||||
|
sodipodi:docname="edit.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:pageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:snap-bbox-midpoints="true"
|
||||||
|
inkscape:snap-object-midpoints="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:snap-page="true"
|
||||||
|
inkscape:zoom="4.2431962"
|
||||||
|
inkscape:cx="59.860536"
|
||||||
|
inkscape:cy="57.03248"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-height="988"
|
||||||
|
inkscape:window-x="3520"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g1827"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:snap-center="true"
|
||||||
|
inkscape:object-paths="true" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g1827"
|
||||||
|
transform="matrix(0.97090565,0,0,0.95328037,1.2130415,-0.2769029)"
|
||||||
|
style="stroke-width:3.11833246;stroke-miterlimit:4;stroke-dasharray:none">
|
||||||
|
<path
|
||||||
|
id="rect1114"
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:3.11833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="M 30.150425,6.2476261 25.752374,1.8495753 9.7345348,17.867413 6.6776561,25.788471 14.132586,22.265464 Z"
|
||||||
|
sodipodi:nodetypes="cccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:3.11833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 20.912356,6.5215018 25.17026,10.953753"
|
||||||
|
id="path1229" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 19.207329,2.6958367 -16.5114923,0 V 29.304162 H 29.304162 l 10e-7,-17.319006"
|
||||||
|
id="path1933"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="M 9.8263677,20.024836 8.4242261,23.592142 11.986204,21.939401 Z"
|
||||||
|
id="path3583"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -63,7 +63,6 @@
|
||||||
|
|
||||||
.list .detail-view-elem .detail-view-header {
|
.list .detail-view-elem .detail-view-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list .detail-view-elem .detail-view-header::before{
|
.list .detail-view-elem .detail-view-header::before{
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@
|
||||||
--form-bg-1: #331826;
|
--form-bg-1: #331826;
|
||||||
--form-bg-2: #462837;
|
--form-bg-2: #462837;
|
||||||
--on-form: #ffc4d1;
|
--on-form: #ffc4d1;
|
||||||
|
|
||||||
|
--btn-1: #7a3053;
|
||||||
|
--btn-1-border: #d77faa;
|
||||||
|
--on-btn-1: #ffffff;
|
||||||
|
|
||||||
|
--btn-1-hlt: var(--btn-1-border);
|
||||||
|
--on-btn-1-hlt: #462837;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
@ -135,3 +142,50 @@ h3 {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-tray {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 0 0;
|
||||||
|
background-color: var(--btn-1);
|
||||||
|
color: var(--on-btn-1);
|
||||||
|
width: max-content;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border: 2px solid var(--btn-1-border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin: 0.5rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-1-hlt);
|
||||||
|
color: var(--on-btn-1-hlt);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex: 0 0;
|
||||||
|
max-height: 1rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1 0;
|
||||||
|
margin: auto;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
{% extends "main.html" %}
|
{% extends "main.html" %}
|
||||||
{% from "_formhelpers.html" import render_field %}
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
|
||||||
|
{% block head_extras %}
|
||||||
|
<link rel="stylesheet" href={{ url_for("static", filename="forms.css") }} />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}Create Task{% endblock %}
|
{% block title %}Create Task{% endblock %}
|
||||||
|
|
||||||
{% block main_content %}
|
{% block main_content %}
|
||||||
<h1>Create Task</h1>
|
<form class="default-form" id="create-task-form" method="POST">
|
||||||
<form id="create-task-form" method="POST">
|
<h1>Create Task</h1>
|
||||||
<dl>
|
<dl>
|
||||||
{{ render_field(form.name) }}
|
{{ render_field(form.name) }}
|
||||||
{{ render_field(form.due) }}
|
{{ render_field(form.due) }}
|
||||||
{{ render_field(form.description) }}
|
{{ render_field(form.description) }}
|
||||||
{{ render_field(form.namespace) }}
|
{{ render_field(form.namespace) }}
|
||||||
</dl>
|
</dl>
|
||||||
<p><input type="submit">Create Task</input></p>
|
<p><button type="submit">Create Task</button></p>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -39,13 +39,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>
|
||||||
|
<div class="link-tray">
|
||||||
|
<a class="link-btn" href="{{url_for('web.task.edit', id=task.id, next=request.path)}}"><img src="{{url_for('static', filename='edit.svg')}}"/><span>Edit Task</span></a>
|
||||||
|
<a class="link-btn" href="{{url_for('web.task.delete', id=task.id, next=request.path)}}"><img src="{{url_for('static', filename='delete.svg')}}"/><span>Delete Task</span></a>
|
||||||
|
</div>
|
||||||
<p class="small-details">
|
<p class="small-details">
|
||||||
Task ID: {{ task.id }}
|
Task ID: {{ task.id }}
|
||||||
<br/>Created: {{ task.created }} in namespace {{ task.namespace_id }}
|
<br/>Created: {{ task.created }} in namespace {{ task.namespace_id }}
|
||||||
<br/>Due: {{ task.due }}
|
<br/>Due: {{ task.due }}
|
||||||
</p>
|
</p>
|
||||||
<hr/>
|
<hr/>
|
||||||
<p class="main-description">{{ task.description }}</p>
|
<div class="main-description">{{ render_as_markdown(task.description)|safe }}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
20
src/taskflower/templates/task/edit.html
Normal file
20
src/taskflower/templates/task/edit.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "main.html" %}
|
||||||
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
|
||||||
|
{% block head_extras %}
|
||||||
|
<link rel="stylesheet" href={{ url_for("static", filename="forms.css") }} />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}Edit Task{% endblock %}
|
||||||
|
|
||||||
|
{% block main_content %}
|
||||||
|
<form class="default-form" id="edit-task-form" method="POST">
|
||||||
|
<h1>Edit Task</h1>
|
||||||
|
<dl>
|
||||||
|
{{ render_field(form.name) }}
|
||||||
|
{{ render_field(form.due) }}
|
||||||
|
{{ render_field(form.description) }}
|
||||||
|
</dl>
|
||||||
|
<button type="submit">Create Task</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -191,3 +191,13 @@ def uncomplete(id: int):
|
||||||
lambda usr_tsk: redirect(next),
|
lambda usr_tsk: redirect(next),
|
||||||
lambda err: response_from_exception(err)
|
lambda err: response_from_exception(err)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@web_tasks.route('/<int:id>/edit')
|
||||||
|
@login_required
|
||||||
|
def edit(id: int):
|
||||||
|
return redirect(url_for('web.task.all'))
|
||||||
|
|
||||||
|
@web_tasks.route('/<int:id>/delete')
|
||||||
|
@login_required
|
||||||
|
def delete(id: int):
|
||||||
|
return redirect(url_for('web.task.all'))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue