587 lines
35 KiB
Python
587 lines
35 KiB
Python
"""Роуты проектов: каталог, карточка, участники, лента, файлы, канбан."""
|
||
from datetime import datetime, timezone
|
||
from flask import render_template, request, redirect, url_for, session, jsonify
|
||
from sqlmodel import Session, select
|
||
from ..db import engine
|
||
from ..models import (
|
||
Project, ProjectMember, ProjectPost, ProjectPostAttachment,
|
||
ProjectSubscription, ProjectTask, PortalFile, User,
|
||
)
|
||
from ..helpers import (
|
||
login_required, require_role, project_permissions,
|
||
normalize_tags, get_all_tags, resolve_gitea_repo,
|
||
_project_subscription, _project_subscriber_ids,
|
||
_save_uploaded_files, notify_user, notify_users,
|
||
PROJECT_ROLE_RANK, TASK_STATUSES, TASK_STATUS_LABELS, TASK_PRIORITIES,
|
||
)
|
||
from .. import gitea_api
|
||
|
||
|
||
def register(app):
|
||
|
||
@app.route("/projects")
|
||
def projects_list():
|
||
user_role = session.get("role")
|
||
user_id = session.get("user_id")
|
||
current_tag = (request.args.get("tag") or "").strip().lower()
|
||
current_status = (request.args.get("status") or "").strip()
|
||
with Session(engine) as db_session:
|
||
all_projects = db_session.exec(select(Project).order_by(Project.created_at.desc())).all()
|
||
member_map = {}
|
||
if user_id:
|
||
for m in db_session.exec(select(ProjectMember).where(ProjectMember.user_id == user_id)).all():
|
||
member_map[m.project_id] = m.role
|
||
all_tags = get_all_tags(db_session)
|
||
filtered = []
|
||
for p in all_projects:
|
||
if p.visibility == "public": filtered.append(p)
|
||
elif p.visibility == "internal" and user_id: filtered.append(p)
|
||
elif p.visibility == "private" and user_id and (
|
||
p.owner_id == user_id or p.id in member_map or user_role == "admin"
|
||
): filtered.append(p)
|
||
if current_status:
|
||
filtered = [p for p in filtered if p.status == current_status]
|
||
if current_tag:
|
||
filtered = [p for p in filtered
|
||
if p.tags and current_tag in [t.strip().lower() for t in p.tags.split(",")]]
|
||
return render_template("projects.html", projects=filtered, role=user_role, user_id=user_id,
|
||
member_map=member_map, all_tags=all_tags,
|
||
current_tag=current_tag, current_status=current_status)
|
||
|
||
@app.route("/projects/new", methods=["GET", "POST"])
|
||
@require_role(["admin", "professor", "lead"])
|
||
def project_new():
|
||
if request.method == "POST":
|
||
with Session(engine) as db_session:
|
||
new_proj = Project(
|
||
title=request.form.get("title"),
|
||
description=request.form.get("description"),
|
||
status=request.form.get("status"),
|
||
visibility=request.form.get("visibility"),
|
||
gitea_url=(request.form.get("gitea_url") or "").strip() or None,
|
||
gitea_repo=resolve_gitea_repo(request.form),
|
||
tags=normalize_tags(request.form.get("tags")),
|
||
start_date=(request.form.get("start_date") or "").strip() or None,
|
||
end_date=(request.form.get("end_date") or "").strip() or None,
|
||
contact_person=(request.form.get("contact_person") or "").strip() or None,
|
||
owner_id=session.get("user_id"))
|
||
db_session.add(new_proj); db_session.flush()
|
||
proj_id = new_proj.id
|
||
db_session.add(ProjectMember(
|
||
project_id=proj_id, user_id=session.get("user_id"),
|
||
role="manager", can_edit_files=True, can_post_feed=True,
|
||
can_edit_any_post=True, can_manage_members=True))
|
||
db_session.commit()
|
||
return redirect(url_for("project_detail", project_id=proj_id))
|
||
return render_template("project_form.html", project=None, is_edit=False)
|
||
|
||
@app.route("/projects/<int:project_id>")
|
||
def project_detail(project_id):
|
||
user_id = session.get("user_id")
|
||
user_role = session.get("role")
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = None
|
||
if user_id:
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_id)).first()
|
||
perms = project_permissions(proj, user_id, user_role, membership)
|
||
if proj.visibility == "private" and not perms["is_admin"] and not perms["is_owner"] and not membership:
|
||
return "403: Это приватный проект", 403
|
||
if proj.visibility == "internal" and not user_id:
|
||
return redirect(url_for("login"))
|
||
members = db_session.exec(
|
||
select(ProjectMember, User).join(User, ProjectMember.user_id == User.id)
|
||
.where(ProjectMember.project_id == project_id).order_by(User.username)).all()
|
||
files = db_session.exec(
|
||
select(PortalFile, User.username).join(User, PortalFile.uploaded_by == User.id)
|
||
.where(PortalFile.project_id == project_id)
|
||
.order_by(PortalFile.uploaded_at.desc())).all()
|
||
task_counts = {s: 0 for s in TASK_STATUSES}
|
||
for t in db_session.exec(select(ProjectTask).where(ProjectTask.project_id == project_id)).all():
|
||
if t.status in task_counts: task_counts[t.status] += 1
|
||
posts = db_session.exec(
|
||
select(ProjectPost).where(ProjectPost.project_id == project_id)
|
||
.order_by(ProjectPost.is_pinned.desc(), ProjectPost.created_at.desc())).all()
|
||
post_ids = [p.id for p in posts]
|
||
attachments_by_post = {}
|
||
if post_ids:
|
||
for att in db_session.exec(select(ProjectPostAttachment)
|
||
.where(ProjectPostAttachment.post_id.in_(post_ids))).all():
|
||
attachments_by_post.setdefault(att.post_id, []).append(att)
|
||
file_ids = [a.file_id for arr in attachments_by_post.values() for a in arr]
|
||
files_map = {}
|
||
if file_ids:
|
||
for f in db_session.exec(select(PortalFile).where(PortalFile.id.in_(file_ids))).all():
|
||
files_map[f.id] = f
|
||
author_ids = {p.author_id for p in posts}
|
||
authors_map = {}
|
||
if author_ids:
|
||
for u in db_session.exec(select(User).where(User.id.in_(author_ids))).all():
|
||
authors_map[u.id] = u
|
||
my_sub = _project_subscription(db_session, project_id, user_id)
|
||
return render_template("project_detail.html", project=proj, members=members, files=files,
|
||
perms=perms, user_id=user_id, role=user_role,
|
||
active_tab="overview", task_counts=task_counts,
|
||
posts=posts, attachments_by_post=attachments_by_post,
|
||
files_map=files_map, authors_map=authors_map, my_sub=my_sub)
|
||
|
||
@app.route("/projects/<int:project_id>/gitea-stats")
|
||
def project_gitea_stats(project_id):
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return jsonify({"ok": False, "error": "not_found"}), 404
|
||
membership = None
|
||
if session.get("user_id"):
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if proj.visibility == "private" and not perms["is_admin"] and not perms["is_owner"] and not membership:
|
||
return jsonify({"ok": False, "error": "forbidden"}), 403
|
||
repo = proj.gitea_repo or (gitea_api.extract_repo_from_url(proj.gitea_url) if proj.gitea_url else None)
|
||
if not repo:
|
||
return jsonify({"ok": False, "error": "no_repo",
|
||
"message": "У проекта не указан репозиторий."})
|
||
return jsonify(gitea_api.get_project_stats(repo))
|
||
|
||
@app.route("/projects/<int:project_id>/edit", methods=["GET", "POST"])
|
||
@login_required
|
||
def project_edit(project_id):
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_manage_members"] and not perms["is_admin"] and not perms["is_owner"]:
|
||
return "403: Нет прав", 403
|
||
if request.method == "POST":
|
||
proj.title = request.form.get("title")
|
||
proj.description = request.form.get("description")
|
||
proj.status = request.form.get("status")
|
||
proj.visibility = request.form.get("visibility")
|
||
proj.gitea_url = (request.form.get("gitea_url") or "").strip() or None
|
||
proj.gitea_repo = resolve_gitea_repo(request.form, proj.gitea_url, proj.gitea_repo)
|
||
proj.tags = normalize_tags(request.form.get("tags"))
|
||
proj.start_date = (request.form.get("start_date") or "").strip() or None
|
||
proj.end_date = (request.form.get("end_date") or "").strip() or None
|
||
proj.contact_person = (request.form.get("contact_person") or "").strip() or None
|
||
db_session.add(proj); db_session.commit()
|
||
return redirect(url_for("project_detail", project_id=proj.id))
|
||
return render_template("project_form.html", project=proj, is_edit=True)
|
||
|
||
@app.route("/projects/<int:project_id>/members/add", methods=["POST"])
|
||
@login_required
|
||
def project_add_member(project_id):
|
||
username = (request.form.get("username") or "").strip()
|
||
role = request.form.get("role") or "viewer"
|
||
if role not in PROJECT_ROLE_RANK: role = "viewer"
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_manage_members"]: return "403: Нет прав", 403
|
||
user_to_add = db_session.exec(select(User).where(User.username == username)).first()
|
||
if not user_to_add: return redirect(url_for("project_detail", project_id=project_id))
|
||
existing = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_to_add.id)).first()
|
||
if not existing:
|
||
is_mgr = (role == "manager")
|
||
is_ed = (role in ("editor", "manager"))
|
||
db_session.add(ProjectMember(project_id=project_id, user_id=user_to_add.id, role=role,
|
||
can_edit_files=is_ed, can_post_feed=is_ed,
|
||
can_edit_any_post=is_mgr, can_manage_members=is_mgr))
|
||
db_session.commit()
|
||
notify_user(user_to_add.id, f"Вас добавили в «{proj.title}»",
|
||
f"Роль: {role}",
|
||
link_url=url_for("project_detail", project_id=project_id),
|
||
kind="info")
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/members/<int:user_id>/permissions", methods=["POST"])
|
||
@login_required
|
||
def project_member_permissions(project_id, user_id):
|
||
with Session(engine) as s:
|
||
proj = s.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_manage_members"]: return "403: Нет прав", 403
|
||
if user_id == proj.owner_id: return "Нельзя менять права владельца", 400
|
||
target = s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_id)).first()
|
||
if target:
|
||
target.can_view_files = request.form.get("can_view_files") == "on"
|
||
target.can_edit_files = request.form.get("can_edit_files") == "on"
|
||
target.can_view_repo = request.form.get("can_view_repo") == "on"
|
||
target.can_post_feed = request.form.get("can_post_feed") == "on"
|
||
target.can_edit_own_posts = request.form.get("can_edit_own_posts") == "on"
|
||
target.can_edit_any_post = request.form.get("can_edit_any_post") == "on"
|
||
target.can_manage_members = request.form.get("can_manage_members") == "on"
|
||
if target.can_manage_members: target.role = "manager"
|
||
elif target.can_edit_files or target.can_post_feed: target.role = "editor"
|
||
else: target.role = "viewer"
|
||
s.add(target); s.commit()
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/members/<int:user_id>/remove", methods=["POST"])
|
||
@login_required
|
||
def project_remove_member(project_id, user_id):
|
||
with Session(engine) as s:
|
||
proj = s.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
my_membership = s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), my_membership)
|
||
if not perms["can_manage_members"]: return "403: Нет прав", 403
|
||
if user_id == proj.owner_id: return "Нельзя удалить владельца", 400
|
||
to_remove = s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_id)).first()
|
||
if to_remove: s.delete(to_remove); s.commit()
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/subscribe", methods=["POST"])
|
||
@login_required
|
||
def project_subscribe_toggle(project_id):
|
||
user_id = session.get("user_id")
|
||
with Session(engine) as s:
|
||
existing = _project_subscription(s, project_id, user_id)
|
||
if existing: s.delete(existing); s.commit()
|
||
else:
|
||
s.add(ProjectSubscription(project_id=project_id, user_id=user_id))
|
||
s.commit()
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/posts/new", methods=["POST"])
|
||
@login_required
|
||
def project_post_new(project_id):
|
||
user_id = session.get("user_id")
|
||
post_id = None
|
||
with Session(engine) as s:
|
||
proj = s.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_id)).first()
|
||
perms = project_permissions(proj, user_id, session.get("role"), membership)
|
||
if not perms["can_post_feed"]: return "403: Нет прав", 403
|
||
title = (request.form.get("title") or "").strip()
|
||
if not title: return redirect(url_for("project_detail", project_id=project_id))
|
||
post = ProjectPost(project_id=project_id, author_id=user_id, title=title,
|
||
body=(request.form.get("body") or "").strip() or None,
|
||
is_pinned=bool(request.form.get("is_pinned")))
|
||
s.add(post); s.flush()
|
||
post_id = post.id; s.commit()
|
||
proj_title = proj.title
|
||
files = request.files.getlist("files") or []
|
||
saved_ids = _save_uploaded_files(files, user_id, project_id=project_id,
|
||
upload_folder=app.config['UPLOAD_FOLDER'])
|
||
if saved_ids:
|
||
with Session(engine) as s2:
|
||
for fid in saved_ids:
|
||
s2.add(ProjectPostAttachment(post_id=post_id, file_id=fid))
|
||
s2.commit()
|
||
with Session(engine) as s3:
|
||
sub_ids = _project_subscriber_ids(s3, project_id)
|
||
recipients = [uid for uid in sub_ids if uid != user_id]
|
||
if recipients:
|
||
notify_users(recipients, f"Новый пост в «{proj_title}»",
|
||
request.form.get("title", ""),
|
||
link_url=url_for("project_detail", project_id=project_id), kind="info")
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/posts/<int:post_id>/edit", methods=["POST"])
|
||
@login_required
|
||
def project_post_edit(project_id, post_id):
|
||
user_id = session.get("user_id")
|
||
with Session(engine) as s:
|
||
proj = s.get(Project, project_id)
|
||
post = s.get(ProjectPost, post_id)
|
||
if not proj or not post or post.project_id != project_id: return "Не найдено", 404
|
||
membership = s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_id)).first()
|
||
perms = project_permissions(proj, user_id, session.get("role"), membership)
|
||
can_edit = perms["can_edit_any_post"] or (perms["can_edit_own_posts"] and post.author_id == user_id)
|
||
if not can_edit: return "403: Нет прав", 403
|
||
post.title = (request.form.get("title") or post.title).strip()
|
||
post.body = (request.form.get("body") or "").strip() or None
|
||
post.is_pinned = bool(request.form.get("is_pinned"))
|
||
post.updated_at = datetime.now(timezone.utc)
|
||
s.add(post); s.commit()
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/posts/<int:post_id>/delete", methods=["POST"])
|
||
@login_required
|
||
def project_post_delete(project_id, post_id):
|
||
user_id = session.get("user_id")
|
||
with Session(engine) as s:
|
||
proj = s.get(Project, project_id)
|
||
post = s.get(ProjectPost, post_id)
|
||
if not proj or not post or post.project_id != project_id: return "Не найдено", 404
|
||
membership = s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_id)).first()
|
||
perms = project_permissions(proj, user_id, session.get("role"), membership)
|
||
can_delete = (perms["is_admin"] or perms["is_owner"] or perms["can_edit_any_post"]
|
||
or (perms["can_edit_own_posts"] and post.author_id == user_id))
|
||
if not can_delete: return "403: Нет прав", 403
|
||
for att in s.exec(select(ProjectPostAttachment)
|
||
.where(ProjectPostAttachment.post_id == post_id)).all():
|
||
s.delete(att)
|
||
s.delete(post); s.commit()
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/delete", methods=["POST"])
|
||
@login_required
|
||
def project_delete(project_id):
|
||
with Session(engine) as s:
|
||
proj = s.get(Project, project_id)
|
||
if not proj: return redirect(url_for("projects_list"))
|
||
if not (session.get("role") == "admin" or proj.owner_id == session.get("user_id")):
|
||
return "403: Только владелец или админ", 403
|
||
try:
|
||
for f in s.exec(select(PortalFile).where(PortalFile.project_id == project_id)).all():
|
||
f.project_id = None; s.add(f)
|
||
s.flush()
|
||
for t in s.exec(select(ProjectTask).where(ProjectTask.project_id == project_id)).all():
|
||
s.delete(t)
|
||
s.flush()
|
||
for p in s.exec(select(ProjectPost).where(ProjectPost.project_id == project_id)).all():
|
||
for a in s.exec(select(ProjectPostAttachment)
|
||
.where(ProjectPostAttachment.post_id == p.id)).all():
|
||
s.delete(a)
|
||
s.delete(p)
|
||
for sub in s.exec(select(ProjectSubscription)
|
||
.where(ProjectSubscription.project_id == project_id)).all():
|
||
s.delete(sub)
|
||
for m in s.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)).all():
|
||
s.delete(m)
|
||
s.flush()
|
||
s.delete(proj); s.commit()
|
||
except Exception as e:
|
||
s.rollback()
|
||
print(f"[project_delete] {e}", flush=True)
|
||
return f"Ошибка: {e}", 500
|
||
return redirect(url_for("projects_list"))
|
||
|
||
@app.route("/projects/<int:project_id>/files/upload", methods=["POST"])
|
||
@login_required
|
||
def project_file_upload(project_id):
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_upload"]: return "403: Нет прав", 403
|
||
try:
|
||
file = request.files.get('file')
|
||
except Exception:
|
||
return "Ошибка разбора", 400
|
||
if not file or file.filename == '':
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
saved = _save_uploaded_files([file], session.get("user_id"),
|
||
project_id=project_id,
|
||
upload_folder=app.config['UPLOAD_FOLDER'])
|
||
if not saved: return "Ошибка сохранения", 500
|
||
return redirect(url_for("project_detail", project_id=project_id))
|
||
|
||
# ─── КАНБАН ───────────────────────────────────────────────────────
|
||
|
||
@app.route("/projects/<int:project_id>/board")
|
||
def project_board(project_id):
|
||
user_id = session.get("user_id")
|
||
user_role = session.get("role")
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = None
|
||
if user_id:
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == user_id)).first()
|
||
perms = project_permissions(proj, user_id, user_role, membership)
|
||
if proj.visibility == "private" and not perms["is_admin"] and not perms["is_owner"] and not membership:
|
||
return "403", 403
|
||
if proj.visibility == "internal" and not user_id: return redirect(url_for("login"))
|
||
members = db_session.exec(
|
||
select(ProjectMember, User).join(User, ProjectMember.user_id == User.id)
|
||
.where(ProjectMember.project_id == project_id).order_by(User.username)).all()
|
||
tasks = db_session.exec(
|
||
select(ProjectTask).where(ProjectTask.project_id == project_id)
|
||
.order_by(ProjectTask.position, ProjectTask.id)).all()
|
||
user_ids = set()
|
||
for t in tasks:
|
||
if t.assignee_id: user_ids.add(t.assignee_id)
|
||
if t.created_by: user_ids.add(t.created_by)
|
||
users_map = {}
|
||
if user_ids:
|
||
for u in db_session.exec(select(User).where(User.id.in_(user_ids))).all():
|
||
users_map[u.id] = u
|
||
columns = {s: [] for s in TASK_STATUSES}
|
||
for t in tasks:
|
||
if t.status in columns:
|
||
columns[t.status].append({
|
||
"task": t,
|
||
"assignee": users_map.get(t.assignee_id) if t.assignee_id else None,
|
||
"author": users_map.get(t.created_by)})
|
||
task_counts = {s: len(columns[s]) for s in TASK_STATUSES}
|
||
return render_template("project_board.html", project=proj, members=members,
|
||
columns=columns, perms=perms, user_id=user_id, role=user_role,
|
||
active_tab="board", task_counts=task_counts,
|
||
TASK_STATUSES=TASK_STATUSES,
|
||
TASK_STATUS_LABELS=TASK_STATUS_LABELS,
|
||
TASK_PRIORITIES=TASK_PRIORITIES)
|
||
|
||
@app.route("/projects/<int:project_id>/tasks/new", methods=["POST"])
|
||
@login_required
|
||
def task_new(project_id):
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_edit"]: return "403", 403
|
||
title = (request.form.get("title") or "").strip()
|
||
if not title: return redirect(url_for("project_board", project_id=project_id))
|
||
status = request.form.get("status") or "backlog"
|
||
if status not in TASK_STATUSES: status = "backlog"
|
||
priority = request.form.get("priority") or "medium"
|
||
if priority not in TASK_PRIORITIES: priority = "medium"
|
||
aid = request.form.get("assignee_id")
|
||
assignee_id = int(aid) if aid and aid.isdigit() else None
|
||
max_pos = db_session.exec(select(ProjectTask)
|
||
.where(ProjectTask.project_id == project_id)
|
||
.where(ProjectTask.status == status)).all()
|
||
position = max([t.position for t in max_pos], default=-1) + 1
|
||
task = ProjectTask(project_id=project_id, title=title,
|
||
description=(request.form.get("description") or "").strip() or None,
|
||
status=status, priority=priority, assignee_id=assignee_id,
|
||
created_by=session.get("user_id"),
|
||
due_date=(request.form.get("due_date") or "").strip() or None,
|
||
position=position)
|
||
db_session.add(task); db_session.commit()
|
||
if assignee_id and assignee_id != session.get("user_id"):
|
||
notify_user(assignee_id, f"Задача: {title}", f"Проект: {proj.title}",
|
||
link_url=url_for("project_board", project_id=project_id),
|
||
kind="info")
|
||
return redirect(url_for("project_board", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/tasks/<int:task_id>/edit", methods=["GET", "POST"])
|
||
@login_required
|
||
def task_edit(project_id, task_id):
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_edit"]: return "403", 403
|
||
task = db_session.get(ProjectTask, task_id)
|
||
if not task or task.project_id != project_id: return "Не найдено", 404
|
||
if request.method == "POST":
|
||
old_aid = task.assignee_id
|
||
task.title = (request.form.get("title") or task.title).strip()
|
||
task.description = (request.form.get("description") or "").strip() or None
|
||
new_status = request.form.get("status") or task.status
|
||
if new_status in TASK_STATUSES: task.status = new_status
|
||
new_priority = request.form.get("priority") or task.priority
|
||
if new_priority in TASK_PRIORITIES: task.priority = new_priority
|
||
aid = request.form.get("assignee_id")
|
||
new_aid = int(aid) if aid and aid.isdigit() else None
|
||
task.assignee_id = new_aid
|
||
task.due_date = (request.form.get("due_date") or "").strip() or None
|
||
task.updated_at = datetime.now(timezone.utc)
|
||
db_session.add(task); db_session.commit()
|
||
if new_aid and new_aid != old_aid and new_aid != session.get("user_id"):
|
||
notify_user(new_aid, f"Задача: {task.title}", f"Проект: {proj.title}",
|
||
link_url=url_for("project_board", project_id=project_id),
|
||
kind="info")
|
||
return redirect(url_for("project_board", project_id=project_id))
|
||
members = db_session.exec(
|
||
select(ProjectMember, User).join(User, ProjectMember.user_id == User.id)
|
||
.where(ProjectMember.project_id == project_id).order_by(User.username)).all()
|
||
return render_template("task_edit.html", project=proj, task=task, members=members,
|
||
TASK_STATUSES=TASK_STATUSES,
|
||
TASK_STATUS_LABELS=TASK_STATUS_LABELS,
|
||
TASK_PRIORITIES=TASK_PRIORITIES)
|
||
|
||
@app.route("/projects/<int:project_id>/tasks/<int:task_id>/delete", methods=["POST"])
|
||
@login_required
|
||
def task_delete(project_id, task_id):
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return "Проект не найден", 404
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_manage_members"] and not perms["is_admin"] and not perms["is_owner"]:
|
||
return "403", 403
|
||
task = db_session.get(ProjectTask, task_id)
|
||
if task and task.project_id == project_id:
|
||
db_session.delete(task); db_session.commit()
|
||
return redirect(url_for("project_board", project_id=project_id))
|
||
|
||
@app.route("/projects/<int:project_id>/tasks/<int:task_id>/move", methods=["POST"])
|
||
@login_required
|
||
def task_move(project_id, task_id):
|
||
data = request.get_json(silent=True) or {}
|
||
new_status = data.get("status")
|
||
new_position = data.get("position", 0)
|
||
if new_status not in TASK_STATUSES:
|
||
return jsonify({"error": "invalid status"}), 400
|
||
with Session(engine) as db_session:
|
||
proj = db_session.get(Project, project_id)
|
||
if not proj: return jsonify({"error": "not found"}), 404
|
||
membership = db_session.exec(select(ProjectMember)
|
||
.where(ProjectMember.project_id == project_id)
|
||
.where(ProjectMember.user_id == session.get("user_id"))).first()
|
||
perms = project_permissions(proj, session.get("user_id"), session.get("role"), membership)
|
||
if not perms["can_edit"]: return jsonify({"error": "no permission"}), 403
|
||
task = db_session.get(ProjectTask, task_id)
|
||
if not task or task.project_id != project_id:
|
||
return jsonify({"error": "not found"}), 404
|
||
old_status = task.status
|
||
siblings = db_session.exec(
|
||
select(ProjectTask).where(ProjectTask.project_id == project_id)
|
||
.where(ProjectTask.status == new_status)
|
||
.where(ProjectTask.id != task_id)
|
||
.order_by(ProjectTask.position, ProjectTask.id)).all()
|
||
siblings.insert(max(0, min(int(new_position), len(siblings))), task)
|
||
for i, t in enumerate(siblings):
|
||
t.position = i
|
||
if t.id == task_id:
|
||
t.status = new_status
|
||
t.updated_at = datetime.now(timezone.utc)
|
||
db_session.add(t)
|
||
db_session.flush()
|
||
if old_status != new_status:
|
||
olds = db_session.exec(
|
||
select(ProjectTask).where(ProjectTask.project_id == project_id)
|
||
.where(ProjectTask.status == old_status)
|
||
.order_by(ProjectTask.position, ProjectTask.id)).all()
|
||
for i, t in enumerate(olds):
|
||
t.position = i; db_session.add(t)
|
||
db_session.commit()
|
||
return jsonify({"ok": True})
|