Monorepo: consolidate 7 repos into one
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:
121
events/bp/calendar_entry/services/post_associations.py
Normal file
121
events/bp/calendar_entry/services/post_associations.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from models.calendars import CalendarEntry, CalendarEntryPost
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
async def add_post_to_entry(
|
||||
session: AsyncSession,
|
||||
entry_id: int,
|
||||
post_id: int
|
||||
) -> tuple[bool, str | None]:
|
||||
"""
|
||||
Associate a post with a calendar entry.
|
||||
Returns (success, error_message).
|
||||
"""
|
||||
# Check if entry exists
|
||||
entry = await session.scalar(
|
||||
select(CalendarEntry).where(
|
||||
CalendarEntry.id == entry_id,
|
||||
CalendarEntry.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
if not entry:
|
||||
return False, "Calendar entry not found"
|
||||
|
||||
# Check if post exists
|
||||
post = await services.blog.get_post_by_id(session, post_id)
|
||||
if not post:
|
||||
return False, "Post not found"
|
||||
|
||||
# Check if association already exists
|
||||
existing = await session.scalar(
|
||||
select(CalendarEntryPost).where(
|
||||
CalendarEntryPost.entry_id == entry_id,
|
||||
CalendarEntryPost.content_type == "post",
|
||||
CalendarEntryPost.content_id == post_id,
|
||||
CalendarEntryPost.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
if existing:
|
||||
return False, "Post is already associated with this entry"
|
||||
|
||||
# Create association
|
||||
association = CalendarEntryPost(
|
||||
entry_id=entry_id,
|
||||
content_type="post",
|
||||
content_id=post_id
|
||||
)
|
||||
session.add(association)
|
||||
await session.flush()
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
async def remove_post_from_entry(
|
||||
session: AsyncSession,
|
||||
entry_id: int,
|
||||
post_id: int
|
||||
) -> tuple[bool, str | None]:
|
||||
"""
|
||||
Remove a post association from a calendar entry (soft delete).
|
||||
Returns (success, error_message).
|
||||
"""
|
||||
# Find the association
|
||||
association = await session.scalar(
|
||||
select(CalendarEntryPost).where(
|
||||
CalendarEntryPost.entry_id == entry_id,
|
||||
CalendarEntryPost.content_type == "post",
|
||||
CalendarEntryPost.content_id == post_id,
|
||||
CalendarEntryPost.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
if not association:
|
||||
return False, "Association not found"
|
||||
|
||||
# Soft delete
|
||||
association.deleted_at = func.now()
|
||||
await session.flush()
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
async def get_entry_posts(
|
||||
session: AsyncSession,
|
||||
entry_id: int
|
||||
) -> list:
|
||||
"""
|
||||
Get all posts (as PostDTOs) associated with a calendar entry.
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(CalendarEntryPost.content_id).where(
|
||||
CalendarEntryPost.entry_id == entry_id,
|
||||
CalendarEntryPost.content_type == "post",
|
||||
CalendarEntryPost.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
post_ids = list(result.scalars().all())
|
||||
if not post_ids:
|
||||
return []
|
||||
posts = await services.blog.get_posts_by_ids(session, post_ids)
|
||||
return sorted(posts, key=lambda p: (p.title or ""))
|
||||
|
||||
|
||||
async def search_posts(
|
||||
session: AsyncSession,
|
||||
query: str,
|
||||
page: int = 1,
|
||||
per_page: int = 10
|
||||
) -> tuple[list, int]:
|
||||
"""
|
||||
Search for posts by title with pagination.
|
||||
If query is empty, returns all posts in published order.
|
||||
Returns (post_dtos, total_count).
|
||||
"""
|
||||
return await services.blog.search_posts(session, query, page, per_page)
|
||||
87
events/bp/calendar_entry/services/ticket_operations.py
Normal file
87
events/bp/calendar_entry/services/ticket_operations.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.calendars import CalendarEntry
|
||||
|
||||
|
||||
|
||||
async def update_ticket_config(
|
||||
session: AsyncSession,
|
||||
entry_id: int,
|
||||
ticket_price: Optional[Decimal],
|
||||
ticket_count: Optional[int],
|
||||
) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Update ticket configuration for a calendar entry.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
entry_id: Calendar entry ID
|
||||
ticket_price: Price per ticket (None = no tickets)
|
||||
ticket_count: Total available tickets (None = unlimited)
|
||||
|
||||
Returns:
|
||||
(success, error_message)
|
||||
"""
|
||||
# Get the entry
|
||||
entry = await session.scalar(
|
||||
select(CalendarEntry).where(
|
||||
CalendarEntry.id == entry_id,
|
||||
CalendarEntry.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
if not entry:
|
||||
return False, "Calendar entry not found"
|
||||
|
||||
# Validate inputs
|
||||
if ticket_price is not None and ticket_price < 0:
|
||||
return False, "Ticket price cannot be negative"
|
||||
|
||||
if ticket_count is not None and ticket_count < 0:
|
||||
return False, "Ticket count cannot be negative"
|
||||
|
||||
# Update ticket configuration
|
||||
entry.ticket_price = ticket_price
|
||||
entry.ticket_count = ticket_count
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
async def get_available_tickets(
|
||||
session: AsyncSession,
|
||||
entry_id: int,
|
||||
) -> tuple[Optional[int], Optional[str]]:
|
||||
"""
|
||||
Get the number of available tickets for a calendar entry.
|
||||
|
||||
Returns:
|
||||
(available_count, error_message)
|
||||
- available_count is None if unlimited tickets
|
||||
- available_count is the remaining count if limited
|
||||
"""
|
||||
entry = await session.scalar(
|
||||
select(CalendarEntry).where(
|
||||
CalendarEntry.id == entry_id,
|
||||
CalendarEntry.deleted_at.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
if not entry:
|
||||
return None, "Calendar entry not found"
|
||||
|
||||
# If no ticket configuration, return None (unlimited)
|
||||
if entry.ticket_price is None:
|
||||
return None, None
|
||||
|
||||
# If ticket_count is None, unlimited tickets
|
||||
if entry.ticket_count is None:
|
||||
return None, None
|
||||
|
||||
# Returns total count (booked tickets not yet subtracted)
|
||||
return entry.ticket_count, None
|
||||
Reference in New Issue
Block a user