intent/intent/projects/views.py
Alec Perkins b9378b8397 Add projects and features with revision history (Phase 2)
Projects under organizations with fixed URL name and feature ID
prefix, three visibility levels, open contribution, per-project
roles, settings and typed-confirmation delete. Hidden projects 404.
Leaving an org drops its project roles. Project access rules live in
intent/access.py alongside org access.

Features numbered per project (INT-12) in a tree. Every change is a
revision with a required note; history, old revisions, and
side-by-side field and line diffs between any two. Cycles and
superseded-without-replacement are rejected. Markdown renders safely
with source-line attributes for Phase 3 line comments and headings
shifted below the page h1. Server-side preview works without JS.

Structural accessibility check runs on every page in tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 07:58:16 -04:00

118 lines
4.6 KiB
Python

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied, ValidationError
from django.shortcuts import get_object_or_404, redirect, render
from intent.access import org_access
from intent.features.services import build_tree
from intent.orgs.models import Organization
from . import services
from .decorators import project_view
from .forms import (
AddMemberForm,
MemberRoleForm,
ProjectCreateForm,
ProjectDeleteForm,
ProjectSettingsForm,
)
def _project_redirect(project, name="home"):
return redirect(f"projects:{name}", org_slug=project.org.slug, project_slug=project.slug)
@login_required
def project_create(request, org_slug):
org = get_object_or_404(Organization, slug=org_slug)
if not org_access(request.user, org).can_create_projects:
raise PermissionDenied
form = ProjectCreateForm(request.POST or None)
if request.method == "POST" and form.is_valid():
try:
project = services.create_project(org, request.user, **form.cleaned_data)
except ValidationError as exc:
form.add_error(None, exc)
else:
messages.success(request, f"Created {project.name}.")
return _project_redirect(project)
return render(request, "projects/new.html", {"org": org, "form": form})
@project_view()
def project_home(request, project, access):
features = list(project.features.select_related("project"))
context = {"project": project, "access": access, "tree": build_tree(features)}
return render(request, "projects/home.html", context)
@project_view(require="can_admin")
def project_settings(request, project, access):
form = ProjectSettingsForm(request.POST or None, instance=project)
if request.method == "POST" and form.is_valid():
services.update_settings(project, request.user, **form.cleaned_data)
messages.success(request, "Settings saved.")
return _project_redirect(project, "settings")
return render(
request, "projects/settings.html", {"project": project, "access": access, "form": form}
)
@project_view(require="can_admin")
def project_delete(request, project, access):
form = ProjectDeleteForm(request.POST or None, project=project)
if request.method == "POST" and form.is_valid():
services.delete_project(project, request.user)
messages.success(request, f"Deleted {project.name}.")
return redirect("orgs:detail", org_slug=project.org.slug)
return render(
request, "projects/delete.html", {"project": project, "access": access, "form": form}
)
@project_view(require="can_edit")
def project_members(request, project, access):
form = AddMemberForm(request.POST or None, project=project)
if request.method == "POST":
if not access.can_admin:
raise PermissionDenied
if form.is_valid():
try:
membership = services.add_member(project, request.user, **form.cleaned_data)
except ValidationError as exc:
form.add_error(None, exc)
else:
messages.success(request, f"Added {membership.user.username}.")
return _project_redirect(project, "members")
context = {
"project": project,
"access": access,
"memberships": project.memberships.select_related("user"),
"form": form,
}
return render(request, "projects/members.html", context)
@project_view(require="can_edit")
def project_member_update(request, project, access, membership_id):
if request.method != "POST":
return _project_redirect(project, "members")
membership = get_object_or_404(project.memberships.select_related("user"), pk=membership_id)
form = MemberRoleForm(request.POST)
if form.is_valid():
services.update_member(membership, request.user, role=form.cleaned_data["role"])
messages.success(request, f"Updated {membership.user.username}.")
return _project_redirect(project, "members")
@project_view(require="can_edit")
def project_member_remove(request, project, access, membership_id):
if request.method != "POST":
return _project_redirect(project, "members")
membership = get_object_or_404(project.memberships.select_related("user"), pk=membership_id)
services.remove_member(membership, request.user)
if membership.user_id == request.user.pk:
messages.success(request, f"You left {project.name}.")
return redirect("orgs:detail", org_slug=project.org.slug)
messages.success(request, f"Removed {membership.user.username}.")
return _project_redirect(project, "members")