Removes events

This commit is contained in:
ben 2023-02-11 08:53:24 +00:00
parent 252ff65563
commit 82add2189a
17 changed files with 0 additions and 1714 deletions

View File

@ -1,33 +0,0 @@
# Events
## Sell tickets for events and use the built-in scanner for registering attendants
Events alows you to make tickets for an event. Each ticket is in the form of a uniqque QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance.
Events includes a shareable ticket scanner, which can be used to register attendees.
## Usage
1. Create an event\
![create event](https://i.imgur.com/dadK1dp.jpg)
2. Fill out the event information:
- event name
- wallet (normally there's only one)
- event information
- closing date for event registration
- begin and end date of the event
![event info](https://imgur.com/KAv68Yr.jpg)
3. Share the event registration link\
![event ticket](https://imgur.com/AQWUOBY.jpg)
- ticket example\
![ticket example](https://i.imgur.com/trAVSLd.jpg)
- QR code ticket, presented after invoice paid, to present at registration\
![event ticket](https://i.imgur.com/M0ROM82.jpg)
4. Use the built-in ticket scanner to validate registered, and paid, attendees\
![ticket scanner](https://i.imgur.com/zrm9202.jpg)

View File

@ -1,35 +0,0 @@
import asyncio
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_events")
events_ext: APIRouter = APIRouter(prefix="/events", tags=["Events"])
events_static_files = [
{
"path": "/events/static",
"app": StaticFiles(packages=[("lnbits", "extensions/events/static")]),
"name": "events_static",
}
]
def events_renderer():
return template_renderer(["lnbits/extensions/events/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
def events_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View File

@ -1,6 +0,0 @@
{
"name": "Events",
"short_description": "Sell and register event tickets",
"tile": "/events/static/image/events.png",
"contributors": ["benarc"]
}

View File

@ -1,144 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import CreateEvent, Events, Tickets
# TICKETS
async def create_ticket(
payment_hash: str, wallet: str, event: str, name: str, email: str
) -> Tickets:
await db.execute(
"""
INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(payment_hash, wallet, event, name, email, False, True),
)
# UPDATE EVENT DATA ON SOLD TICKET
eventdata = await get_event(event)
assert eventdata, "Couldn't get event from ticket being paid"
sold = eventdata.sold + 1
amount_tickets = eventdata.amount_tickets - 1
await db.execute(
"""
UPDATE events.events
SET sold = ?, amount_tickets = ?
WHERE id = ?
""",
(sold, amount_tickets, event),
)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly created ticket couldn't be retrieved"
return ticket
async def get_ticket(payment_hash: str) -> Optional[Tickets]:
row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,))
return Tickets(**row) if row else None
async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
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,)
)
return [Tickets(**row) for row in rows]
async def delete_ticket(payment_hash: str) -> None:
await db.execute("DELETE FROM events.ticket WHERE id = ?", (payment_hash,))
async def delete_event_tickets(event_id: str) -> None:
await db.execute("DELETE FROM events.ticket WHERE event = ?", (event_id,))
# EVENTS
async def create_event(data: CreateEvent) -> Events:
event_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO events.events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
event_id,
data.wallet,
data.name,
data.info,
data.closing_date,
data.event_start_date,
data.event_end_date,
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
async def update_event(event_id: str, **kwargs) -> Events:
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"
return event
async def get_event(event_id: str) -> Optional[Events]:
row = await db.fetchone("SELECT * FROM events.events WHERE id = ?", (event_id,))
return Events(**row) if row else None
async def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
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 [Events(**row) for row in rows]
async def delete_event(event_id: str) -> None:
await db.execute("DELETE FROM events.events WHERE id = ?", (event_id,))
# EVENTTICKETS
async def get_event_tickets(event_id: str, wallet_id: str) -> List[Tickets]:
rows = await db.fetchall(
"SELECT * FROM events.ticket WHERE wallet = ? AND event = ?",
(wallet_id, event_id),
)
return [Tickets(**row) for row in rows]
async def reg_ticket(ticket_id: str) -> List[Tickets]:
await db.execute(
"UPDATE events.ticket SET registered = ? 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 [Tickets(**row) for row in rows]

View File

@ -1,83 +0,0 @@
async def m001_initial(db):
await db.execute(
"""
CREATE TABLE events.events (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
info TEXT NOT NULL,
closing_date TEXT NOT NULL,
event_start_date TEXT NOT NULL,
event_end_date TEXT NOT NULL,
amount_tickets INTEGER NOT NULL,
price_per_ticket INTEGER NOT NULL,
sold INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
await db.execute(
"""
CREATE TABLE events.tickets (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
event TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
registered BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
async def m002_changed(db):
await db.execute(
"""
CREATE TABLE events.ticket (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
event TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
registered BOOLEAN NOT NULL,
paid BOOLEAN NOT NULL,
time TIMESTAMP NOT NULL DEFAULT """
+ db.timestamp_now
+ """
);
"""
)
for row in [list(row) for row in await db.fetchall("SELECT * FROM events.tickets")]:
usescsv = ""
for i in range(row[5]):
if row[7]:
usescsv += "," + str(i + 1)
else:
usescsv += "," + str(1)
usescsv = usescsv[1:]
await db.execute(
"""
INSERT INTO events.ticket (
id,
wallet,
event,
name,
email,
registered,
paid
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3], row[4], row[5], True),
)
await db.execute("DROP TABLE events.tickets")

View File

@ -1,43 +0,0 @@
from fastapi.param_functions import Query
from pydantic import BaseModel
class CreateEvent(BaseModel):
wallet: str
name: str
info: str
closing_date: str
event_start_date: str
event_end_date: str
amount_tickets: int = Query(..., ge=0)
price_per_ticket: int = Query(..., ge=0)
class CreateTicket(BaseModel):
name: str
email: str
class Events(BaseModel):
id: str
wallet: str
name: str
info: str
closing_date: str
event_start_date: str
event_end_date: str
amount_tickets: int
price_per_ticket: int
sold: int
time: int
class Tickets(BaseModel):
id: str
wallet: str
event: str
name: str
email: str
registered: bool
paid: bool
time: int

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

View File

@ -1,36 +0,0 @@
import asyncio
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .models import CreateTicket
from .views_api import api_ticket_send_ticket
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name())
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
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 api_ticket_send_ticket(
payment.memo,
payment.payment_hash,
CreateTicket(
name=str(payment.extra.get("name")),
email=str(payment.extra.get("email")),
),
)
return

View File

@ -1,25 +0,0 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Events: Sell and register ticket waves for an event
</h5>
<p>
Events alows you to make a wave of tickets for an event, each ticket is
in the form of a unqiue QRcode, which the user presents at registration.
Events comes with a shareable ticket scanner, which can be used to
register attendees.<br />
<small>
Created by,
<a class="text-secondary" href="https://github.com/benarc">Ben Arc</a>
</small>
</p>
</q-card-section>
</q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/events"></q-btn>
</q-expansion-item>

View File

@ -1,216 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h3 class="q-my-none">{{ event_name }}</h3>
<br />
<h5 class="q-my-none">{{ event_info }}</h5>
<br />
<q-form @submit="Invoice()" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Your name "
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.email"
type="email"
label="Your email "
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
type="submit"
>Submit</q-btn
>
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card-section>
</q-card>
<q-card v-show="ticketLink.show" class="q-pa-lg">
<div class="text-center q-mb-lg">
<q-btn
unelevated
size="xl"
:href="ticketLink.data.link"
target="_blank"
color="primary"
type="a"
>Link to your ticket!</q-btn
>
<br /><br />
<p>You'll be redirected in a few moments...</p>
</div>
</q-card>
</div>
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
</q-card>
<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>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %}
<script>
console.log('{{ form_costpword }}')
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
}
}
},
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)
},
Invoice: function () {
var self = this
axios
.get(
'/events/api/v1/tickets/' +
'{{ event_id }}' +
'/' +
self.formDialog.data.name +
'/' +
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)
})
}
}
})
</script>
{% endblock %}

View File

@ -1,35 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ event_name }} error</h3>
<br />
<q-icon
name="warning"
class="text-grey"
style="font-size: 20rem"
></q-icon>
<h5 class="q-my-none">{{ event_error }}</h5>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>

View File

@ -1,538 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New Event</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Events</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exporteventsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="events"
row-key="id"
:columns="eventsTable.columns"
:pagination.sync="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 }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="link"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="how_to_reg"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/register/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateformDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteEvent(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportticketsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="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 }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="local_activity"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/ticket/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTicket(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Events extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "events/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendEventData" class="q-gutter-md">
<div class="row">
<div class="col">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
type="name"
label="Title of event "
></q-input>
</div>
<div class="col q-pl-sm">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
</div>
</div>
<q-input
filled
dense
v-model.trim="formDialog.data.info"
type="textarea"
label="Info about the event "
></q-input>
<div class="row">
<div class="col-4">Ticket closing date</div>
<div class="col-8">
<q-input
filled
dense
v-model.trim="formDialog.data.closing_date"
type="date"
></q-input>
</div>
</div>
<div class="row">
<div class="col-4">Event begins</div>
<div class="col-8">
<q-input
filled
dense
v-model.trim="formDialog.data.event_start_date"
type="date"
></q-input>
</div>
</div>
<div class="row">
<div class="col-4">Event ends</div>
<div class="col-8">
<q-input
filled
dense
v-model.trim="formDialog.data.event_end_date"
type="date"
></q-input>
</div>
</div>
<div class="row">
<div class="col">
<q-input
filled
dense
v-model.number="formDialog.data.amount_tickets"
type="number"
label="Amount of tickets "
></q-input>
</div>
<div class="col q-pl-sm">
<q-input
filled
dense
v-model.number="formDialog.data.price_per_ticket"
type="number"
label="Sats per ticket "
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Event</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="formDialog.data.wallet == null || formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || formDialog.data.price_per_ticket == null"
type="submit"
>Create Event</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<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: [],
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: '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: '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) {
console.log(response)
self.tickets = response.data.map(function (obj) {
console.log(obj)
return mapEvents(obj)
})
})
},
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.inkey, 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})
console.log(link.id)
this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name
this.formDialog.data.info = link.info
this.formDialog.data.closing_date = link.closing_date
this.formDialog.data.event_start_date = link.event_start_date
this.formDialog.data.event_end_date = link.event_end_date
this.formDialog.data.amount_tickets = link.amount_tickets
this.formDialog.data.price_per_ticket = link.price_per_ticket
this.formDialog.show = true
},
updateEvent: function (wallet, data) {
var self = this
console.log(data)
LNbits.api
.request(
'PUT',
'/events/api/v1/events/' + data.id,
wallet.inkey,
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}).inkey
)
.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)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getTickets()
this.getEvents()
}
}
})
</script>
{% endblock %}

View File

@ -1,176 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ event_name }} Registration</h3>
<br />
<br />
<q-btn unelevated color="primary" @click="showCamera" size="xl"
>Scan ticket</q-btn
>
</center>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<q-table
dense
flat
:data="tickets"
row-key="id"
:columns="ticketsTable.columns"
:pagination.sync="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 }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="local_activity"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="'/events/ticket/' + props.row.id"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="sendCamera.show" position="top">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
</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
console.log('obj')
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>
{% endblock %}

View File

@ -1,44 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">{{ ticket_name }} Ticket</h3>
<br />
<h5 class="q-my-none">
Bookmark, print or screenshot this page,<br />
and present it for registration!
</h5>
<br />
<qrcode
:value="'ticket://{{ ticket_id }}'"
:options="{width: 500}"
></qrcode>
<br />
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
<q-icon left size="3em" name="print"></q-icon> Print</q-btn
>
</center>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
},
methods: {
printWindow: function () {
window.print()
}
}
})
</script>
{% endblock %}

View File

@ -1,106 +0,0 @@
from datetime import date, datetime
from http import HTTPStatus
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import events_ext, events_renderer
from .crud import get_event, get_ticket
templates = Jinja2Templates(directory="templates")
@events_ext.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_ext.get("/{event_id}", response_class=HTMLResponse)
async def display(request: Request, event_id):
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
if event.amount_tickets < 1:
return events_renderer().TemplateResponse(
"events/error.html",
{
"request": request,
"event_name": event.name,
"event_error": "Sorry, tickets are sold out :(",
},
)
datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date()
if date.today() > datetime_object:
return events_renderer().TemplateResponse(
"events/error.html",
{
"request": request,
"event_name": event.name,
"event_error": "Sorry, ticket closing date has passed :(",
},
)
return events_renderer().TemplateResponse(
"events/display.html",
{
"request": request,
"event_id": event_id,
"event_name": event.name,
"event_info": event.info,
"event_price": event.price_per_ticket,
},
)
@events_ext.get("/ticket/{ticket_id}", response_class=HTMLResponse)
async def ticket(request: Request, ticket_id):
ticket = await get_ticket(ticket_id)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
)
event = await get_event(ticket.event)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
return events_renderer().TemplateResponse(
"events/ticket.html",
{
"request": request,
"ticket_id": ticket_id,
"ticket_name": event.name,
"ticket_info": event.info,
},
)
@events_ext.get("/register/{event_id}", response_class=HTMLResponse)
async def register(request: Request, event_id):
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
return events_renderer().TemplateResponse(
"events/register.html",
{
"request": request,
"event_id": event_id,
"event_name": event.name,
"wallet_id": event.wallet,
},
)

