diff --git a/crud.py b/crud.py index 450c091..004fa7f 100644 --- a/crud.py +++ b/crud.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta, timezone from lnbits.db import Database from lnbits.helpers import urlsafe_short_hash -from .models import CreateEvent, Event, Ticket, TicketExtra +from .models import CreateEvent, Event, EventsSettings, Ticket, TicketExtra db = Database("ext_events") @@ -143,6 +143,11 @@ async def purge_unpaid_tickets(event_id: str) -> None: async def create_event(data: CreateEvent) -> Event: event_id = urlsafe_short_hash() + # Default end_date to start_date and closing_date to end_date when omitted. + if not data.event_end_date: + data.event_end_date = data.event_start_date + if not data.closing_date: + data.closing_date = data.event_end_date event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict()) await db.insert("events.events", event) return event @@ -171,6 +176,50 @@ async def get_events(wallet_ids: str | list[str]) -> list[Event]: ) +async def get_all_events() -> list[Event]: + """All events, no wallet filter. Admin-only callers.""" + return await db.fetchall( + "SELECT * FROM events.events ORDER BY time DESC", + model=Event, + ) + + +async def get_public_events() -> list[Event]: + """Approved, non-canceled events for the public listing.""" + 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]: + """Proposed events awaiting admin approval.""" + return await db.fetchall( + "SELECT * FROM events.events WHERE status = 'proposed' ORDER BY time DESC", + model=Event, + ) + + +async def get_settings() -> EventsSettings: + """Singleton settings row, seeded by m010.""" + row = await db.fetchone("SELECT * FROM events.settings WHERE id = 1") + if row: + return EventsSettings(**dict(row)) + return EventsSettings() + + +async def update_settings(settings: EventsSettings) -> EventsSettings: + await db.execute( + "UPDATE events.settings SET auto_approve = :auto_approve WHERE id = 1", + {"auto_approve": settings.auto_approve}, + ) + return settings + + async def delete_event(event_id: str) -> None: await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id}) diff --git a/models.py b/models.py index 0216653..a9cb31b 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,5 @@ from datetime import datetime -from fastapi import Query from pydantic import BaseModel, EmailStr, Field, root_validator, validator @@ -31,55 +30,64 @@ class EventExtra(BaseModel): class CreateEvent(BaseModel): - wallet: str - name: str - info: str - closing_date: str - event_start_date: str - event_end_date: str + wallet: str | None = None # filled from caller's wallet if absent + name: str # title (required) + info: str = "" # description (optional) + closing_date: str | None = None # defaults to event_end_date + event_start_date: str # required + event_end_date: str | None = None # defaults to event_start_date currency: str = "sat" allow_fiat: bool = False fiat_currency: str = "GBP" - amount_tickets: int = Query(..., ge=0) - price_per_ticket: float = Query(..., ge=0) + amount_tickets: int = 0 # 0 = unlimited / not ticketed + price_per_ticket: float = 0 # 0 = free banner: str | None = None extra: EventExtra = Field(default_factory=EventExtra) + status: str = "approved" # proposed, approved, rejected class Event(BaseModel): id: str wallet: str name: str - info: str - closing_date: str + info: str = "" + closing_date: str | None = None canceled: bool = False event_start_date: str - event_end_date: str - currency: str + event_end_date: str | None = None + currency: str = "sat" allow_fiat: bool = False fiat_currency: str = "GBP" - amount_tickets: int - price_per_ticket: float + amount_tickets: int = 0 + price_per_ticket: float = 0 time: datetime sold: int = 0 banner: str | None = None extra: EventExtra = Field(default_factory=EventExtra) + status: str = "approved" class PublicEvent(BaseModel): id: str name: str info: str - closing_date: str + closing_date: str | None = None canceled: bool event_start_date: str - event_end_date: str + event_end_date: str | None = None currency: str allow_fiat: bool = False fiat_currency: str = "GBP" price_per_ticket: float banner: str | None extra: EventExtra = Field(default_factory=EventExtra) + status: str = "approved" # surfaces "proposed"/"rejected" so SFC can render banner + + +class EventsSettings(BaseModel): + """Extension-level settings for the events extension.""" + + auto_approve: bool = False # Skip approval workflow for non-admin users class TicketExtra(BaseModel): diff --git a/static/js/display.vue b/static/js/display.vue index f5ac5fb..9b27783 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -12,7 +12,32 @@
- + + + + Pending approval — this + event is awaiting an admin review and is not yet open for tickets. + + + + + Not approved — this event + was reviewed and is not being published. + + +
Buy Ticket
diff --git a/static/js/index.js b/static/js/index.js index 5424a32..9044ef1 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -6,6 +6,12 @@ window.PageEvents = { tickets: [], resendingTicketEmails: [], currencies: [], + pendingEvents: [], + allUserEvents: [], + isAdmin: false, + settings: { + auto_approve: false + }, eventsTable: { columns: [ {name: 'id', align: 'left', label: 'ID', field: 'id'}, @@ -66,7 +72,8 @@ window.PageEvents = { field: 'sold' }, {name: 'info', align: 'left', label: 'Info', field: 'info'}, - {name: 'banner', align: 'left', label: 'Banner', field: 'banner'} + {name: 'banner', align: 'left', label: 'Banner', field: 'banner'}, + {name: 'status', align: 'left', label: 'Status', field: 'status'} ], pagination: { rowsPerPage: 10 @@ -189,6 +196,84 @@ window.PageEvents = { this.events = response.data this.checkCanceledEvents() }) + + // Admin probe: a 200 from /all means we're an LNbits admin. + LNbits.api + .request('GET', '/events/api/v1/events/all') + .then(response => { + this.isAdmin = true + const ownWalletIds = this.g.user.wallets.map(w => w.id) + this.allUserEvents = response.data.filter( + e => !ownWalletIds.includes(e.wallet) + ) + }) + .catch(() => { + this.isAdmin = false + this.allUserEvents = [] + }) + }, + getSettings() { + LNbits.api + .request('GET', '/events/api/v1/events/settings') + .then(response => { + this.settings = response.data + }) + .catch(() => { + // Not admin or settings unavailable; keep defaults. + }) + }, + saveSettings() { + LNbits.api + .request( + 'PUT', + '/events/api/v1/events/settings', + null, + this.settings + ) + .then(() => { + Quasar.Notify.create({type: 'positive', message: 'Settings saved'}) + }) + .catch(LNbits.utils.notifyApiError) + }, + getPendingEvents() { + LNbits.api + .request('GET', '/events/api/v1/events/pending') + .then(response => { + this.pendingEvents = response.data + }) + .catch(() => { + this.pendingEvents = [] + }) + }, + approveEvent(eventId) { + LNbits.utils.confirmDialog('Approve this event?').onOk(() => { + LNbits.api + .request('PUT', '/events/api/v1/events/' + eventId + '/approve') + .then(() => { + Quasar.Notify.create({ + type: 'positive', + message: 'Event approved' + }) + this.getEvents() + this.getPendingEvents() + }) + .catch(LNbits.utils.notifyApiError) + }) + }, + rejectEvent(eventId) { + LNbits.utils.confirmDialog('Reject this event?').onOk(() => { + LNbits.api + .request('PUT', '/events/api/v1/events/' + eventId + '/reject') + .then(() => { + Quasar.Notify.create({ + type: 'positive', + message: 'Event rejected' + }) + this.getEvents() + this.getPendingEvents() + }) + .catch(LNbits.utils.notifyApiError) + }) }, sendEventData() { const wallet = _.findWhere(this.g.user.wallets, { @@ -333,6 +418,8 @@ window.PageEvents = { if (this.g.user.wallets.length) { this.getTickets() this.getEvents() + this.getSettings() + this.getPendingEvents() if (this.g.allowedCurrencies && this.g.allowedCurrencies.length > 0) { this.currencies = ['sats', ...this.g.allowedCurrencies] } else { diff --git a/static/js/index.vue b/static/js/index.vue index fa39a4e..88ae90d 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -1,6 +1,23 @@