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 datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -6,6 +7,30 @@ from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from .models import CreateEvent, Event, Ticket, TicketExtra
|
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")
|
db = Database("ext_events")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -94,14 +119,7 @@ async def get_ticket(payment_hash: str) -> Optional[Ticket]:
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Convert empty strings back to None for the model
|
return Ticket(**_parse_ticket_row(row))
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]:
|
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])
|
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
|
||||||
rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})")
|
rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})")
|
||||||
|
|
||||||
tickets = []
|
return [Ticket(**_parse_ticket_row(row)) for row in rows]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
|
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}
|
{"user_id": user_id}
|
||||||
)
|
)
|
||||||
|
|
||||||
tickets = []
|
return [Ticket(**_parse_ticket_row(row)) for row in rows]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_ticket(payment_hash: str) -> None:
|
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:
|
async def delete_event(event_id: str) -> None:
|
||||||
await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id})
|
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},
|
{"event": event_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
tickets = []
|
return [Ticket(**_parse_ticket_row(row)) for row in rows]
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -191,3 +191,14 @@ async def m007_add_extra_fields(db):
|
||||||
|
|
||||||
# Add 'extra' column to ticket table
|
# Add 'extra' column to ticket table
|
||||||
await db.execute("ALTER TABLE events.ticket ADD COLUMN extra TEXT;")
|
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)
|
price_per_ticket: float = Query(..., ge=0)
|
||||||
banner: Optional[str] = None
|
banner: Optional[str] = None
|
||||||
extra: EventExtra = Field(default_factory=EventExtra)
|
extra: EventExtra = Field(default_factory=EventExtra)
|
||||||
|
status: str = "approved" # proposed, approved, rejected
|
||||||
|
|
||||||
|
|
||||||
class CreateTicket(BaseModel):
|
class CreateTicket(BaseModel):
|
||||||
|
|
@ -78,6 +79,7 @@ class Event(BaseModel):
|
||||||
sold: int = 0
|
sold: int = 0
|
||||||
banner: str | None = None
|
banner: str | None = None
|
||||||
extra: EventExtra = Field(default_factory=EventExtra)
|
extra: EventExtra = Field(default_factory=EventExtra)
|
||||||
|
status: str = "approved" # proposed, approved, rejected
|
||||||
|
|
||||||
|
|
||||||
class TicketExtra(BaseModel):
|
class TicketExtra(BaseModel):
|
||||||
|
|
|
||||||
81
views_api.py
81
views_api.py
|
|
@ -24,6 +24,8 @@ from .crud import (
|
||||||
get_event,
|
get_event,
|
||||||
get_event_tickets,
|
get_event_tickets,
|
||||||
get_events,
|
get_events,
|
||||||
|
get_pending_events,
|
||||||
|
get_public_events,
|
||||||
get_ticket,
|
get_ticket,
|
||||||
get_tickets,
|
get_tickets,
|
||||||
get_tickets_by_user_id,
|
get_tickets_by_user_id,
|
||||||
|
|
@ -54,12 +56,10 @@ async def api_events(
|
||||||
@events_api_router.get("/api/v1/events/public")
|
@events_api_router.get("/api/v1/events/public")
|
||||||
async def api_events_public():
|
async def api_events_public():
|
||||||
"""
|
"""
|
||||||
Retrieve all events in the database with read-only access.
|
Retrieve approved, non-canceled events for public display.
|
||||||
This endpoint allows access to all events using any valid API key (read access).
|
No authentication required.
|
||||||
"""
|
"""
|
||||||
# Get all events from the database without wallet filtering
|
events = await get_public_events()
|
||||||
from .crud import get_all_events
|
|
||||||
events = await get_all_events()
|
|
||||||
return [event.dict() for event in events]
|
return [event.dict() for event in events]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -128,6 +128,75 @@ async def api_form_delete(
|
||||||
return "", HTTPStatus.NO_CONTENT
|
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##########
|
#########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}")
|
@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)
|
event = await get_event(event_id)
|
||||||
if not event:
|
if not event:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue