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",
"short_description": "Sell and register event tickets",
"tile": "/events/static/image/events.png",
"min_lnbits_version": "0.12.5",
"min_lnbits_version": "1.0.0",
"contributors": [
{
"name": "talvasconcelos",

186
crud.py
View file

@ -1,5 +1,5 @@
from datetime import datetime, timedelta
from typing import List, Optional, Union
from datetime import datetime, timedelta, timezone
from typing import Optional, Union
from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash
@ -12,177 +12,103 @@ db = Database("ext_events")
async def create_ticket(
payment_hash: str, wallet: str, event: str, name: str, email: str
) -> Ticket:
await db.execute(
"""
INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(payment_hash, wallet, event, name, email, False, False),
now = datetime.now(timezone.utc)
ticket = Ticket(
id=payment_hash,
wallet=wallet,
event=event,
name=name,
email=email,
registered=False,
paid=False,
reg_timestamp=now,
time=now,
)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly created ticket couldn't be retrieved"
await db.insert("events.ticket", ticket)
return ticket
async def set_ticket_paid(payment_hash: str) -> Ticket:
ticket = await get_ticket(payment_hash)
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)
async def update_ticket(ticket: Ticket) -> Ticket:
await db.update("events.ticket", 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]:
row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,))
return Ticket(**row) if row else None
return await db.fetchone(
"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):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM events.ticket WHERE wallet IN ({q})", (*wallet_ids,)
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
return await db.fetchall(
f"SELECT * FROM events.ticket WHERE wallet IN ({q})",
model=Ticket,
)
return [Ticket(**row) for row in rows]
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:
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:
time_diff = datetime.now() - timedelta(hours=24)
await db.execute(
f"""
DELETE FROM events.ticket WHERE event = ? AND paid = false
AND time < {db.timestamp_placeholder}
DELETE FROM events.ticket WHERE event = :event AND paid = false
AND time < {db.timestamp_placeholder("time")}
""",
(
event_id,
time_diff.timestamp(),
),
{"time": time_diff.timestamp(), "event": event_id},
)
async def create_event(data: CreateEvent) -> Event:
event_id = urlsafe_short_hash()
await db.execute(
"""
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"
event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict())
await db.insert("events.events", event)
return event
async def update_event(event_id: str, **kwargs) -> Event:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
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"
async def update_event(event: Event) -> Event:
await db.update("events.events", event)
return event
async def get_event(event_id: str) -> Optional[Event]:
row = await db.fetchone("SELECT * FROM events.events WHERE id = ?", (event_id,))
return Event(**row) if row else None
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 await db.fetchone(
"SELECT * FROM events.events WHERE id = :id",
{"id": event_id},
Event,
)
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:
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, wallet_id: str) -> List[Ticket]:
rows = await db.fetchall(
"SELECT * FROM events.ticket WHERE wallet = ? AND event = ?",
(wallet_id, event_id),
async def get_event_tickets(event_id: str) -> list[Ticket]:
return await db.fetchall(
"SELECT * FROM events.ticket WHERE event = :event",
{"event": event_id},
Ticket,
)
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 fastapi import Query
@ -8,13 +9,13 @@ class CreateEvent(BaseModel):
wallet: str
name: str
info: str
banner: Optional[str]
closing_date: str
event_start_date: str
event_end_date: str
currency: str = "sat"
amount_tickets: int = Query(..., ge=0)
price_per_ticket: float = Query(..., ge=0)
banner: Optional[str] = None
class CreateTicket(BaseModel):
@ -27,15 +28,15 @@ class Event(BaseModel):
wallet: str
name: str
info: str
banner: Optional[str]
closing_date: str
event_start_date: str
event_end_date: str
currency: str
amount_tickets: int
price_per_ticket: float
sold: int
time: int
time: datetime
sold: int = 0
banner: Optional[str] = None
class Ticket(BaseModel):
@ -45,6 +46,6 @@ class Ticket(BaseModel):
name: str
email: str
registered: bool
reg_timestamp: Optional[int]
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]
python = "^3.10 | ^3.9"
lnbits = "*"
lnbits = {version = "*", allow-prereleases = true}
[tool.poetry.group.dev.dependencies]
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.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():
@ -16,12 +18,16 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
# (avoid loops)
if (
payment.extra
and "events" == payment.extra.get("tag")
and payment.extra.get("name")
and payment.extra.get("email")
):
await set_ticket_paid(payment.payment_hash)
return
if not payment.extra or "events" != payment.extra.get("tag"):
return
if not payment.extra.get("name") or not payment.extra.get("email"):
logger.warning(f"Ticket {payment.payment_hash} missing name or email.")
return
ticket = await get_ticket(payment.payment_hash)
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">
<div class="text-center q-mb-lg">
<a class="text-secondary" :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="'lightning:' + receive.paymentReq.toUpperCase()"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
<lnbits-qrcode
:value="'lightning:' + receive.paymentReq.toUpperCase()"
></lnbits-qrcode>
</a>
</div>
<div class="row q-mt-lg">
@ -94,152 +90,10 @@
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
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)
}
}
}
})
const event_id = '{{ event_id }}'
const event_name = '{{ event_name }}'
const event_info = '{{ event_info | tojson }}'
const event_banner = JSON.parse('{{ event_banner | tojson | safe }}')
</script>
<script src="{{ static_url_for('events/static', path='js/display.js') }}"></script>
{% endblock %}

