Compare commits
5 commits
a77145e08e
...
eb474b1390
| Author | SHA1 | Date | |
|---|---|---|---|
| eb474b1390 | |||
| a41348df94 | |||
| 0c782e6239 | |||
| 1dcff37df5 | |||
| 68e6e3d02e |
4 changed files with 137 additions and 47 deletions
90
crud.py
90
crud.py
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
|
@ -6,6 +7,30 @@ from lnbits.helpers import urlsafe_short_hash
|
|||
|
||||
from .models import CreateEvent, Event, Ticket, TicketExtra
|
||||
|
||||
|
||||
def _parse_ticket_row(row) -> dict:
|
||||
"""
|
||||
Parse a database row into a dict suitable for Ticket model creation.
|
||||
Handles:
|
||||
- Empty string to None conversion for name/email
|
||||
- JSON string to dict conversion for extra field
|
||||
"""
|
||||
ticket_data = dict(row)
|
||||
|
||||
# Convert empty strings back to None for the model
|
||||
if ticket_data.get("name") == "":
|
||||
ticket_data["name"] = None
|
||||
if ticket_data.get("email") == "":
|
||||
ticket_data["email"] = None
|
||||
|
||||
# Parse extra field from JSON string if needed
|
||||
# (db.insert() serializes to JSON, but manual fetchone/fetchall returns string)
|
||||
extra = ticket_data.get("extra")
|
||||
if isinstance(extra, str):
|
||||
ticket_data["extra"] = json.loads(extra)
|
||||
|
||||
return ticket_data
|
||||
|
||||
db = Database("ext_events")
|
||||
|
||||
|
||||
|
|
@ -94,14 +119,7 @@ async def get_ticket(payment_hash: str) -> Optional[Ticket]:
|
|||
if not row:
|
||||
return None
|
||||
|
||||
# Convert empty strings back to None for the model
|
||||
ticket_data = dict(row)
|
||||
if ticket_data.get("name") == "":
|
||||
ticket_data["name"] = None
|
||||
if ticket_data.get("email") == "":
|
||||
ticket_data["email"] = None
|
||||
|
||||
return Ticket(**ticket_data)
|
||||
return Ticket(**_parse_ticket_row(row))
|
||||
|
||||
|
||||
async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]:
|
||||
|
|
@ -110,17 +128,7 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]:
|
|||
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
|
||||
rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})")
|
||||
|
||||
tickets = []
|
||||
for row in rows:
|
||||
# Convert empty strings back to None for the model
|
||||
ticket_data = dict(row)
|
||||
if ticket_data.get("name") == "":
|
||||
ticket_data["name"] = None
|
||||
if ticket_data.get("email") == "":
|
||||
ticket_data["email"] = None
|
||||
tickets.append(Ticket(**ticket_data))
|
||||
|
||||
return tickets
|
||||
return [Ticket(**_parse_ticket_row(row)) for row in rows]
|
||||
|
||||
|
||||
async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
|
||||
|
|
@ -130,17 +138,7 @@ async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
|
|||
{"user_id": user_id}
|
||||
)
|
||||
|
||||
tickets = []
|
||||
for row in rows:
|
||||
# Convert empty strings back to None for the model
|
||||
ticket_data = dict(row)
|
||||
if ticket_data.get("name") == "":
|
||||
ticket_data["name"] = None
|
||||
if ticket_data.get("email") == "":
|
||||
ticket_data["email"] = None
|
||||
tickets.append(Ticket(**ticket_data))
|
||||
|
||||
return tickets
|
||||
return [Ticket(**_parse_ticket_row(row)) for row in rows]
|
||||
|
||||
|
||||
async def delete_ticket(payment_hash: str) -> None:
|
||||
|
|
@ -202,6 +200,26 @@ async def get_all_events() -> list[Event]:
|
|||
)
|
||||
|
||||
|
||||
async def get_public_events() -> list[Event]:
|
||||
"""Get approved, non-canceled events for public display."""
|
||||
return await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM events.events
|
||||
WHERE status = 'approved' AND canceled = FALSE
|
||||
ORDER BY event_start_date ASC
|
||||
""",
|
||||
model=Event,
|
||||
)
|
||||
|
||||
|
||||
async def get_pending_events() -> list[Event]:
|
||||
"""Get proposed events awaiting admin approval."""
|
||||
return await db.fetchall(
|
||||
"SELECT * FROM events.events WHERE status = 'proposed' ORDER BY time DESC",
|
||||
model=Event,
|
||||
)
|
||||
|
||||
|
||||
async def delete_event(event_id: str) -> None:
|
||||
await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id})
|
||||
|
||||
|
|
@ -212,14 +230,4 @@ async def get_event_tickets(event_id: str) -> list[Ticket]:
|
|||
{"event": event_id},
|
||||
)
|
||||
|
||||
tickets = []
|
||||
for row in rows:
|
||||
# Convert empty strings back to None for the model
|
||||
ticket_data = dict(row)
|
||||
if ticket_data.get("name") == "":
|
||||
ticket_data["name"] = None
|
||||
if ticket_data.get("email") == "":
|
||||
ticket_data["email"] = None
|
||||
tickets.append(Ticket(**ticket_data))
|
||||
|
||||
return tickets
|
||||
return [Ticket(**_parse_ticket_row(row)) for row in rows]
|
||||
|
|
|
|||
|
|
@ -191,3 +191,14 @@ async def m007_add_extra_fields(db):
|
|||
|
||||
# Add 'extra' column to ticket table
|
||||
await db.execute("ALTER TABLE events.ticket ADD COLUMN extra TEXT;")
|
||||
|
||||
|
||||
async def m008_add_event_status(db):
|
||||
"""
|
||||
Add status column to events table for proposal/approval workflow.
|
||||
Values: 'proposed', 'approved', 'rejected'.
|
||||
Default 'approved' for backward compatibility with existing events.
|
||||
"""
|
||||
await db.execute(
|
||||
"ALTER TABLE events.events ADD COLUMN status TEXT NOT NULL DEFAULT 'approved';"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class CreateEvent(BaseModel):
|
|||
price_per_ticket: float = Query(..., ge=0)
|
||||
banner: Optional[str] = None
|
||||
extra: EventExtra = Field(default_factory=EventExtra)
|
||||
status: str = "approved" # proposed, approved, rejected
|
||||
|
||||
|
||||
class CreateTicket(BaseModel):
|
||||
|
|
@ -78,6 +79,7 @@ class Event(BaseModel):
|
|||
sold: int = 0
|
||||
banner: str | None = None
|
||||
extra: EventExtra = Field(default_factory=EventExtra)
|
||||
status: str = "approved" # proposed, approved, rejected
|
||||
|
||||
|
||||
class TicketExtra(BaseModel):
|
||||
|
|
|
|||
81
views_api.py
81
views_api.py
|
|
@ -24,6 +24,8 @@ from .crud import (
|
|||
get_event,
|
||||
get_event_tickets,
|
||||
get_events,
|
||||
get_pending_events,
|
||||
get_public_events,
|
||||
get_ticket,
|
||||
get_tickets,
|
||||
get_tickets_by_user_id,
|
||||
|
|
@ -54,12 +56,10 @@ async def api_events(
|
|||
@events_api_router.get("/api/v1/events/public")
|
||||
async def api_events_public():
|
||||
"""
|
||||
Retrieve all events in the database with read-only access.
|
||||
This endpoint allows access to all events using any valid API key (read access).
|
||||
Retrieve approved, non-canceled events for public display.
|
||||
No authentication required.
|
||||
"""
|
||||
# Get all events from the database without wallet filtering
|
||||
from .crud import get_all_events
|
||||
events = await get_all_events()
|
||||
events = await get_public_events()
|
||||
return [event.dict() for event in events]
|
||||
|
||||
|
||||
|
|
@ -128,6 +128,75 @@ async def api_form_delete(
|
|||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########Event Approval##########
|
||||
|
||||
|
||||
@events_api_router.post("/api/v1/events/propose")
|
||||
async def api_event_propose(
|
||||
data: CreateEvent,
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
):
|
||||
"""
|
||||
Propose a new event for admin approval.
|
||||
Requires invoice key (any authenticated user, not admin-only).
|
||||
"""
|
||||
data.status = "proposed"
|
||||
data.wallet = wallet.wallet.id
|
||||
event = await create_event(data)
|
||||
return event.dict()
|
||||
|
||||
|
||||
@events_api_router.get("/api/v1/events/pending")
|
||||
async def api_events_pending(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
"""Get all proposed events awaiting approval. Admin only."""
|
||||
events = await get_pending_events()
|
||||
return [event.dict() for event in events]
|
||||
|
||||
|
||||
@events_api_router.put("/api/v1/events/{event_id}/approve")
|
||||
async def api_event_approve(
|
||||
event_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
"""Approve a proposed event. Admin only."""
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
|
||||
)
|
||||
if event.status != "proposed":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Event is already {event.status}.",
|
||||
)
|
||||
event.status = "approved"
|
||||
event = await update_event(event)
|
||||
return event.dict()
|
||||
|
||||
|
||||
@events_api_router.put("/api/v1/events/{event_id}/reject")
|
||||
async def api_event_reject(
|
||||
event_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
"""Reject a proposed event. Admin only."""
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
|
||||
)
|
||||
if event.status != "proposed":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Event is already {event.status}.",
|
||||
)
|
||||
event.status = "rejected"
|
||||
event = await update_event(event)
|
||||
return event.dict()
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
|
||||
|
||||
|
|
@ -207,7 +276,7 @@ async def api_ticket_make_ticket_user_id(event_id: str, user_id: str):
|
|||
|
||||
|
||||
@events_api_router.get("/api/v1/tickets/{event_id}/{name}/{email}")
|
||||
async def api_ticket_make_ticket(event_id, name, email, promo_code, refund_address):
|
||||
async def api_ticket_make_ticket(event_id, name, email, promo_code=None, refund_address=None):
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue