Monorepo: consolidate 7 repos into one
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s

Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
This commit is contained in:
giles
2026-02-24 19:44:17 +00:00
commit f42042ccb7
895 changed files with 61147 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession
from shared.services.registry import services
async def toggle_entry_association(
session: AsyncSession,
post_id: int,
entry_id: int
) -> tuple[bool, str | None]:
"""
Toggle association between a post and calendar entry.
Returns (is_now_associated, error_message).
"""
post = await services.blog.get_post_by_id(session, post_id)
if not post:
return False, "Post not found"
is_associated = await services.calendar.toggle_entry_post(
session, entry_id, "post", post_id,
)
return is_associated, None
async def get_post_entry_ids(
session: AsyncSession,
post_id: int
) -> set[int]:
"""
Get all entry IDs associated with this post.
Returns a set of entry IDs.
"""
return await services.calendar.entry_ids_for_content(session, "post", post_id)
async def get_associated_entries(
session: AsyncSession,
post_id: int,
page: int = 1,
per_page: int = 10
) -> dict:
"""
Get paginated associated entries for this post.
Returns dict with entries (CalendarEntryDTOs), total_count, and has_more.
"""
entries, has_more = await services.calendar.associated_entries(
session, "post", post_id, page,
)
total_count = len(entries) + (page - 1) * per_page
if has_more:
total_count += 1 # at least one more
return {
"entries": entries,
"total_count": total_count,
"has_more": has_more,
"page": page,
}

View File

@@ -0,0 +1,61 @@
from __future__ import annotations
import re
import unicodedata
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from shared.models.page_config import PageConfig
from shared.contracts.dtos import MarketPlaceDTO
from shared.services.registry import services
class MarketError(ValueError):
"""Base error for market service operations."""
def slugify(value: str, max_len: int = 255) -> str:
if value is None:
value = ""
value = unicodedata.normalize("NFKD", value)
value = value.encode("ascii", "ignore").decode("ascii")
value = value.lower()
value = value.replace("/", "-")
value = re.sub(r"[^a-z0-9]+", "-", value)
value = re.sub(r"-{2,}", "-", value)
value = value.strip("-")[:max_len].strip("-")
return value or "market"
async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPlaceDTO:
name = (name or "").strip()
if not name:
raise MarketError("Market name must not be empty.")
slug = slugify(name)
post = await services.blog.get_post_by_id(sess, post_id)
if not post:
raise MarketError(f"Post {post_id} does not exist.")
if not post.is_page:
raise MarketError("Markets can only be created on pages, not posts.")
pc = (await sess.execute(
select(PageConfig).where(PageConfig.container_type == "page", PageConfig.container_id == post_id)
)).scalar_one_or_none()
if pc is None or not (pc.features or {}).get("market"):
raise MarketError("Market feature is not enabled for this page. Enable it in page settings first.")
try:
return await services.market.create_marketplace(sess, "page", post_id, name, slug)
except ValueError as e:
raise MarketError(str(e)) from e
async def soft_delete_market(sess: AsyncSession, post_slug: str, market_slug: str) -> bool:
post = await services.blog.get_post_by_slug(sess, post_slug)
if not post:
return False
return await services.market.soft_delete_marketplace(sess, "page", post.id, market_slug)

View File

@@ -0,0 +1,42 @@
from ...blog.ghost_db import DBClient # adjust import path
from sqlalchemy import select
from models.ghost_content import PostLike
from quart import g
async def post_data(slug, session, include_drafts=False):
client = DBClient(session)
posts = (await client.posts_by_slug(slug, include_drafts=include_drafts))
if not posts:
# 404 page (you can make a template for this if you want)
return None
post, original_post = posts[0]
# Check if current user has liked this post
is_liked = False
if g.user:
liked_record = await session.scalar(
select(PostLike).where(
PostLike.user_id == g.user.id,
PostLike.post_id == post["id"],
PostLike.deleted_at.is_(None),
)
)
is_liked = liked_record is not None
# Add is_liked to post dict
post["is_liked"] = is_liked
tags=await client.list_tags(
limit=50000
) # <-- new
authors=await client.list_authors(
limit=50000
)
return {
"post": post,
"original_post": original_post,
"tags": tags,
"authors": authors,
}

View File

@@ -0,0 +1,58 @@
from __future__ import annotations
from typing import Optional
from sqlalchemy import select, func, update
from sqlalchemy.ext.asyncio import AsyncSession
from models.ghost_content import Post, PostLike
async def toggle_post_like(
session: AsyncSession,
user_id: int,
post_id: int,
) -> tuple[bool, Optional[str]]:
"""
Toggle a post like for a given user using soft deletes.
Returns (liked_state, error_message).
- If error_message is not None, an error occurred.
- liked_state indicates whether post is now liked (True) or unliked (False).
"""
# Verify post exists
post_exists = await session.scalar(
select(Post.id).where(Post.id == post_id, Post.deleted_at.is_(None))
)
if not post_exists:
return False, "Post not found"
# Check if like exists (not deleted)
existing = await session.scalar(
select(PostLike).where(
PostLike.user_id == user_id,
PostLike.post_id == post_id,
PostLike.deleted_at.is_(None),
)
)
if existing:
# Unlike: soft delete the like
await session.execute(
update(PostLike)
.where(
PostLike.user_id == user_id,
PostLike.post_id == post_id,
PostLike.deleted_at.is_(None),
)
.values(deleted_at=func.now())
)
return False, None
else:
# Like: add a new like
new_like = PostLike(
user_id=user_id,
post_id=post_id,
)
session.add(new_like)
return True, None