View file

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

View file

@ -25,18 +25,17 @@
<q-table
dense
flat
:data="events"
:rows="events"
row-key="id"
:columns="eventsTable.columns"
:pagination.sync="eventsTable.pagination"
v-model:pagination="eventsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<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 auto-width></q-th>
@ -67,7 +66,7 @@
></q-btn>
</q-td>
<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 auto-width>
<q-btn
@ -91,7 +90,6 @@
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
@ -111,17 +109,16 @@
<q-table
dense
flat
:data="tickets"
:rows="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination"
v-model:pagination="ticketsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
<span v-text="col.label"></span>
</q-th>
</q-tr>
</template>
@ -141,7 +138,7 @@
</q-td>
<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 auto-width>
@ -156,7 +153,6 @@
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
@ -280,8 +276,8 @@
v-model.number="formDialog.data.price_per_ticket"
type="number"
:label="'Price (' + formDialog.data.currency + ') *'"
:step="formDialog.data.currency != 'sat' ? '0.01' : '1'"
:mask="formDialog.data.currency != 'sat' ? '#.##' : '#'"
:step="formDialog.data.currency != 'sats' ? '0.01' : '1'"
:mask="formDialog.data.currency != 'sats' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
></q-input>
@ -318,264 +314,5 @@
overflow-x: hidden;
}
</style>
<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>
<script src="{{ static_url_for('events/static', path='js/index.js') }}"></script>
{% endblock %}

View file

@ -22,17 +22,16 @@
<q-table
dense
flat
:data="tickets"
:rows="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="ticketsTable.pagination"
v-model:pagination="ticketsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
<span v-text="col.label"></span>
</q-th>
</q-tr>
</template>
@ -52,11 +51,10 @@
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
<span v-text="col.value"></span>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
@ -66,7 +64,7 @@
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
@detect="decodeQR"
class="rounded-borders"
></qrcode-stream>
</div>
@ -80,96 +78,7 @@
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
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()
}
})
const event_id = '{{ event_id }}'
</script>
<script src="{{ static_url_for('events/static', path='js/register.js') }}"></script>
{% endblock %}

View file

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

View file

@ -2,7 +2,6 @@ from datetime import date, datetime
from http import HTTPStatus
from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer
@ -12,7 +11,6 @@ from starlette.responses import HTMLResponse
from .crud import get_event, get_ticket
events_generic_router = APIRouter()
templates = Jinja2Templates(directory="templates")
def events_renderer():
@ -22,7 +20,7 @@ def events_renderer():
@events_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
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 typing import Optional
from fastapi import APIRouter, Depends, Query
from lnbits.core.crud import get_standalone_payment, get_user
from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import create_invoice
from lnbits.decorators import (
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.utils.exchange_rates import (
currencies,
fiat_amount_as_satoshis,
get_fiat_rate_satoshis,
)
@ -27,18 +28,19 @@ from .crud import (
get_ticket,
get_tickets,
purge_unpaid_tickets,
reg_ticket,
set_ticket_paid,
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.get("/api/v1/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]
@ -53,8 +55,8 @@ async def api_events(
@events_api_router.put("/api/v1/events/{event_id}")
async def api_event_create(
data: CreateEvent,
event_id=None,
wallet: WalletTypeInfo = Depends(require_admin_key),
event_id: Optional[str] = None,
):
if event_id:
event = await get_event(event_id)
@ -67,16 +69,18 @@ async def api_event_create(
raise HTTPException(
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:
event = await create_event(data=data)
event = await create_event(data)
return event.dict()
@events_api_router.delete("/api/v1/events/{event_id}")
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)
if not event:
@ -97,15 +101,16 @@ async def api_form_delete(
@events_api_router.get("/api/v1/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]
if all_wallets:
user = await get_user(wallet.wallet.user)
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}")
@ -126,7 +131,7 @@ async def api_ticket_make_ticket(event_id, name, email):
price = event.price_per_ticket
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)
extra["fiat"] = True
@ -174,7 +179,7 @@ async def api_ticket_send_ticket(event_id, payment_hash):
assert payment
price = (
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)
* 1000
)
@ -182,14 +187,16 @@ async def api_ticket_send_ticket(event_id, payment_hash):
lower_bound = price * 0.99 # 1% decrease
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": False}
@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)
if not ticket:
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.")
await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT
# TODO: DELETE, updates db! @tal
@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)
if not event:
raise HTTPException(
@ -213,19 +220,14 @@ async def api_event_purge_tickets(event_id):
return await purge_unpaid_tickets(event_id)
# Event Tickets
@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)
]
@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)
# TODO: PUT, updates db! @tal
@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)
if not ticket:
@ -243,9 +245,7 @@ async def api_event_register_ticket(ticket_id):
status_code=HTTPStatus.FORBIDDEN, detail="Ticket already registered"
)
return [ticket.dict() for ticket in await reg_ticket(ticket_id)]
@events_api_router.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
ticket.registered = True
ticket.reg_timestamp = datetime.now(timezone.utc)
await update_ticket(ticket)
return await get_event_tickets(ticket.event)