import logging
from typing import Annotated
from datetime import datetime, timezone
from sqlalchemy import or_
from fastapi import APIRouter, Request, HTTPException, Form
from devplacepy.models import ProjectForm
from fastapi.responses import HTMLResponse, RedirectResponse
from devplacepy.database import get_table, get_users_by_uids, get_site_stats
from devplacepy.content import load_detail, delete_content_item
from devplacepy.attachments import link_attachments
from devplacepy.templating import templates
from devplacepy.utils import generate_uid, get_current_user, require_user, make_combined_slug, create_mention_notifications
from devplacepy.seo import base_seo_context, site_url, website_schema, software_application_schema
logger = logging.getLogger(__name__)
router = APIRouter()
def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None, project_type: str = None):
projects = get_table("projects")
filters = {}
if user_uid:
filters["user_uid"] = user_uid
if project_type:
filters["project_type"] = project_type
if tab == "released":
filters["status"] = "Released"
clauses = []
if search and projects.exists:
like = f"%{search}%"
clauses.append(or_(projects.table.columns.title.ilike(like), projects.table.columns.description.ilike(like)))
order = ["-stars", "-created_at"] if tab == "popular" else ["-created_at"]
all_projects = list(projects.find(*clauses, **filters, order_by=order))
if all_projects:
users_map = get_users_by_uids([p["user_uid"] for p in all_projects])
for p in all_projects:
author = users_map.get(p["user_uid"])
p["author_name"] = author["username"] if author else "Unknown"
return all_projects
@router.get("", response_class=HTMLResponse)
async def projects_page(
request: Request,
tab: str = "recent",
search: str = "",
user_uid: str = None,
project_type: str = None,
):
user = get_current_user(request)
projects = get_projects_list(tab, search, user_uid, project_type)
total_members = get_site_stats()["total_members"]
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="Projects",
description=f"Explore {len(projects)} developer projects on DevPlace. Games, software, mobile apps and more.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Projects", "url": "/projects"},
],
schemas=[website_schema(base)],
)
return templates.TemplateResponse(request, "projects.html", {
**seo_ctx,
"request": request,
"user": user,
"projects": projects,
"current_tab": tab,
"search": search,
"project_type": project_type,
"total_count": len(projects),
"total_members": total_members,
})
@router.get("/{project_slug}", response_class=HTMLResponse)
async def project_detail(request: Request, project_slug: str):
user = get_current_user(request)
detail = load_detail("projects", "project", project_slug, user)
if not detail:
raise HTTPException(status_code=404, detail="Project not found")
project = detail["item"]
base = site_url(request)
seo_ctx = base_seo_context(
request,
title=project.get("title", "Project"),
description=project.get("description", "")[:160],
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Projects", "url": "/projects"},
{"name": project.get("title", "Project"), "url": f"/projects/{project['slug'] or project['uid']}"},
],
schemas=[website_schema(base), software_application_schema(project, base)],
)
return templates.TemplateResponse(request, "project_detail.html", {
**seo_ctx,
"request": request,
"user": user,
"project": project,
"author": detail["author"],
"is_owner": detail["is_owner"],
"star_count": detail["star_count"],
"platforms": project.get("platforms", "").split(",") if project.get("platforms") else [],
"comments": detail["comments"],
"attachments": detail["attachments"],
})
@router.post("/delete/{project_slug}")
async def delete_project(request: Request, project_slug: str):
user = require_user(request)
return delete_content_item("projects", "project", user, project_slug, "/projects")
@router.post("/create")
async def create_project(request: Request, data: Annotated[ProjectForm, Form()]):
user = require_user(request)
title = data.title.strip()
description = data.description.strip()
projects = get_table("projects")
uid = generate_uid()
project_slug = make_combined_slug(title, uid)
projects.insert({
"uid": uid,
"user_uid": user["uid"],
"title": title,
"slug": project_slug,
"description": description,
"release_date": data.release_date or None,
"demo_date": data.demo_date or None,
"project_type": data.project_type,
"platforms": data.platforms.strip(),
"status": data.status,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
})
if data.attachment_uids:
link_attachments(data.attachment_uids, "project", uid)
create_mention_notifications(description, user["uid"], f"/projects/{project_slug}")
logger.info(f"Project {uid} created by {user['username']}")
return RedirectResponse(url=f"/projects/{project_slug}", status_code=302)