View File

@ -1,194 +0,0 @@
from http import HTTPStatus
from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type
from . import events_ext
from .crud import (
create_event,
create_ticket,
delete_event,
delete_event_tickets,
delete_ticket,
get_event,
get_event_tickets,
get_events,
get_ticket,
get_tickets,
reg_ticket,
update_event,
)
from .models import CreateEvent, CreateTicket
# Events
@events_ext.get("/api/v1/events")
async def api_events(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [event.dict() for event in await get_events(wallet_ids)]
@events_ext.post("/api/v1/events")
@events_ext.put("/api/v1/events/{event_id}")
async def api_event_create(
data: CreateEvent, event_id=None, wallet: WalletTypeInfo = Depends(get_key_type)
):
if event_id:
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
if event.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your event."
)
event = await update_event(event_id, **data.dict())
else:
event = await create_event(data=data)
return event.dict()
@events_ext.delete("/api/v1/events/{event_id}")
async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_type)):
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
if event.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your event.")
await delete_event(event_id)
await delete_event_tickets(event_id)
return "", HTTPStatus.NO_CONTENT
#########Tickets##########
@events_ext.get("/api/v1/tickets")
async def api_tickets(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
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)]
@events_ext.get("/api/v1/tickets/{event_id}/{name}/{email}")
async def api_ticket_make_ticket(event_id, name, email):
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
try:
payment_hash, payment_request = await create_invoice(
wallet_id=event.wallet,
amount=event.price_per_ticket,
memo=f"{event_id}",
extra={"tag": "events", "name": name, "email": email},
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
return {"payment_hash": payment_hash, "payment_request": payment_request}
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Event could not be fetched.",
)
status = await api_payment(payment_hash)
if status["paid"]:
exists = await get_ticket(payment_hash)
if exists:
return {"paid": True, "ticket_id": exists.id}
ticket = await create_ticket(
payment_hash=payment_hash,
wallet=event.wallet,
event=event_id,
name=data.name,
email=data.email,
)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Event could not be fetched.",
)
return {"paid": True, "ticket_id": ticket.id}
return {"paid": False}
@events_ext.delete("/api/v1/tickets/{ticket_id}")
async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_type)):
ticket = await get_ticket(ticket_id)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
)
if ticket.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT
# Event Tickets
@events_ext.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_ext.get("/api/v1/register/ticket/{ticket_id}")
async def api_event_register_ticket(ticket_id):
ticket = await get_ticket(ticket_id)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
)
if not ticket.paid:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Ticket not paid for."
)
if ticket.registered is True:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Ticket already registered"
)
return [ticket.dict() for ticket in await reg_ticket(ticket_id)]

Binary file not shown.