diff --git a/config.json b/config.json
index 11312b6..acfb7d7 100644
--- a/config.json
+++ b/config.json
@@ -6,7 +6,7 @@
"short_description": "Sell and register event tickets",
"description": "",
"tile": "/events/static/image/events.png",
- "min_lnbits_version": "1.3.0",
+ "min_lnbits_version": "1.4.1",
"contributors": [
{
"name": "talvasconcelos",
diff --git a/pyproject.toml b/pyproject.toml
index 4508802..bec6870 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,8 +10,8 @@ dependencies = [ "lnbits>1" ]
[tool.poetry]
package-mode = false
-[tool.uv]
-dev-dependencies = [
+[dependency-groups]
+dev = [
"black",
"pytest-asyncio",
"pytest",
diff --git a/static/js/display.js b/static/js/display.js
index 6098e5a..6b3bea2 100644
--- a/static/js/display.js
+++ b/static/js/display.js
@@ -1,8 +1,8 @@
-window.app = Vue.createApp({
- el: '#vue',
- mixins: [windowMixin],
+window.PageEventsDisplay = {
+ template: '#page-events-display',
data() {
return {
+ eventName: '',
paymentReq: null,
redirectUrl: null,
formDialog: {
@@ -27,11 +27,8 @@ window.app = Vue.createApp({
}
},
async created() {
- this.info = event_info
- this.info = this.info.substring(1, this.info.length - 1)
- this.banner = event_banner
- this.extra = event_extra
- this.hasPromoCodes = has_promoCodes
+ this.eventId = this.$route.params.id
+ await this.getEvent()
},
computed: {
formatDescription() {
@@ -39,6 +36,21 @@ window.app = Vue.createApp({
}
},
methods: {
+ async getEvent() {
+ try {
+ const {data} = await LNbits.api.request(
+ 'GET',
+ `/events/api/v1/events/${this.eventId}`
+ )
+ this.eventName = data.event_name
+ this.info = data.event_info
+ this.banner = data.event_banner
+ this.extra = data.event_extra
+ this.hasPromoCodes = data.has_promo_codes
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
resetForm(e) {
e.preventDefault()
this.formDialog.data.name = ''
@@ -63,68 +75,78 @@ window.app = Vue.createApp({
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,
- promo_code: this.formDialog.data.promo_code || null
- })
- .then(response => {
- this.paymentReq = response.data.payment_request
- this.paymentCheck = response.data.payment_hash
+ async createInvoice() {
+ try {
+ const {data} = await LNbits.api.request(
+ 'POST',
+ `/events/api/v1/tickets/${this.eventId}`,
+ null,
+ {
+ name: this.formDialog.data.name,
+ email: this.formDialog.data.email,
+ refund: this.formDialog.data.refund || null
+ }
+ )
+ this.paymentReq = data.payment_request
+ this.paymentCheck = data.payment_hash
- dismissMsg = Quasar.Notify.create({
- timeout: 0,
- message: 'Waiting for payment...'
- })
+ 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,
+ this.receive = {
+ show: true,
+ status: 'pending',
+ paymentReq: this.paymentReq
+ }
+ //TODO: use websockets
+ paymentChecker = setInterval(async () => {
+ try {
+ const res = await LNbits.api.request(
+ 'POST',
+ `/events/api/v1/tickets/${this.eventId}/${this.paymentCheck}`,
+ {
+ event: this.eventId,
+ event_name: this.eventName,
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 = ''
+ }
+ )
+ 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
- }
+ 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)
+ this.ticketLink = {
+ show: true,
+ data: {
+ link: `/events/ticket/${res.data.ticket_id}`
}
- })
- .catch(LNbits.utils.notifyApiError)
- }, 2000)
- })
- .catch(LNbits.utils.notifyApiError)
+ }
+ setTimeout(() => {
+ window.location.href = `/events/ticket/${res.data.ticket_id}`
+ }, 5000)
+ }
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ }, 2000)
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
}
}
-})
+}
diff --git a/static/js/display.vue b/static/js/display.vue
new file mode 100644
index 0000000..253fefe
--- /dev/null
+++ b/static/js/display.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Buy Ticket
+
+
+
+
+
+
+ Submit
+ Cancel
+
+
+
+
+
+
+
+
Link to your ticket!
+
+
You'll be redirected in a few moments...
+
+
+
+
+
+
+
+
+
+
+
+
+ Copy invoice
+ Close
+
+
+
+
+
diff --git a/static/js/index.js b/static/js/index.js
index d26133c..ccfa2ba 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -5,9 +5,8 @@ const mapEvents = function (obj) {
return obj
}
-window.app = Vue.createApp({
- el: '#vue',
- mixins: [windowMixin],
+window.PageEvents = {
+ template: '#page-events',
data() {
return {
events: [],
@@ -290,7 +289,11 @@ window.app = Vue.createApp({
if (this.g.user.wallets.length) {
this.getTickets()
this.getEvents()
- this.currencies = await LNbits.api.getCurrencies()
+ if (g.allowedCurrencies && g.allowedCurrencies.length) {
+ this.currencies = ['sats', ...g.allowedCurrencies]
+ } else {
+ this.currencies = ['sats', ...g.currencies]
+ }
}
}
-})
+}
diff --git a/static/js/index.vue b/static/js/index.vue
new file mode 100644
index 0000000..5e8cc4c
--- /dev/null
+++ b/static/js/index.vue
@@ -0,0 +1,512 @@
+
+
+
+
+
+ New Event
+
+
+
+
+
+
+
+
Events
+
+
+ Export to CSV
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Promo codes
+
+
+ No promo codes for this event.
+
+
+
+
+
+
+
+
+
+ Discount:
+ %
+
+
+ Status:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tickets
+
+
+ Export to CSV
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Events extension
+
+
+
+
+
+
+
+
+
+ Events: Sell and register ticket waves for an event
+
+
+ Events allows you to make a wave of tickets for an event,
+ each ticket is in the form of a unique QRcode, which the
+ user presents at registration. Events comes with a shareable
+ ticket scanner, which can be used to register attendees.
+
+ Created by,
+ Ben Arc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ticket closing date
+
+
+
+
+
+
+
+
+
+
+
Conditional Events
+
+ Make this event conditional if
+ minimum tickets are sold. User will be asked to
+ provide a Lightning Address or LNURL pay for refunds.
+
+
+
+
+
+
+
+
+
+ Promo Codes
+
+ Allow users to enter a promo code for discounts.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add Promo Code
+
+
+
+
+ Update Event
+ Create Event
+ Cancel
+
+
+
+
+
+
diff --git a/static/js/register.js b/static/js/register.js
index a7ab92f..32863ab 100644
--- a/static/js/register.js
+++ b/static/js/register.js
@@ -8,9 +8,8 @@ const mapEvents = function (obj) {
return obj
}
-window.app = Vue.createApp({
- el: '#vue',
- mixins: [windowMixin],
+window.PageEventsRegister = {
+ template: '#page-events-register',
data() {
return {
tickets: [],
@@ -75,4 +74,4 @@ window.app = Vue.createApp({
created() {
this.getEventTickets()
}
-})
+}
diff --git a/static/js/register.vue b/static/js/register.vue
new file mode 100644
index 0000000..34fa9a8
--- /dev/null
+++ b/static/js/register.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+ Registration
+
+
+
+
+ Scan ticket
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
diff --git a/static/js/ticket.js b/static/js/ticket.js
new file mode 100644
index 0000000..3085f47
--- /dev/null
+++ b/static/js/ticket.js
@@ -0,0 +1,26 @@
+window.PageEventsTicket = {
+ template: '#page-events-ticket',
+ data() {
+ return {
+ ticketId: null,
+ ticketName: null
+ }
+ },
+ methods: {
+ printWindow() {
+ window.print()
+ }
+ },
+ async created() {
+ this.ticketId = this.$route.params.id
+ try {
+ const {data} = await LNbits.api.request(
+ 'GET',
+ `/events/api/v1/tickets/${this.ticketId}`
+ )
+ this.ticketName = data.ticket_name
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ }
+}
diff --git a/static/js/ticket.vue b/static/js/ticket.vue
new file mode 100644
index 0000000..7fdecce
--- /dev/null
+++ b/static/js/ticket.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Ticket
+
+
+ Bookmark, print or screenshot this page,
+ and present it for registration!
+
+
+
+
+
+ Print
+
+
+
+
+
+
diff --git a/static/routes.json b/static/routes.json
new file mode 100644
index 0000000..ae46a9a
--- /dev/null
+++ b/static/routes.json
@@ -0,0 +1,26 @@
+[
+ {
+ "path": "/events/",
+ "name": "PageEvents",
+ "template": "/events/static/js/index.vue",
+ "component": "/events/static/js/index.js"
+ },
+ {
+ "path": "/events/:id",
+ "name": "PageEventsDisplay",
+ "template": "/events/static/js/display.vue",
+ "component": "/events/static/js/display.js"
+ },
+ {
+ "path": "/events/ticket/:id",
+ "name": "PageEventsTicket",
+ "template": "/events/static/js/ticket.vue",
+ "component": "/events/static/js/ticket.js"
+ },
+ {
+ "path": "/events/register/:id",
+ "name": "PageEventsRegister",
+ "template": "/events/static/js/register.vue",
+ "component": "/events/static/js/register.js"
+ }
+]
diff --git a/templates/events/_api_docs.html b/templates/events/_api_docs.html
deleted file mode 100644
index dbf0131..0000000
--- a/templates/events/_api_docs.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- Events: Sell and register ticket waves for an event
-
-
- Events allows you to make a wave of tickets for an event, each ticket is
- in the form of a unique QRcode, which the user presents at registration.
- Events comes with a shareable ticket scanner, which can be used to
- register attendees.
-
- Created by,
- Ben Arc
-
-
-
-
-
-
diff --git a/templates/events/display.html b/templates/events/display.html
deleted file mode 100644
index 73d279d..0000000
--- a/templates/events/display.html
+++ /dev/null
@@ -1,116 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
- {{ event_name }}
-
-
-
-
-
-
-
- Buy Ticket
-
-
-
-
-
-
- Submit
- Cancel
-
-
-
-
-
-
-
-
Link to your ticket!
-
-
You'll be redirected in a few moments...
-
-
-
-
-
-
-
-
-
-
-
-
- Copy invoice
- Close
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-
-{% endblock %}
diff --git a/templates/events/error.html b/templates/events/error.html
deleted file mode 100644
index 3993db5..0000000
--- a/templates/events/error.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
- {{ event_name }} error
-
-
-
- {{ event_error }}
-
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-
-
-{% endblock %}
diff --git a/templates/events/index.html b/templates/events/index.html
deleted file mode 100644
index 62752d1..0000000
--- a/templates/events/index.html
+++ /dev/null
@@ -1,464 +0,0 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New Event
-
-
-
-
-
-
-
-
Events
-
-
- Export to CSV
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Promo codes
-
-
- No promo codes for this event.
-
-
-
-
-
-
-
-
-
- Discount: %
-
-
- Status:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Tickets
-
-
- Export to CSV
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} Events extension
-
-
-
-
- {% include "events/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
Ticket closing date
-
-
-
-
-
-
-
-
-
-
-
Conditional Events
-
- Make this event conditional if
- minimum tickets are sold. User will be asked to
- provide a Lightning Address or LNURL pay for refunds.
-
-
-
-
-
-
-
-
-
- Promo Codes
-
- Allow users to enter a promo code for discounts.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Promo Code
-
-
-
-
- Update Event
- Create Event
- Cancel
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
-
-{% endblock %}
diff --git a/templates/events/register.html b/templates/events/register.html
deleted file mode 100644
index 92589a3..0000000
--- a/templates/events/register.html
+++ /dev/null
@@ -1,84 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
-
- {{ event_name }} Registration
-
-
-
-
- Scan ticket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
-{% endblock %} {% block scripts %}
-
-
-{% endblock %}
diff --git a/templates/events/ticket.html b/templates/events/ticket.html
deleted file mode 100644
index bcf7e82..0000000
--- a/templates/events/ticket.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{% extends "public.html" %} {% block page %}
-
-
-
-
-
- {{ ticket_name }} Ticket
-
-
- Bookmark, print or screenshot this page,
- and present it for registration!
-
-
-
-
-
- Print
-
-
-
-
-
-{% endblock %} {% block scripts %}
-
-{% endblock %}
diff --git a/views.py b/views.py
index 0680dcc..39ac273 100644
--- a/views.py
+++ b/views.py
@@ -1,139 +1,24 @@
-from datetime import date, datetime
-from http import HTTPStatus
-
-from fastapi import APIRouter, Depends, Request
-from lnbits.core.models import User
-from lnbits.decorators import check_user_exists
-from lnbits.helpers import template_renderer
-from starlette.exceptions import HTTPException
-from starlette.responses import HTMLResponse
-
-from .crud import get_event, get_ticket, purge_unpaid_tickets, update_event
-from .services import refund_tickets
+from fastapi import APIRouter, Depends
+from lnbits.core.views.generic import index, index_public # type: ignore
+from lnbits.decorators import check_account_id_exists
events_generic_router = APIRouter()
+events_generic_router.add_api_route(
+ "/",
+ methods=["GET"],
+ endpoint=index,
+ dependencies=[Depends(check_account_id_exists)],
+)
-def events_renderer():
- return template_renderer(["events/templates"])
-
-
-@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.json()}
- )
-
-
-@events_generic_router.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."
- )
-
- await purge_unpaid_tickets(event_id)
-
- is_window_open = (
- date.today() < datetime.strptime(event.closing_date, "%Y-%m-%d").date()
- )
- is_min_tickets_met = (
- event.sold >= event.extra.min_tickets if event.extra.conditional else True
- )
-
- 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 :(",
- },
- )
- if event.extra.conditional and not is_min_tickets_met and not is_window_open:
- event.canceled = True
- await update_event(event)
- await refund_tickets(event_id)
-
- return events_renderer().TemplateResponse(
- "events/error.html",
- {
- "request": request,
- "event_name": event.name,
- "event_error": "Sorry, event was cancelled.",
- },
- )
- if not is_window_open:
- return events_renderer().TemplateResponse(
- "events/error.html",
- {
- "request": request,
- "event_name": event.name,
- "event_error": "Sorry, ticket closing date has passed :(",
- },
- )
-
- if len(event.extra.promo_codes) > 0:
- has_promo_codes = True
- else:
- has_promo_codes = False
-
- event.extra.promo_codes = []
- 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,
- "event_banner": event.banner,
- "event_extra": event.extra.json(),
- "has_promo_codes": has_promo_codes,
- },
- )
-
-
-@events_generic_router.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_generic_router.add_api_route(
+ "/{event_id}", methods=["GET"], endpoint=index_public
+)
-@events_generic_router.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."
- )
+events_generic_router.add_api_route(
+ "/ticket/{ticket_id}", methods=["GET"], endpoint=index_public
+)
- return events_renderer().TemplateResponse(
- "events/register.html",
- {
- "request": request,
- "event_id": event_id,
- "event_name": event.name,
- "wallet_id": event.wallet,
- },
- )
+events_generic_router.add_api_route(
+ "/register/{event_id}", methods=["GET"], endpoint=index_public
+)
diff --git a/views_api.py b/views_api.py
index 58c9bca..03f03fa 100644
--- a/views_api.py
+++ b/views_api.py
@@ -26,6 +26,7 @@
get_events,
get_ticket,
get_tickets,
+ purge_unpaid_tickets,
update_event,
update_ticket,
)
@@ -49,6 +50,46 @@ async def api_events(
return [event.dict() for event in await get_events(wallet_ids)]
+@events_api_router.get("/api/v1/events/{event_id}")
+async def api_get_event(event_id: str) -> dict:
+ event = await get_event(event_id)
+ if not event:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
+ )
+ await purge_unpaid_tickets(event_id)
+
+ is_window_open = datetime.now(timezone.utc) < datetime.strptime(
+ event.closing_date, "%Y-%m-%d"
+ ).replace(tzinfo=timezone.utc)
+ is_min_tickets_met = (
+ event.sold >= event.extra.min_tickets if event.extra.conditional else True
+ )
+ if event.amount_tickets < 1:
+ raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.")
+ if event.extra.conditional and not is_min_tickets_met and not is_window_open:
+ event.canceled = True
+ await update_event(event)
+ await refund_tickets(event_id)
+
+ raise HTTPException(status_code=HTTPStatus.GONE, detail="Event canceled.")
+
+ if not is_window_open:
+ raise HTTPException(
+ status_code=HTTPStatus.GONE, detail="Ticket closing date has passed."
+ )
+
+ return {
+ "event_id": event_id,
+ "event_name": event.name,
+ "event_info": event.info,
+ "event_price": event.price_per_ticket,
+ "event_banner": event.banner,
+ "event_extra": event.extra.json(),
+ "has_promo_codes": len(event.extra.promo_codes) > 0,
+ }
+
+
@events_api_router.post("/api/v1/events")
@events_api_router.put("/api/v1/events/{event_id}")
async def api_event_create(
@@ -131,6 +172,25 @@ async def api_tickets(
return await get_tickets(wallet_ids)
+@events_api_router.get("/api/v1/tickets/{ticket_id}")
+async def api_get_ticket(ticket_id: str) -> dict:
+ 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 {
+ "ticket_id": ticket.id,
+ "ticket_name": event.name,
+ "ticket_info": event.info,
+ }
+
+
@events_api_router.post("/api/v1/tickets/{event_id}")
async def api_ticket_create(event_id: str, data: CreateTicket):
name = data.name