feat: event proposal and approval workflow #9

Closed
padreug wants to merge 38 commits from feat/event-approval-workflow into main
4 changed files with 48 additions and 15 deletions
Showing only changes of commit 29045163a3 - Show all commits

feat: add location and categories fields, simplify event creation
Some checks failed
lint.yml / feat: add location and categories fields, simplify event creation (pull_request) Failing after 0s

- Add location (text) and categories (JSON list) to Event model
- Make most CreateEvent fields optional: only title + start date required
- Default end_date to start_date, closing_date to end_date
- Categories stored as JSON text, parsed via validator
- NIP-52 publisher includes location tag and t tags for categories
- Migration m011 adds location and categories columns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Padreug 2026-04-27 18:05:25 +02:00

View file

@ -164,6 +164,12 @@ async def purge_unpaid_tickets(event_id: str) -> None:
async def create_event(data: CreateEvent) -> Event: async def create_event(data: CreateEvent) -> Event:
event_id = urlsafe_short_hash() event_id = urlsafe_short_hash()
# Default end date to start date if not provided
if not data.event_end_date:
data.event_end_date = data.event_start_date
# Default closing date to end date if not provided
if not data.closing_date:
data.closing_date = data.event_end_date
event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict()) event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict())
await db.insert("events.events", event) await db.insert("events.events", event)
return event return event

View file

@ -231,3 +231,15 @@ async def m010_add_events_settings(db):
await db.execute( await db.execute(
"INSERT OR IGNORE INTO events.settings (id, auto_approve) VALUES (1, FALSE);" "INSERT OR IGNORE INTO events.settings (id, auto_approve) VALUES (1, FALSE);"
) )
async def m011_add_location_and_categories(db):
"""
Add location and categories columns for NIP-52 calendar event support.
"""
await db.execute(
"ALTER TABLE events.events ADD COLUMN location TEXT;"
)
await db.execute(
"ALTER TABLE events.events ADD COLUMN categories TEXT;"
)

View file

@ -1,3 +1,4 @@
import json
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@ -29,15 +30,17 @@ class EventExtra(BaseModel):
class CreateEvent(BaseModel): class CreateEvent(BaseModel):
wallet: Optional[str] = None wallet: Optional[str] = None
name: str name: str # title (required)
info: str info: str = "" # description (optional, visible by default)
closing_date: str closing_date: Optional[str] = None # defaults to event_end_date or event_start_date
event_start_date: str event_start_date: str # required
event_end_date: str event_end_date: Optional[str] = None # defaults to event_start_date
currency: str = "sat" currency: str = "sat"
amount_tickets: int = Query(..., ge=0) amount_tickets: int = 0 # 0 = unlimited / not ticketed
price_per_ticket: float = Query(..., ge=0) price_per_ticket: float = 0 # 0 = free
banner: Optional[str] = None banner: Optional[str] = None # image URL (optional, visible by default)
location: Optional[str] = None # venue/address (optional, visible by default)
categories: list[str] = Field(default_factory=list) # NIP-52 't' tags
extra: EventExtra = Field(default_factory=EventExtra) extra: EventExtra = Field(default_factory=EventExtra)
status: str = "approved" # proposed, approved, rejected status: str = "approved" # proposed, approved, rejected
@ -67,22 +70,30 @@ class Event(BaseModel):
id: str id: str
wallet: str wallet: str
name: str name: str
info: str info: str = ""
closing_date: str closing_date: str | None = None
canceled: bool = False canceled: bool = False
event_start_date: str event_start_date: str
event_end_date: str event_end_date: str | None = None
currency: str currency: str = "sat"
amount_tickets: int amount_tickets: int = 0
price_per_ticket: float price_per_ticket: float = 0
time: datetime time: datetime
sold: int = 0 sold: int = 0
banner: str | None = None banner: str | None = None
location: str | None = None
categories: list[str] = Field(default_factory=list)
extra: EventExtra = Field(default_factory=EventExtra) extra: EventExtra = Field(default_factory=EventExtra)
status: str = "approved" # proposed, approved, rejected status: str = "approved" # proposed, approved, rejected
nostr_event_id: str | None = None nostr_event_id: str | None = None
nostr_event_created_at: int | None = None nostr_event_created_at: int | None = None
@validator("categories", pre=True)
def parse_categories(cls, v):
if isinstance(v, str):
return json.loads(v) if v else []
return v or []
class EventsSettings(BaseModel): class EventsSettings(BaseModel):
"""Extension-level settings for the events extension.""" """Extension-level settings for the events extension."""

View file

@ -40,13 +40,17 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
tags.append(["end", event.event_end_date]) tags.append(["end", event.event_end_date])
if event.banner: if event.banner:
tags.append(["image", event.banner]) tags.append(["image", event.banner])
if event.location:
tags.append(["location", event.location])
for cat in (event.categories or []):
tags.append(["t", cat])
nostr_event = NostrEvent( nostr_event = NostrEvent(
pubkey=pubkey, pubkey=pubkey,
created_at=int(time.time()), created_at=int(time.time()),
kind=31922, kind=31922,
tags=tags, tags=tags,
content=event.info, content=event.info or "",
) )
nostr_event.id = nostr_event.event_id nostr_event.id = nostr_event.event_id
return nostr_event return nostr_event