flask_template_codex/app/tasks/routes.py
2026-08-08 05:37:41 +02:00

384 lines
10 KiB
Python

from datetime import date
from io import BytesIO
from uuid import UUID
from flask import (
abort,
flash,
redirect,
render_template,
request,
send_file,
url_for,
)
from flask_login import current_user, login_required
from app.core.authorization import TASK_EDITOR_ROLES, roles_required
from . import bp
from .forms import (
ActionForm,
BackupImportForm,
FocusSettingsForm,
MarkdownImportForm,
STATUS_LABELS,
SubtaskForm,
TaskForm,
)
from .models import TASK_STATUSES
from .services import (
TaskNotFoundError,
add_subtask,
calculate_forgiving_streak,
clear_completed_tasks,
complete_task,
configure_focus,
create_task,
delete_task,
get_task,
list_today_tasks,
paginate_tasks,
toggle_chunk_session,
toggle_subtask,
undo_clear_completed,
update_task,
)
from .transfer import (
TransferValidationError,
export_backup,
import_backup,
import_markdown,
)
def _owned_task_or_404(task_id: int):
try:
return get_task(task_id, current_user.id)
except TaskNotFoundError:
abort(404)
@bp.get("/")
@login_required
def index():
selected_status = request.args.get("status") or None
if selected_status not in {None, *TASK_STATUSES}:
abort(400)
raw_page = request.args.get("page", "1")
if not raw_page.isdigit() or int(raw_page) < 1:
abort(400)
pagination = paginate_tasks(
current_user.id,
status=selected_status,
page=int(raw_page),
)
if pagination.total and pagination.page > pagination.pages:
abort(404)
return render_template(
"tasks/list.html",
tasks=pagination.items,
pagination=pagination,
selected_status=selected_status,
status_labels=STATUS_LABELS,
)
@bp.get("/today")
@login_required
def today():
tasks = list_today_tasks(current_user.id)
return render_template("tasks/today.html", tasks=tasks)
def _focus_settings_form(task):
chunks = ", ".join(
str(session.get("minutes", 0))
if isinstance(session, dict)
else str(session)
for session in task.chunk_sessions
)
return FocusSettingsForm(
timer_minutes=task.timer_seconds // 60 or None,
context=task.context,
chunk_plan=chunks,
)
def _render_focus(task=None, *, celebrate: bool = False):
return render_template(
"tasks/focus.html",
task=task,
celebrate=celebrate,
focus_form=_focus_settings_form(task) if task else None,
action_form=ActionForm(),
streak=calculate_forgiving_streak(current_user.id),
)
@bp.get("/focus")
@login_required
def focus():
tasks = list_today_tasks(current_user.id, limit=1)
return _render_focus(
tasks[0] if tasks else None,
celebrate=request.args.get("celebrate") == "1",
)
@bp.get("/<int:task_id>/focus")
@login_required
def focus_task(task_id: int):
task = _owned_task_or_404(task_id)
return _render_focus(task)
@bp.route("/new", methods=["GET", "POST"])
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def create():
form = TaskForm()
if form.validate_on_submit():
task = create_task(
current_user.id,
title=form.title.data,
description=form.description.data,
status=form.status.data,
priority=form.priority.data,
due_date=form.due_date.data,
)
flash("Task captured.", "success")
return redirect(url_for("tasks.detail", task_id=task.id))
return render_template("tasks/form.html", form=form, heading="New task")
@bp.get("/<int:task_id>")
@login_required
def detail(task_id: int):
task = _owned_task_or_404(task_id)
return render_template(
"tasks/detail.html",
task=task,
subtask_form=SubtaskForm(),
action_form=ActionForm(),
)
@bp.route("/<int:task_id>/edit", methods=["GET", "POST"])
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def edit(task_id: int):
task = _owned_task_or_404(task_id)
form = TaskForm(obj=task)
if form.validate_on_submit():
update_task(
task,
title=form.title.data,
description=form.description.data,
status=form.status.data,
priority=form.priority.data,
due_date=form.due_date.data,
)
flash("Task updated.", "success")
return redirect(url_for("tasks.detail", task_id=task.id))
return render_template("tasks/form.html", form=form, heading="Edit task")
@bp.post("/<int:task_id>/delete")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def delete(task_id: int):
task = _owned_task_or_404(task_id)
form = ActionForm()
if not form.validate_on_submit():
abort(400)
delete_task(task)
flash("Task deleted.", "info")
return redirect(url_for("tasks.index"))
@bp.post("/<int:task_id>/subtasks")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def create_subtask(task_id: int):
task = _owned_task_or_404(task_id)
form = SubtaskForm()
if form.validate_on_submit():
add_subtask(task, form.title.data)
flash("Small step added.", "success")
else:
for errors in form.errors.values():
for error in errors:
flash(error, "error")
return redirect(url_for("tasks.detail", task_id=task.id))
@bp.post("/<int:task_id>/subtasks/<int:subtask_id>/toggle")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def toggle_subtask_status(task_id: int, subtask_id: int):
task = _owned_task_or_404(task_id)
form = ActionForm()
if not form.validate_on_submit():
abort(400)
try:
toggle_subtask(task, subtask_id)
except TaskNotFoundError:
abort(404)
return redirect(url_for("tasks.detail", task_id=task.id))
@bp.post("/<int:task_id>/focus-settings")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def update_focus_settings(task_id: int):
task = _owned_task_or_404(task_id)
form = FocusSettingsForm()
if form.validate_on_submit():
configure_focus(
task,
timer_minutes=form.timer_minutes.data,
context=form.context.data,
chunk_minutes=form.parsed_chunks(),
)
flash("Focus setup saved.", "success")
else:
for errors in form.errors.values():
for error in errors:
flash(error, "error")
return redirect(url_for("tasks.focus_task", task_id=task.id))
@bp.post("/<int:task_id>/chunks/<int:chunk_index>/toggle")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def toggle_focus_chunk(task_id: int, chunk_index: int):
task = _owned_task_or_404(task_id)
form = ActionForm()
if not form.validate_on_submit():
abort(400)
try:
toggle_chunk_session(task, chunk_index)
except TaskNotFoundError:
abort(404)
return redirect(url_for("tasks.focus_task", task_id=task.id))
@bp.post("/<int:task_id>/complete")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def complete(task_id: int):
task = _owned_task_or_404(task_id)
form = ActionForm()
if not form.validate_on_submit():
abort(400)
completed_title = task.title
complete_task(task)
flash(f"You completed {completed_title}. That counts.", "success")
return redirect(url_for("tasks.focus", celebrate=1))
@bp.get("/transfer")
@login_required
def transfer():
undo_batch = None
raw_undo = request.args.get("undo")
if raw_undo:
try:
undo_batch = UUID(raw_undo)
except ValueError:
abort(400)
return render_template(
"tasks/transfer.html",
markdown_form=MarkdownImportForm(),
backup_form=BackupImportForm(),
action_form=ActionForm(),
undo_batch=undo_batch,
)
@bp.post("/import/markdown")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def import_markdown_file():
form = MarkdownImportForm()
if not form.validate_on_submit():
_flash_form_errors(form)
return redirect(url_for("tasks.transfer"))
try:
count = import_markdown(current_user.id, form.markdown_file.data.read())
except TransferValidationError as exc:
flash(str(exc), "error")
else:
flash(f"Imported {count} tasks.", "success")
return redirect(url_for("tasks.transfer"))
@bp.get("/export/json")
@login_required
def export_json():
content = export_backup(current_user)
return send_file(
BytesIO(content),
mimetype="application/json",
as_attachment=True,
download_name=f"steady-backup-{date.today().isoformat()}.json",
max_age=0,
)
@bp.post("/import/json")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def import_json():
form = BackupImportForm()
if not form.validate_on_submit():
_flash_form_errors(form)
return redirect(url_for("tasks.transfer"))
try:
count = import_backup(
current_user,
form.backup_file.data.read(),
replace_existing=form.replace_existing.data,
)
except TransferValidationError as exc:
flash(str(exc), "error")
else:
action = "Restored" if form.replace_existing.data else "Merged"
flash(f"{action} {count} tasks from backup.", "success")
return redirect(url_for("tasks.transfer"))
@bp.post("/completed/clear")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def clear_completed():
form = ActionForm()
if not form.validate_on_submit():
abort(400)
batch_id, count = clear_completed_tasks(current_user.id)
if batch_id is None:
flash("There are no completed tasks to clear.", "info")
return redirect(url_for("tasks.transfer"))
flash(f"Cleared {count} completed tasks. You can undo this below.", "info")
return redirect(url_for("tasks.transfer", undo=batch_id))
@bp.post("/completed/undo/<uuid:batch_id>")
@login_required
@roles_required(*TASK_EDITOR_ROLES)
def undo_completed_clear(batch_id):
form = ActionForm()
if not form.validate_on_submit():
abort(400)
count = undo_clear_completed(current_user.id, str(batch_id))
if count:
flash(f"Restored {count} completed tasks.", "success")
else:
flash("That clear operation is no longer available to undo.", "info")
return redirect(url_for("tasks.transfer"))
def _flash_form_errors(form) -> None:
for errors in form.errors.values():
for error in errors:
flash(error, "error")