feat: update to lnbits 1.0.0 (#36)

This commit is contained in:
dni ⚡ 2024-10-11 13:52:39 +02:00 committed by GitHub
parent 9ca714d878
commit 6714dcddc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1769 additions and 1772 deletions

View file

@ -2,7 +2,7 @@
"name": "Events", "name": "Events",
"short_description": "Sell and register event tickets", "short_description": "Sell and register event tickets",
"tile": "/events/static/image/events.png", "tile": "/events/static/image/events.png",
"min_lnbits_version": "0.12.5", "min_lnbits_version": "1.0.0",
"contributors": [ "contributors": [
{ {
"name": "talvasconcelos", "name": "talvasconcelos",

186
crud.py
View file

@ -1,5 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import List, Optional, Union from typing import Optional, Union
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
@ -12,177 +12,103 @@ db = Database("ext_events")
async def create_ticket( async def create_ticket(
payment_hash: str, wallet: str, event: str, name: str, email: str payment_hash: str, wallet: str, event: str, name: str, email: str
) -> Ticket: ) -> Ticket:
await db.execute( now = datetime.now(timezone.utc)
""" ticket = Ticket(
INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid) id=payment_hash,
VALUES (?, ?, ?, ?, ?, ?, ?) wallet=wallet,
""", event=event,
(payment_hash, wallet, event, name, email, False, False), name=name,
email=email,
registered=False,
paid=False,
reg_timestamp=now,
time=now,
) )
await db.insert("events.ticket", ticket)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly created ticket couldn't be retrieved"
return ticket return ticket
async def set_ticket_paid(payment_hash: str) -> Ticket: async def update_ticket(ticket: Ticket) -> Ticket:
ticket = await get_ticket(payment_hash) await db.update("events.ticket", ticket)
assert ticket, "Ticket couldn't be retrieved"
if ticket.paid:
return ticket
await db.execute(
"""
UPDATE events.ticket
SET paid = ?
WHERE id = ?
""",
(True, ticket.id),
)
await update_event_sold(ticket.event)
return ticket return ticket
async def update_event_sold(event_id: str):
event = await get_event(event_id)
assert event, "Couldn't get event from ticket being paid"
sold = event.sold + 1
amount_tickets = event.amount_tickets - 1
await db.execute(
"""
UPDATE events.events
SET sold = ?, amount_tickets = ?
WHERE id = ?
""",
(sold, amount_tickets, event_id),
)
return
async def get_ticket(payment_hash: str) -> Optional[Ticket]: async def get_ticket(payment_hash: str) -> Optional[Ticket]:
row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,)) return await db.fetchone(
return Ticket(**row) if row else None "SELECT * FROM events.ticket WHERE id = :id",
{"id": payment_hash},
Ticket,
)
async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Ticket]: async def get_tickets(wallet_ids: Union[str, list[str]]) -> list[Ticket]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
q = ",".join(["?"] * len(wallet_ids)) return await db.fetchall(
rows = await db.fetchall( f"SELECT * FROM events.ticket WHERE wallet IN ({q})",
f"SELECT * FROM events.ticket WHERE wallet IN ({q})", (*wallet_ids,) model=Ticket,
) )
return [Ticket(**row) for row in rows]
async def delete_ticket(payment_hash: str) -> None: async def delete_ticket(payment_hash: str) -> None:
await db.execute("DELETE FROM events.ticket WHERE id = ?", (payment_hash,)) await db.execute("DELETE FROM events.ticket WHERE id = :id", {"id": payment_hash})
async def delete_event_tickets(event_id: str) -> None: async def delete_event_tickets(event_id: str) -> None:
await db.execute("DELETE FROM events.ticket WHERE event = ?", (event_id,)) await db.execute(
"DELETE FROM events.ticket WHERE event = :event", {"event": event_id}
)
async def purge_unpaid_tickets(event_id: str) -> None: async def purge_unpaid_tickets(event_id: str) -> None:
time_diff = datetime.now() - timedelta(hours=24) time_diff = datetime.now() - timedelta(hours=24)
await db.execute( await db.execute(
f""" f"""
DELETE FROM events.ticket WHERE event = ? AND paid = false DELETE FROM events.ticket WHERE event = :event AND paid = false
AND time < {db.timestamp_placeholder} AND time < {db.timestamp_placeholder("time")}
""", """,
( {"time": time_diff.timestamp(), "event": event_id},
event_id,
time_diff.timestamp(),
),
) )
async def create_event(data: CreateEvent) -> Event: async def create_event(data: CreateEvent) -> Event:
event_id = urlsafe_short_hash() event_id = urlsafe_short_hash()
await db.execute( event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict())
""" await db.insert("events.events", event)
INSERT INTO events.events (
id, wallet, name, info, banner, closing_date, event_start_date,
event_end_date, currency, amount_tickets, price_per_ticket, sold
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
event_id,
data.wallet,
data.name,
data.info,
data.banner,
data.closing_date,
data.event_start_date,
data.event_end_date,
data.currency,
data.amount_tickets,
data.price_per_ticket,
0,
),
)
event = await get_event(event_id)
assert event, "Newly created event couldn't be retrieved"
return event return event
async def update_event(event_id: str, **kwargs) -> Event: async def update_event(event: Event) -> Event:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.update("events.events", event)
await db.execute(
f"UPDATE events.events SET {q} WHERE id = ?", (*kwargs.values(), event_id)
)
event = await get_event(event_id)
assert event, "Newly updated event couldn't be retrieved"
return event return event
async def get_event(event_id: str) -> Optional[Event]: async def get_event(event_id: str) -> Optional[Event]:
row = await db.fetchone("SELECT * FROM events.events WHERE id = ?", (event_id,)) return await db.fetchone(
return Event(**row) if row else None "SELECT * FROM events.events WHERE id = :id",
{"id": event_id},
Event,
async def get_events(wallet_ids: Union[str, List[str]]) -> List[Event]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM events.events WHERE wallet IN ({q})", (*wallet_ids,)
) )
return [Event(**row) for row in rows]
async def get_events(wallet_ids: Union[str, list[str]]) -> list[Event]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
return await db.fetchall(
f"SELECT * FROM events.events WHERE wallet IN ({q})",
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 = ?", (event_id,)) await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id})
# EVENTTICKETS async def get_event_tickets(event_id: str) -> list[Ticket]:
return await db.fetchall(
"SELECT * FROM events.ticket WHERE event = :event",
async def get_event_tickets(event_id: str, wallet_id: str) -> List[Ticket]: {"event": event_id},
rows = await db.fetchall( Ticket,
"SELECT * FROM events.ticket WHERE wallet = ? AND event = ?",
(wallet_id, event_id),
) )
return [Ticket(**row) for row in rows]
async def reg_ticket(ticket_id: str) -> List[Ticket]:
await db.execute(
f"""
UPDATE events.ticket SET registered = ?,
reg_timestamp = {db.timestamp_now} WHERE id = ?
""",
(True, ticket_id),
)
ticket = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (ticket_id,))
rows = await db.fetchall(
"SELECT * FROM events.ticket WHERE event = ?", (ticket[1],)
)
return [Ticket(**row) for row in rows]

View file

@ -1,3 +1,4 @@
from datetime import datetime
from typing import Optional from typing import Optional
from fastapi import Query from fastapi import Query
@ -8,13 +9,13 @@ class CreateEvent(BaseModel):
wallet: str wallet: str
name: str name: str
info: str info: str
banner: Optional[str]
closing_date: str closing_date: str
event_start_date: str event_start_date: str
event_end_date: str event_end_date: str
currency: str = "sat" currency: str = "sat"
amount_tickets: int = Query(..., ge=0) amount_tickets: int = Query(..., ge=0)
price_per_ticket: float = Query(..., ge=0) price_per_ticket: float = Query(..., ge=0)
banner: Optional[str] = None
class CreateTicket(BaseModel): class CreateTicket(BaseModel):
@ -27,15 +28,15 @@ class Event(BaseModel):
wallet: str wallet: str
name: str name: str
info: str info: str
banner: Optional[str]
closing_date: str closing_date: str
event_start_date: str event_start_date: str
event_end_date: str event_end_date: str
currency: str currency: str
amount_tickets: int amount_tickets: int
price_per_ticket: float price_per_ticket: float
sold: int time: datetime
time: int sold: int = 0
banner: Optional[str] = None
class Ticket(BaseModel): class Ticket(BaseModel):
@ -45,6 +46,6 @@ class Ticket(BaseModel):
name: str name: str
email: str email: str
registered: bool registered: bool
reg_timestamp: Optional[int]
paid: bool paid: bool
time: int time: datetime
reg_timestamp: datetime

2186
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ authors = ["Alan Bits <alan@lnbits.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10 | ^3.9" python = "^3.10 | ^3.9"
lnbits = "*" lnbits = {version = "*", allow-prereleases = true}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^24.3.0" black = "^24.3.0"

18
services.py Normal file
View file

@ -0,0 +1,18 @@
from .crud import get_event, update_event, update_ticket
from .models import Ticket
async def set_ticket_paid(ticket: Ticket) -> Ticket:
if ticket.paid:
return ticket
ticket.paid = True
await update_ticket(ticket)
event = await get_event(ticket.event)
assert event, "Couldn't get event from ticket being paid"
event.sold += 1
event.amount_tickets -= 1
await update_event(event)
return ticket

134
static/js/display.js Normal file
View file

@ -0,0 +1,134 @@
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
data() {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {
name: '',
email: ''
}
},
ticketLink: {
show: false,
data: {
link: ''
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null
}
}
},
async created() {
this.info = event_info
this.info = this.info.substring(1, this.info.length - 1)
this.banner = event_banner
await this.purgeUnpaidTickets()
},
computed: {
formatDescription() {
return LNbits.utils.convertMarkdown(this.info)
}
},
methods: {
resetForm(e) {
e.preventDefault()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
},
closeReceiveDialog() {
const checker = this.receive.paymentChecker
dismissMsg()
clearInterval(paymentChecker)
setTimeout(() => {}, 10000)
},
nameValidation(val) {
const regex = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/g
return (
!regex.test(val) ||
'Please enter valid name. No special character allowed.'
)
},
emailValidation(val) {
const regex = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/
return regex.test(val) || 'Please enter valid email.'
},
Invoice() {
axios
.post(`/events/api/v1/tickets/${event_id}`, {
name: this.formDialog.data.name,
email: this.formDialog.data.email
})
.then(response => {
this.paymentReq = response.data.payment_request
this.paymentCheck = response.data.payment_hash
dismissMsg = Quasar.Notify.create({
timeout: 0,
message: 'Waiting for payment...'
})
this.receive = {
show: true,
status: 'pending',
paymentReq: this.paymentReq
}
paymentChecker = setInterval(() => {
axios
.post(`/events/api/v1/tickets/${event_id}/${this.paymentCheck}`, {
event: event_id,
event_name: event_name,
name: this.formDialog.data.name,
email: this.formDialog.data.email
})
.then(res => {
if (res.data.paid) {
clearInterval(paymentChecker)
dismissMsg()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
Quasar.Notify.create({
type: 'positive',
message: 'Sent, thank you!',
icon: null
})
this.receive = {
show: false,
status: 'complete',
paymentReq: null
}
this.ticketLink = {
show: true,
data: {
link: `/events/ticket/${res.data.ticket_id}`
}
}
setTimeout(() => {
window.location.href = `/events/ticket/${res.data.ticket_id}`
}, 5000)
}
})
.catch(LNbits.utils.notifyApiError)
}, 2000)
})
.catch(LNbits.utils.notifyApiError)
},
async purgeUnpaidTickets() {
try {
await LNbits.api.request('GET', `/events/api/v1/purge/${event_id}`)
} catch (error) {
LNbits.utils.notifyApiError(error)
}
}
}
})

228
static/js/index.js Normal file
View file

@ -0,0 +1,228 @@
const mapEvents = function (obj) {
obj.date = Quasar.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.price_per_ticket)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
data() {
return {
events: [],
tickets: [],
currencies: [],
eventsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'info', align: 'left', label: 'Info', field: 'info'},
{name: 'banner', align: 'left', label: 'Banner', field: 'banner'},
{
name: 'event_start_date',
align: 'left',
label: 'Start date',
field: 'event_start_date'
},
{
name: 'event_end_date',
align: 'left',
label: 'End date',
field: 'event_end_date'
},
{
name: 'closing_date',
align: 'left',
label: 'Ticket close',
field: 'closing_date'
},
{
name: 'price_per_ticket',
align: 'left',
label: 'Price',
field: row => {
if (row.currency != 'sats') {
return LNbits.utils.formatCurrency(
row.price_per_ticket.toFixed(2),
row.currency
)
}
return row.price_per_ticket
}
},
{
name: 'amount_tickets',
align: 'left',
label: 'No tickets',
field: 'amount_tickets'
},
{
name: 'sold',
align: 'left',
label: 'Sold',
field: 'sold'
}
],
pagination: {
rowsPerPage: 10
}
},
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'event', align: 'left', label: 'Event', field: 'event'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
getTickets() {
LNbits.api
.request(
'GET',
'/events/api/v1/tickets?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.tickets = response.data
.map(function (obj) {
return mapEvents(obj)
})
.filter(e => e.paid)
})
},
deleteTicket(ticketId) {
const tickets = _.findWhere(this.tickets, {id: ticketId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this ticket')
.onOk(() => {
LNbits.api
.request(
'DELETE',
'/events/api/v1/tickets/' + ticketId,
_.findWhere(this.g.user.wallets, {id: tickets.wallet}).inkey
)
.then(response => {
this.tickets = _.reject(this.tickets, function (obj) {
return obj.id == ticketId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportticketsCSV() {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
getEvents() {
LNbits.api
.request(
'GET',
'/events/api/v1/events?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.events = response.data.map(function (obj) {
return mapEvents(obj)
})
})
},
sendEventData() {
const wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
const data = this.formDialog.data
if (data.id) {
this.updateEvent(wallet, data)
} else {
this.createEvent(wallet, data)
}
},
createEvent(wallet, data) {
LNbits.api
.request('POST', '/events/api/v1/events', wallet.adminkey, data)
.then(response => {
this.events.push(mapEvents(response.data))
this.formDialog.show = false
this.formDialog.data = {}
})
.catch(LNbits.utils.notifyApiError)
},
updateformDialog(formId) {
const link = _.findWhere(this.events, {id: formId})
this.formDialog.data = {...link}
this.formDialog.show = true
},
updateEvent(wallet, data) {
LNbits.api
.request(
'PUT',
'/events/api/v1/events/' + data.id,
wallet.adminkey,
data
)
.then(response => {
this.events = _.reject(this.events, function (obj) {
return obj.id == data.id
})
this.events.push(mapEvents(response.data))
this.formDialog.show = false
this.formDialog.data = {}
})
.catch(LNbits.utils.notifyApiError)
},
deleteEvent(eventsId) {
const events = _.findWhere(this.events, {id: eventsId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this form link?')
.onOk(() => {
LNbits.api
.request(
'DELETE',
'/events/api/v1/events/' + eventsId,
_.findWhere(this.g.user.wallets, {id: events.wallet}).adminkey
)
.then(response => {
this.events = _.reject(this.events, function (obj) {
return obj.id == eventsId
})
})
.catch(LNbits.utils.notifyApiError(error))
})
},
exporteventsCSV() {
LNbits.utils.exportCSV(this.eventsTable.columns, this.events)
}
},
async created() {
if (this.g.user.wallets.length) {
this.getTickets()
this.getEvents()
this.currencies = await LNbits.api.getCurrencies()
}
}
})

78
static/js/register.js Normal file
View file

@ -0,0 +1,78 @@
const mapEvents = function (obj) {
obj.date = Quasar.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
data() {
return {
tickets: [],
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
sendCamera: {
show: false,
camera: 'auto'
}
}
},
methods: {
hoverEmail(tmp) {
this.tickets.data.emailtemp = tmp
},
closeCamera() {
this.sendCamera.show = false
},
showCamera() {
this.sendCamera.show = true
},
decodeQR(res) {
this.sendCamera.show = false
const value = res[0].rawValue.split('//')[1]
LNbits.api
.request('GET', `/events/api/v1/register/ticket/${value}`)
.then(() => {
Quasar.Notify.create({
type: 'positive',
message: 'Registered!'
})
setTimeout(() => {
window.location.reload()
}, 2000)
})
.catch(LNbits.utils.notifyApiError)
},
getEventTickets() {
LNbits.api
.request('GET', `/events/api/v1/eventtickets/${event_id}`)
.then(response => {
this.tickets = response.data.map(obj => {
return mapEvents(obj)
})
})
.catch(LNbits.utils.notifyApiError)
}
},
created() {
this.getEventTickets()
}
})

View file

@ -2,8 +2,10 @@ import asyncio
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
from loguru import logger
from .crud import set_ticket_paid from .crud import get_ticket
from .services import set_ticket_paid
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
@ -16,12 +18,16 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
# (avoid loops) if not payment.extra or "events" != payment.extra.get("tag"):
if ( return
payment.extra
and "events" == payment.extra.get("tag") if not payment.extra.get("name") or not payment.extra.get("email"):
and payment.extra.get("name") logger.warning(f"Ticket {payment.payment_hash} missing name or email.")
and payment.extra.get("email") return
):
await set_ticket_paid(payment.payment_hash) ticket = await get_ticket(payment.payment_hash)
return if not ticket:
logger.warning(f"Ticket for payment {payment.payment_hash} not found.")
return
await set_ticket_paid(ticket)

View file

@ -73,13 +73,9 @@
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg"> <div class="text-center q-mb-lg">
<a class="text-secondary" :href="'lightning:' + receive.paymentReq"> <a class="text-secondary" :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl"> <lnbits-qrcode
<qrcode :value="'lightning:' + receive.paymentReq.toUpperCase()"
:value="'lightning:' + receive.paymentReq.toUpperCase()" ></lnbits-qrcode>
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a> </a>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
@ -94,152 +90,10 @@
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) const event_id = '{{ event_id }}'
const event_name = '{{ event_name }}'
new Vue({ const event_info = '{{ event_info | tojson }}'
el: '#vue', const event_banner = JSON.parse('{{ event_banner | tojson | safe }}')
mixins: [windowMixin],
data: function () {
return {
paymentReq: null,
redirectUrl: null,
formDialog: {
show: false,
data: {
name: '',
email: ''
}
},
ticketLink: {
show: false,
data: {
link: ''
}
},
receive: {
show: false,
status: 'pending',
paymentReq: null
}
}
},
async created() {
this.info = '{{ event_info | tojson }}'
this.info = this.info.substring(1, this.info.length - 1)
this.banner = JSON.parse('{{ event_banner | tojson |safe }}')
await this.purgeUnpaidTickets()
},
computed: {
formatDescription() {
return LNbits.utils.convertMarkdown(this.info)
}
},
methods: {
resetForm: function (e) {
e.preventDefault()
this.formDialog.data.name = ''
this.formDialog.data.email = ''
},
closeReceiveDialog: function () {
var checker = this.receive.paymentChecker
dismissMsg()
clearInterval(paymentChecker)
setTimeout(function () {}, 10000)
},
nameValidation(val) {
const regex = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/g
return (
!regex.test(val) ||
'Please enter valid name. No special character allowed.'
)
},
emailValidation(val) {
let regex = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/
return regex.test(val) || 'Please enter valid email.'
},
Invoice: function () {
var self = this
axios
.post(`/events/api/v1/tickets/{{ event_id }}`, {
name: self.formDialog.data.name,
email: self.formDialog.data.email
})
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash
dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.receive = {
show: true,
status: 'pending',
paymentReq: self.paymentReq
}
paymentChecker = setInterval(function () {
axios
.post(
`/events/api/v1/tickets/{{ event_id }}/${self.paymentCheck}`,
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (res) {
if (res.data.paid) {
clearInterval(paymentChecker)
dismissMsg()
self.formDialog.data.name = ''
self.formDialog.data.email = ''
self.$q.notify({
type: 'positive',
message: 'Sent, thank you!',
icon: null
})
self.receive = {
show: false,
status: 'complete',
paymentReq: null
}
self.ticketLink = {
show: true,
data: {
link: `/events/ticket/${res.data.ticket_id}`
}
}
setTimeout(function () {
window.location.href = `/events/ticket/${res.data.ticket_id}`
}, 5000)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
async purgeUnpaidTickets() {
try {
await LNbits.api.request('GET', `/events/api/v1/purge/{{ event_id }}`)
} catch (error) {
console.warn(error)
LNbits.utils.notifyApiError(error)
}
}
}
})
</script> </script>
<script src="{{ static_url_for('events/static', path='js/display.js') }}"></script>
{% endblock %} {% endblock %}

View file

@ -18,18 +18,14 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div> </div>
{% endblock %} {% block scripts %}
<script>
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin]
})
</script>
{% endblock %}

View file

@ -25,18 +25,17 @@
<q-table <q-table
dense dense
flat flat
:data="events" :rows="events"
row-key="id" row-key="id"
:columns="eventsTable.columns" :columns="eventsTable.columns"
:pagination.sync="eventsTable.pagination" v-model:pagination="eventsTable.pagination"
> >
{% raw %}
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} <span v-text="col.label"></span>
</q-th> </q-th>
<q-th auto-width></q-th> <q-th auto-width></q-th>
@ -67,7 +66,7 @@
></q-btn> ></q-btn>
</q-td> </q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }} <span v-text="col.value"></span>
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
<q-btn <q-btn
@ -91,7 +90,6 @@
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
{% endraw %}
</q-table> </q-table>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -111,17 +109,16 @@
<q-table <q-table
dense dense
flat flat
:data="tickets" :rows="tickets"
row-key="id" row-key="id"
:columns="ticketsTable.columns" :columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination" v-model:pagination="ticketsTable.pagination"
> >
{% raw %}
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} <span v-text="col.label"></span>
</q-th> </q-th>
</q-tr> </q-tr>
</template> </template>
@ -141,7 +138,7 @@
</q-td> </q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }} <span v-text="col.value"></span>
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
@ -156,7 +153,6 @@
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
{% endraw %}
</q-table> </q-table>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -280,8 +276,8 @@
v-model.number="formDialog.data.price_per_ticket" v-model.number="formDialog.data.price_per_ticket"
type="number" type="number"
:label="'Price (' + formDialog.data.currency + ') *'" :label="'Price (' + formDialog.data.currency + ') *'"
:step="formDialog.data.currency != 'sat' ? '0.01' : '1'" :step="formDialog.data.currency != 'sats' ? '0.01' : '1'"
:mask="formDialog.data.currency != 'sat' ? '#.##' : '#'" :mask="formDialog.data.currency != 'sats' ? '#.##' : '#'"
fill-mask="0" fill-mask="0"
reverse-fill-mask reverse-fill-mask
></q-input> ></q-input>
@ -318,264 +314,5 @@
overflow-x: hidden; overflow-x: hidden;
} }
</style> </style>
<script> <script src="{{ static_url_for('events/static', path='js/index.js') }}"></script>
var mapEvents = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
events: [],
tickets: [],
currencies: [],
eventsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'info', align: 'left', label: 'Info', field: 'info'},
{name: 'banner', align: 'left', label: 'Banner', field: 'banner'},
{
name: 'event_start_date',
align: 'left',
label: 'Start date',
field: 'event_start_date'
},
{
name: 'event_end_date',
align: 'left',
label: 'End date',
field: 'event_end_date'
},
{
name: 'closing_date',
align: 'left',
label: 'Ticket close',
field: 'closing_date'
},
{
name: 'price_per_ticket',
align: 'left',
label: 'Price',
field: row => {
if (row.currency != 'sat') {
return LNbits.utils.formatCurrency(
row.price_per_ticket.toFixed(2),
row.currency
)
}
return row.price_per_ticket
}
},
{
name: 'amount_tickets',
align: 'left',
label: 'No tickets',
field: 'amount_tickets'
},
{
name: 'sold',
align: 'left',
label: 'Sold',
field: 'sold'
}
],
pagination: {
rowsPerPage: 10
}
},
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'event', align: 'left', label: 'Event', field: 'event'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
getTickets: function () {
var self = this
LNbits.api
.request(
'GET',
'/events/api/v1/tickets?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tickets = response.data
.map(function (obj) {
return mapEvents(obj)
})
.filter(e => e.paid)
})
},
deleteTicket: function (ticketId) {
var self = this
var tickets = _.findWhere(this.tickets, {id: ticketId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this ticket')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/events/api/v1/tickets/' + ticketId,
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
)
.then(function (response) {
self.tickets = _.reject(self.tickets, function (obj) {
return obj.id == ticketId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
getEvents: function () {
var self = this
LNbits.api
.request(
'GET',
'/events/api/v1/events?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.events = response.data.map(function (obj) {
return mapEvents(obj)
})
})
},
sendEventData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
var data = this.formDialog.data
if (data.id) {
this.updateEvent(wallet, data)
} else {
this.createEvent(wallet, data)
}
},
createEvent: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/events/api/v1/events', wallet.adminkey, data)
.then(function (response) {
self.events.push(mapEvents(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateformDialog: function (formId) {
var link = _.findWhere(this.events, {id: formId})
this.formDialog.data = {...link}
this.formDialog.show = true
},
updateEvent: function (wallet, data) {
var self = this
LNbits.api
.request(
'PUT',
'/events/api/v1/events/' + data.id,
wallet.adminkey,
data
)
.then(function (response) {
self.events = _.reject(self.events, function (obj) {
return obj.id == data.id
})
self.events.push(mapEvents(response.data))
self.formDialog.show = false
self.formDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteEvent: function (eventsId) {
var self = this
var events = _.findWhere(this.events, {id: eventsId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this form link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/events/api/v1/events/' + eventsId,
_.findWhere(self.g.user.wallets, {id: events.wallet}).adminkey
)
.then(function (response) {
self.events = _.reject(self.events, function (obj) {
return obj.id == eventsId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exporteventsCSV: function () {
LNbits.utils.exportCSV(this.eventsTable.columns, this.events)
},
async getCurrencies() {
try {
const {data} = await LNbits.api.request(
'GET',
'/events/api/v1/currencies',
this.inkey
)
this.currencies = ['sat', ...data]
} catch (error) {
LNbits.utils.notifyApiError(error)
}
}
},
created: async function () {
if (this.g.user.wallets.length) {
this.getTickets()
this.getEvents()
await this.getCurrencies()
}
}
})
</script>
{% endblock %} {% endblock %}

View file

@ -22,17 +22,16 @@
<q-table <q-table
dense dense
flat flat
:data="tickets" :rows="tickets"
row-key="id" row-key="id"
:columns="ticketsTable.columns" :columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination" v-model:pagination="ticketsTable.pagination"
> >
{% raw %}
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} <span v-text="col.label"></span>
</q-th> </q-th>
</q-tr> </q-tr>
</template> </template>
@ -52,11 +51,10 @@
</q-td> </q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }} <span v-text="col.value"></span>
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
{% endraw %}
</q-table> </q-table>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -66,7 +64,7 @@
<q-card class="q-pa-lg q-pt-xl"> <q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg"> <div class="text-center q-mb-lg">
<qrcode-stream <qrcode-stream
@decode="decodeQR" @detect="decodeQR"
class="rounded-borders" class="rounded-borders"
></qrcode-stream> ></qrcode-stream>
</div> </div>
@ -80,96 +78,7 @@
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) const event_id = '{{ event_id }}'
Vue.use(VueQrcodeReader)
var mapEvents = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/events/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tickets: [],
ticketsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'registered',
align: 'left',
label: 'Registered',
field: 'registered'
}
],
pagination: {
rowsPerPage: 10
}
},
sendCamera: {
show: false,
camera: 'auto'
}
}
},
methods: {
hoverEmail: function (tmp) {
this.tickets.data.emailtemp = tmp
},
closeCamera: function () {
this.sendCamera.show = false
},
showCamera: function () {
this.sendCamera.show = true
},
decodeQR: function (res) {
this.sendCamera.show = false
var self = this
LNbits.api
.request(
'GET',
'/events/api/v1/register/ticket/' + res.split('//')[1]
)
.then(function (response) {
self.$q.notify({
type: 'positive',
message: 'Registered!'
})
setTimeout(function () {
window.location.reload()
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getEventTickets: function () {
var self = this
LNbits.api
.request(
'GET',
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
)
.then(function (response) {
self.tickets = response.data.map(function (obj) {
return mapEvents(obj)
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
},
created: function () {
this.getEventTickets()
}
})
</script> </script>
<script src="{{ static_url_for('events/static', path='js/register.js') }}"></script>
{% endblock %} {% endblock %}

View file

@ -11,12 +11,10 @@
and present it for registration! and present it for registration!
</h5> </h5>
<br /> <br />
<q-responsive :ratio="1" class="q-mb-md" style="max-width: 300px"> <lnbits-qrcode
<qrcode :value="'ticket://{{ ticket_id }}'"
:value="'ticket://{{ ticket_id }}'" :options="{width: 500}"
:options="{width: 500}" ></lnbits-qrcode>
></qrcode>
</q-responsive>
<br /> <br />
<q-btn @click="printWindow" color="grey" class="q-ml-auto"> <q-btn @click="printWindow" color="grey" class="q-ml-auto">
<q-icon left size="3em" name="print"></q-icon> Print</q-btn <q-icon left size="3em" name="print"></q-icon> Print</q-btn
@ -28,15 +26,11 @@
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) window.app = Vue.createApp({
new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () {
return {}
},
methods: { methods: {
printWindow: function () { printWindow() {
window.print() window.print()
} }
} }

View file

@ -2,7 +2,6 @@ from datetime import date, datetime
from http import HTTPStatus from http import HTTPStatus
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
@ -12,7 +11,6 @@ from starlette.responses import HTMLResponse
from .crud import get_event, get_ticket from .crud import get_event, get_ticket
events_generic_router = APIRouter() events_generic_router = APIRouter()
templates = Jinja2Templates(directory="templates")
def events_renderer(): def events_renderer():
@ -22,7 +20,7 @@ def events_renderer():
@events_generic_router.get("/", response_class=HTMLResponse) @events_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return events_renderer().TemplateResponse( return events_renderer().TemplateResponse(
"events/index.html", {"request": request, "user": user.dict()} "events/index.html", {"request": request, "user": user.json()}
) )

View file

@ -1,15 +1,16 @@
from datetime import datetime, timezone
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from lnbits.core.crud import get_standalone_payment, get_user from lnbits.core.crud import get_standalone_payment, get_user
from lnbits.core.models import WalletTypeInfo from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.decorators import ( from lnbits.decorators import (
get_key_type,
require_admin_key, require_admin_key,
require_invoice_key,
) )
from lnbits.utils.exchange_rates import ( from lnbits.utils.exchange_rates import (
currencies,
fiat_amount_as_satoshis, fiat_amount_as_satoshis,
get_fiat_rate_satoshis, get_fiat_rate_satoshis,
) )
@ -27,18 +28,19 @@ from .crud import (
get_ticket, get_ticket,
get_tickets, get_tickets,
purge_unpaid_tickets, purge_unpaid_tickets,
reg_ticket,
set_ticket_paid,
update_event, update_event,
update_ticket,
) )
from .models import CreateEvent, CreateTicket from .models import CreateEvent, CreateTicket, Ticket
from .services import set_ticket_paid
events_api_router = APIRouter() events_api_router = APIRouter()
@events_api_router.get("/api/v1/events") @events_api_router.get("/api/v1/events")
async def api_events( async def api_events(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) all_wallets: bool = Query(False),
wallet: WalletTypeInfo = Depends(require_invoice_key),
): ):
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
@ -53,8 +55,8 @@ async def api_events(
@events_api_router.put("/api/v1/events/{event_id}") @events_api_router.put("/api/v1/events/{event_id}")
async def api_event_create( async def api_event_create(
data: CreateEvent, data: CreateEvent,
event_id=None,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
event_id: Optional[str] = None,
): ):
if event_id: if event_id:
event = await get_event(event_id) event = await get_event(event_id)
@ -67,16 +69,18 @@ async def api_event_create(
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your event." status_code=HTTPStatus.FORBIDDEN, detail="Not your event."
) )
event = await update_event(event_id, **data.dict()) for k, v in data.dict().items():
setattr(event, k, v)
event = await update_event(event)
else: else:
event = await create_event(data=data) event = await create_event(data)
return event.dict() return event.dict()
@events_api_router.delete("/api/v1/events/{event_id}") @events_api_router.delete("/api/v1/events/{event_id}")
async def api_form_delete( async def api_form_delete(
event_id, wallet: WalletTypeInfo = Depends(require_admin_key) event_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
event = await get_event(event_id) event = await get_event(event_id)
if not event: if not event:
@ -97,15 +101,16 @@ async def api_form_delete(
@events_api_router.get("/api/v1/tickets") @events_api_router.get("/api/v1/tickets")
async def api_tickets( async def api_tickets(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) all_wallets: bool = Query(False),
): wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> list[Ticket]:
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
if all_wallets: if all_wallets:
user = await get_user(wallet.wallet.user) user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else [] wallet_ids = user.wallet_ids if user else []
return [ticket.dict() for ticket in await get_tickets(wallet_ids)] return await get_tickets(wallet_ids)
@events_api_router.post("/api/v1/tickets/{event_id}") @events_api_router.post("/api/v1/tickets/{event_id}")
@ -126,7 +131,7 @@ async def api_ticket_make_ticket(event_id, name, email):
price = event.price_per_ticket price = event.price_per_ticket
extra = {"tag": "events", "name": name, "email": email} extra = {"tag": "events", "name": name, "email": email}
if event.currency != "sat": if event.currency != "sats":
price = await fiat_amount_as_satoshis(event.price_per_ticket, event.currency) price = await fiat_amount_as_satoshis(event.price_per_ticket, event.currency)
extra["fiat"] = True extra["fiat"] = True
@ -174,7 +179,7 @@ async def api_ticket_send_ticket(event_id, payment_hash):
assert payment assert payment
price = ( price = (
event.price_per_ticket * 1000 event.price_per_ticket * 1000
if event.currency == "sat" if event.currency == "sats"
else await fiat_amount_as_satoshis(event.price_per_ticket, event.currency) else await fiat_amount_as_satoshis(event.price_per_ticket, event.currency)
* 1000 * 1000
) )
@ -182,14 +187,16 @@ async def api_ticket_send_ticket(event_id, payment_hash):
lower_bound = price * 0.99 # 1% decrease lower_bound = price * 0.99 # 1% decrease
if not payment.pending and abs(payment.amount) >= lower_bound: # allow 1% error if not payment.pending and abs(payment.amount) >= lower_bound: # allow 1% error
await set_ticket_paid(payment_hash) await set_ticket_paid(ticket)
return {"paid": True, "ticket_id": ticket.id} return {"paid": True, "ticket_id": ticket.id}
return {"paid": False} return {"paid": False}
@events_api_router.delete("/api/v1/tickets/{ticket_id}") @events_api_router.delete("/api/v1/tickets/{ticket_id}")
async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_ticket_delete(
ticket_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
):
ticket = await get_ticket(ticket_id) ticket = await get_ticket(ticket_id)
if not ticket: if not ticket:
raise HTTPException( raise HTTPException(
@ -200,11 +207,11 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.") raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
await delete_ticket(ticket_id) await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT
# TODO: DELETE, updates db! @tal
@events_api_router.get("/api/v1/purge/{event_id}") @events_api_router.get("/api/v1/purge/{event_id}")
async def api_event_purge_tickets(event_id): async def api_event_purge_tickets(event_id: str):
event = await get_event(event_id) event = await get_event(event_id)
if not event: if not event:
raise HTTPException( raise HTTPException(
@ -213,19 +220,14 @@ async def api_event_purge_tickets(event_id):
return await purge_unpaid_tickets(event_id) return await purge_unpaid_tickets(event_id)
# Event Tickets @events_api_router.get("/api/v1/eventtickets/{event_id}")
async def api_event_tickets(event_id: str) -> list[Ticket]:
return await get_event_tickets(event_id)
@events_api_router.get("/api/v1/eventtickets/{wallet_id}/{event_id}")
async def api_event_tickets(wallet_id, event_id):
return [
ticket.dict()
for ticket in await get_event_tickets(wallet_id=wallet_id, event_id=event_id)
]
# TODO: PUT, updates db! @tal
@events_api_router.get("/api/v1/register/ticket/{ticket_id}") @events_api_router.get("/api/v1/register/ticket/{ticket_id}")
async def api_event_register_ticket(ticket_id): async def api_event_register_ticket(ticket_id) -> list[Ticket]:
ticket = await get_ticket(ticket_id) ticket = await get_ticket(ticket_id)
if not ticket: if not ticket:
@ -243,9 +245,7 @@ async def api_event_register_ticket(ticket_id):
status_code=HTTPStatus.FORBIDDEN, detail="Ticket already registered" status_code=HTTPStatus.FORBIDDEN, detail="Ticket already registered"
) )
return [ticket.dict() for ticket in await reg_ticket(ticket_id)] ticket.registered = True
ticket.reg_timestamp = datetime.now(timezone.utc)
await update_ticket(ticket)
@events_api_router.get("/api/v1/currencies") return await get_event_tickets(ticket.event)
async def api_list_currencies_available():
return list(currencies.keys())