Update
DevPlace CI / test (push) Has been cancelled

This commit is contained in:
2026-08-10 00:23:20 +02:00
parent 2bdcf6528f
commit 6cac64a3f6
63 changed files with 3499 additions and 68 deletions
+46 -1
View File
@@ -8,16 +8,24 @@ from fastapi.responses import JSONResponse
from devplacepy.database import db, get_table, get_users_by_uids
from devplacepy.dependencies import json_or_form
from devplacepy.models import (
EditorPrefsForm,
WorkspaceFlagForm,
WorkspaceQuotaForm,
WorkspaceSuspendForm,
)
from devplacepy.responses import action_result, json_error, respond
from devplacepy.routers.admin._shared import deny_senior, is_senior_admin
from devplacepy.schemas import AdminWorkspacesOut
from devplacepy.seo import base_seo_context
from devplacepy.services.audit import record as audit
from devplacepy.services.containers import store
from devplacepy.services.containers.workspace import flags, provision, quota, tunnels
from devplacepy.services.containers.workspace import (
editor,
flags,
provision,
quota,
tunnels,
)
from devplacepy.utils import create_notification, generate_uid, not_found, require_admin
router = APIRouter()
@@ -237,6 +245,43 @@ async def admin_flag_resolve(request: Request, flag_uid: str, status: str = "res
return action_result(request, "/admin/workspaces")
@router.post("/workspaces/{uid}/editor")
async def admin_workspace_editor(
request: Request,
uid: str,
data: Annotated[EditorPrefsForm, Depends(json_or_form(EditorPrefsForm))],
):
admin = require_admin(request)
if not isinstance(admin, dict):
return admin
instance = _instance_or_404(uid)
owner_uid = instance.get("workspace_owner_uid", "")
owner = get_users_by_uids([owner_uid]).get(owner_uid) if owner_uid else None
if is_senior_admin(admin, owner):
return deny_senior(
request,
admin,
owner_uid,
owner,
"container.workspace.editor.update",
"/admin/workspaces",
)
if not owner_uid:
return json_error(400, "this workspace has no owner")
if data.reset:
editor.reset_prefs(owner_uid, admin["uid"])
else:
editor.save_prefs(
owner_uid, data.model_dump(exclude={"reset"}, exclude_unset=True)
)
_audit(request, admin, "container.workspace.editor.update", instance)
return action_result(
request,
"/admin/workspaces",
data={"editor": editor.view(owner_uid, instance)},
)
@router.post("/workspaces/quota")
async def admin_workspace_quota(
request: Request,
+6
View File
@@ -58,6 +58,12 @@ DOCS_PAGES = [
"kind": "prose",
"section": SECTION_GENERAL,
},
{
"slug": "workspace-editor",
"title": "The workspace editor",
"kind": "prose",
"section": SECTION_GENERAL,
},
{
"slug": "feed",
"title": "The feed",
@@ -2,7 +2,7 @@
from typing import Annotated
from fastapi import APIRouter, Form, Request, WebSocket
from fastapi import APIRouter, Depends, Form, Request, WebSocket
from starlette.responses import Response
from devplacepy.content import (
@@ -10,13 +10,14 @@ from devplacepy.content import (
can_open_workspace,
)
from devplacepy.database import get_table, resolve_by_slug
from devplacepy.models import TunnelForm
from devplacepy.dependencies import json_or_form
from devplacepy.models import EditorPrefsForm, TunnelForm
from devplacepy.responses import action_result, json_error, respond
from devplacepy.schemas import WorkspaceOut
from devplacepy.services.audit import record as audit
from devplacepy.services.containers import activity, api, forward, store
from devplacepy.services.containers.api import ContainerError
from devplacepy.services.containers.workspace import provision, quota, tunnels
from devplacepy.services.containers.workspace import editor, provision, quota, tunnels
from devplacepy.services.containers.workspace.provision import WorkspaceError
from devplacepy.utils import not_found, require_user
@@ -25,6 +26,12 @@ from ._shared import audit_instance, fail
router = APIRouter()
def _restart_required(instance: dict, profile: editor.EditorProfile) -> bool:
if instance.get("status") != store.ST_RUNNING:
return False
return editor.restart_required(instance, profile)
def _project_or_404(slug: str) -> dict:
project = resolve_by_slug(get_table("projects"), slug)
if not project:
@@ -64,6 +71,7 @@ async def workspace_page(request: Request, slug: str):
_guard(request, project, user, "container.workspace.open")
instance = provision.find_for_project(project["uid"], user["uid"])
limits = quota.resolve(user["uid"])
profile = editor.resolve(user["uid"], instance)
context = {
"project": project,
"workspace": provision.view(instance) if instance else None,
@@ -79,6 +87,10 @@ async def workspace_page(request: Request, slug: str):
"editor_password": (
api.ensure_editor_password(instance) if instance else ""
),
"editor": editor.view(user["uid"], instance),
"restart_required": (
_restart_required(instance, profile) if instance else False
),
"user": user,
}
return respond(request, "workspace.html", context, model=WorkspaceOut)
@@ -151,6 +163,63 @@ async def workspace_delete(request: Request, slug: str):
return action_result(request, f"/projects/{slug}/workspace")
@router.get("/{slug}/workspace/editor")
async def editor_prefs_read(request: Request, slug: str):
user = require_user(request)
if isinstance(user, Response):
return user
project = _project_or_404(slug)
instance = _workspace_or_404(project, user)
if not can_manage_workspace(instance, project, user):
return json_error(403, "Not allowed to manage this workspace")
profile = editor.resolve(user["uid"], instance)
return {
"editor": editor.view(user["uid"], instance),
"restart_required": _restart_required(instance, profile),
}
@router.post("/{slug}/workspace/editor")
async def editor_prefs_write(
request: Request,
slug: str,
data: Annotated[EditorPrefsForm, Depends(json_or_form(EditorPrefsForm))],
):
user = require_user(request)
if isinstance(user, Response):
return user
project = _project_or_404(slug)
instance = _workspace_or_404(project, user)
if not can_manage_workspace(instance, project, user):
return json_error(403, "Not allowed to manage this workspace")
owner_uid = instance.get("workspace_owner_uid") or user["uid"]
if data.reset:
editor.reset_prefs(owner_uid, user["uid"])
summary = f"{user['username']} reset their workspace editor preferences"
else:
editor.save_prefs(
owner_uid, data.model_dump(exclude={"reset"}, exclude_unset=True)
)
summary = f"{user['username']} updated their workspace editor preferences"
audit_instance(
request,
user,
"container.workspace.editor.update",
instance,
project,
summary=summary,
)
profile = editor.resolve(owner_uid, instance)
return action_result(
request,
f"/projects/{slug}/workspace",
data={
"editor": editor.view(owner_uid, instance),
"restart_required": _restart_required(instance, profile),
},
)
@router.get("/{slug}/workspace/tunnels")
async def tunnel_list(request: Request, slug: str):
user = require_user(request)
+20 -7
View File
@@ -176,17 +176,24 @@ async def projects_page(
model=ProjectsOut,
)
def _editor_url(project: dict, user: dict) -> str:
def _editor_launch(project: dict, user: dict) -> dict:
from devplacepy.services.containers import store
from devplacepy.services.containers.workspace import provision
from devplacepy.services.containers.workspace import editor, provision
blank = {"url": "", "mode": "tab", "width": 0, "height": 0}
instance = provision.find_for_project(project["uid"], user["uid"])
if not instance or instance.get("suspended_at"):
return ""
return blank
if instance.get("status") != store.ST_RUNNING:
return ""
return blank
slug = project["slug"] or project["uid"]
return f"/projects/{slug}/containers/instances/{instance['uid']}/code/"
profile = editor.resolve(user["uid"], instance)
return {
"url": f"/projects/{slug}/containers/instances/{instance['uid']}/code/",
"mode": profile.window_mode,
"width": profile.window_width,
"height": profile.window_height,
}
@router.get("/{project_slug}", response_class=HTMLResponse)
@@ -226,9 +233,12 @@ async def project_detail(request: Request, project_slug: str, before: str = None
schemas=[website_schema(base), software_application_schema(project, base)],
)
viewer_can_workspace = can_open_workspace(project, user)
workspace_editor_url = (
_editor_url(project, user) if viewer_can_workspace else ""
editor_launch = (
_editor_launch(project, user)
if viewer_can_workspace
else {"url": "", "mode": "tab", "width": 0, "height": 0}
)
workspace_editor_url = editor_launch["url"]
parent = get_fork_parent(project["uid"])
forked_from = (
{
@@ -276,6 +286,9 @@ async def project_detail(request: Request, project_slug: str, before: str = None
"viewer_can_containers": can_view_project_containers(project, user),
"viewer_can_workspace": viewer_can_workspace,
"workspace_editor_url": workspace_editor_url,
"workspace_editor_mode": editor_launch["mode"],
"workspace_editor_width": editor_launch["width"],
"workspace_editor_height": editor_launch["height"],
"forked_from": forked_from,
"fork_count": count_forks(project["uid"]),
"file_count": count_files(project["uid"]),