diff --git a/.gitignore b/.gitignore index d8d1bb8..17840cc 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,5 @@ res/propic/propic_* # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ config.py +furizon_webinit_riverside2023.tar.gz +diomerdas diff --git a/README.md b/README.md index e69de29..88f0cc1 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,26 @@ +# Furizon Webint +Furizon Webint is a powerful control panel designed to complement Pretix, providing management of various aspects related to the attendance of participants at furry conventions. Originally developed for Furizon Beyond (2023), this application is currently undergoing a rehaul to become more versatile and adaptable for use in any convention. + +## How does it work? +The integration with Pretix is achieved by leveraging a simple nginx rule. When individuals place orders through Pretix, they usually receive a "magic" link that allows them to manage their order. Using a nginx rule, we redirect these requests to this backend. This process is seamless because the essential information needed for managing Pretix orders can still be accessed via a shorter URL without compromising any functionality. + +## Why not a pretix plugin? +Developing plugins for Pretix was far too tedious, and Pretix didn't have the flexibility needed for this panel. + +## What can it do? +- User badges management (allow attendees to upload pictures within the deadlines) +- Manage hotel rooms (attendees can create, join, delete rooms) +- Show a nosecount public page +- Data export +- Car pooling (let attendees post announcements and organize trips) +- Karaoke Queue management (apply to sing for the karaoke contest and manage the queue) +- Manage the events and present them via API for usage with he app +- Export an API to be used for the mobile app (no plans to open source that, sorry ☹️) +- Check-in management + +## How to run it +1. Create a Python virtual environment (venv). +2. Install the required dependencies from the `requirements.txt` file. +3. Edit the `config.py` file with your specific data. You can use `config.example.py` as a template to guide you. +4. Set up an nginx rule to redirect requests for `/manage/` and `/[a-z0-9]+/[a-z0-9]+/order/[A-Z0-9]+/[a-z0-9]+/open/[a-z0-9]+/` to the Furizon Webint backend. +5. Run `app.py`. By default, the application will listen on `0.0.0.0:8188`. diff --git a/app.py b/app.py index 88e663a..fa2a1b1 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,7 @@ from os.path import join from ext import * from config import * from aztec_code_generator import AztecCode +from propic import resetDefaultPropic from io import BytesIO from asyncio import Queue import sqlite3 @@ -32,7 +33,7 @@ from carpooling import bp as carpooling_bp from checkin import bp as checkin_bp app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp]) - + @app.exception(exceptions.SanicException) async def clear_session(request, exception): tpl = app.ctx.tpl.get_template('error.html') @@ -50,7 +51,7 @@ async def main_start(*_): app.ctx.om = OrderManager() if FILL_CACHE: log.info("Filling cache!") - await app.ctx.om.fill_cache() + await app.ctx.om.updateCache() log.info("Cache fill done!") app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db') @@ -61,6 +62,7 @@ async def main_start(*_): app.ctx.tpl.globals.update(time=time) app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE) app.ctx.tpl.globals.update(ITEM_IDS=ITEM_IDS) + app.ctx.tpl.globals.update(ROOM_TYPE_NAMES=ROOM_TYPE_NAMES) app.ctx.tpl.globals.update(int=int) app.ctx.tpl.globals.update(len=len) @@ -80,7 +82,7 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None): if not order: async with httpx.AsyncClient() as client: - res = await client.get(join(base_url, f"orders/{code}/"), headers=headers) + res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers) print(res.json()) if res.status_code != 200: raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.") @@ -102,6 +104,11 @@ async def welcome(request, order: Order, quota: Quotas): if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") + + if order.ans("propic_file") is None: + await resetDefaultPropic(request, order, False) + if order.ans("propic_fursuiter_file") is None: + await resetDefaultPropic(request, order, True) pending_roommates = [] if order.pending_roommates: @@ -139,7 +146,7 @@ async def download_ticket(request, order: Order): raise exceptions.Forbidden("You are not allowed to download this ticket.") async with httpx.AsyncClient() as client: - res = await client.get(join(base_url, f"orders/{order.code}/download/pdf/"), headers=headers) + res = await client.get(join(base_url_event, f"orders/{order.code}/download/pdf/"), headers=headers) if res.status_code == 409: raise exceptions.SanicException("Your ticket is still being generated. Please try again later!", status_code=res.status_code) @@ -150,7 +157,7 @@ async def download_ticket(request, order: Order): @app.route("/manage/logout") async def logour(request): - raise exceptions.Forbidden("You have been logged out.", status_code=403) + raise exceptions.Forbidden("You have been logged out.") if __name__ == "__main__": app.run(host="0.0.0.0", port=8188, dev=DEV_MODE) diff --git a/checkin.py b/checkin.py index 03904d1..a51e14f 100644 --- a/checkin.py +++ b/checkin.py @@ -2,7 +2,7 @@ from sanic.response import html, redirect, text from sanic import Blueprint, exceptions, response from random import choice from ext import * -from config import headers, base_url +from config import headers, base_url_event from PIL import Image from os.path import isfile from os import unlink @@ -64,7 +64,7 @@ async def do_checkin(request): if not order.checked_in: async with httpx.AsyncClient() as client: - res = await client.post(base_url.replace('events/beyond/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]}, headers=headers) + res = await client.post(base_url_event.replace(f'events/{EVENT_NAME}/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]}, headers=headers) tpl = request.app.ctx.tpl.get_template('checkin_3.html') return html(tpl.render(order=order, room_owner=room_owner, roommates=roommates)) diff --git a/config.example.py b/config.example.py index de039d9..98eb2fc 100644 --- a/config.example.py +++ b/config.example.py @@ -1,32 +1,53 @@ +API_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' ORGANIZER = 'furizon' -EVENT_NAME = 'river-side-2023' -API_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxx' -HOSTNAME = 'your-pretix-hostname' +EVENT_NAME = 'overlord' +HOSTNAME = 'reg.furizon.net' headers = {'Host': HOSTNAME, 'Authorization': f'Token {API_TOKEN}'} -base_url = f"https://{HOSTNAME}/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/" +base_url = "http://urlllllllllllllllllllll/api/v1/" +base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/" -PROPIC_DEADLINE = 1683575684 +PROPIC_DEADLINE = 9999999999 FILL_CACHE = True +CACHE_EXPIRE_TIME = 60 * 60 * 4 DEV_MODE = True ITEM_IDS = { - 'ticket': [90,], - 'membership_card': [91,], - 'sponsorship': [], # first one = normal, second = super - 'early_arrival': [], - 'late_departure': [], - 'room': 98 + 'ticket': [126, 127, 155], + 'membership_card': [128,], + 'sponsorship': [55, 56], # first one = normal, second = super + 'early_arrival': [133], + 'late_departure': [134], + 'room': 135, + 'bed_in_room': 153, + 'daily': 162, + 'daily_addons': [163, 164, 165, 166] #This should be in date order. If there are holes in the daily-span, insert an unexisting id } # Create a bunch of "room" items which will get added to the order once somebody gets a room. +# Map variationId -> numberOfPeopleInRoom ROOM_MAP = { - 1: 16, - 2: 17, - 3: 18, - 4: 19, - 5: 20 + # SACRO CUORE + 83: 1, + 67: 2, + 68: 3, + 69: 4, + 70: 5, + + # OVERFLOW 1 + 75: 2 +} + +ROOM_TYPE_NAMES = { + 83: "Park Hotel Sacro Cuore (main hotel) - Single", + 67: "Park Hotel Sacro Cuore (main hotel) - Double", + 68: "Park Hotel Sacro Cuore (main hotel) - Triple", + 69: "Park Hotel Sacro Cuore (main hotel) - Quadruple", + 70: "Park Hotel Sacro Cuore (main hotel) - Quintuple", + + # OVERFLOW 1 + 75: "Hotel San Valier (overflow hotel) - Double" } # This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id. @@ -36,6 +57,6 @@ TG_CHAT_ID = -1234567 # These order codes have additional functions. ADMINS = ['XXXXX', 'YYYYY'] -SMTP_HOST = 'your-smtp-host.com' -SMTP_USER = 'username' -SMTP_PASSWORD = 'password' +SMTP_HOST = 'host' +SMTP_USER = 'user' +SMTP_PASSWORD = 'pw' diff --git a/data/boop.db b/data/boop.db index 5bf5436..4ef87e9 100644 Binary files a/data/boop.db and b/data/boop.db differ diff --git a/data/event.db b/data/event.db index 2728c20..9cdb2d5 100644 Binary files a/data/event.db and b/data/event.db differ diff --git a/ext.py b/ext.py index e5abb83..af339e8 100644 --- a/ext.py +++ b/ext.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from sanic import Request, exceptions import httpx import re +from utils import * from config import * from os.path import join import json @@ -32,6 +33,11 @@ class Order: self.country = 'xx' self.address = None self.checked_in = False + self.room_type = None + self.daily = False + self.dailyDays = [] + self.room_person_no = 0 + self.answers = [] idata = data['invoice_address'] if idata: @@ -39,13 +45,22 @@ class Order: self.country = idata['country'] for p in self.data['positions']: - if p['item'] in (ITEM_IDS['ticket'] + ITEM_IDS['daily']): + if p['item'] in (ITEM_IDS['ticket'] + [ITEM_IDS['daily']]): self.position_id = p['id'] self.position_positionid = p['positionid'] self.position_positiontypeid = p['item'] self.answers = p['answers'] + for i, ans in enumerate(self.answers): + if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']): + self.answers[i]['answer'] = "file:keep" self.barcode = p['secret'] self.checked_in = bool(p['checkins']) + if p['item'] == ITEM_IDS['daily']: + self.daily = True + + if p['item'] in ITEM_IDS['daily_addons']: + self.daily = True + self.dailyDays.append(ITEM_IDS['daily_addons'].index(p['item'])) if p['item'] in ITEM_IDS['membership_card']: self.has_card = True @@ -62,6 +77,10 @@ class Order: if p['item'] == ITEM_IDS['late_departure']: self.has_late = True + + if p['item'] == ITEM_IDS['bed_in_room']: + self.bed_in_room = p['variation'] + self.room_person_no = ROOM_MAP[self.bed_in_room] if self.bed_in_room in ROOM_MAP else None self.total = float(data['total']) self.fees = 0 @@ -117,6 +136,18 @@ class Order: return a['answer'] return None + async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes): + if(mimeType != None and data != None): + async with httpx.AsyncClient() as client: + localHeaders = dict(headers) + localHeaders['Content-Type'] = mimeType + localHeaders['Content-Disposition'] = f'attachment; filename="{fileName}"' + res = await client.post(join(base_url, 'upload'), headers=localHeaders, content=data) + res = res.json() + await self.edit_answer(name, res['id']) + else: + await self.edit_answer(name, None) + async def edit_answer(self, name, new_answer): found = False self.pending_update = True @@ -135,7 +166,7 @@ class Order: if (not found) and (new_answer is not None): async with httpx.AsyncClient() as client: - res = await client.get(join(base_url, 'questions/'), headers=headers) + res = await client.get(join(base_url_event, 'questions/'), headers=headers) res = res.json() for r in res['results']: @@ -158,7 +189,7 @@ class Order: del self.answers[i]['options'] del self.answers[i]['option_identifiers'] - res = await client.patch(join(base_url, f'orderpositions/{self.position_id}/'), headers=headers, json={'answers': self.answers}) + res = await client.patch(join(base_url_event, f'orderpositions/{self.position_id}/'), headers=headers, json={'answers': self.answers}) if res.status_code != 200: for ans, err in zip(self.answers, res.json()['answers']): @@ -167,6 +198,10 @@ class Order: raise exceptions.ServerError('There has been an error while updating this answers.') + for i, ans in enumerate(self.answers): + if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']): + self.answers[i]['answer'] = "file:keep" + self.pending_update = False self.time = -1 @@ -183,7 +218,7 @@ class Quotas: async def get_quotas(request: Request=None): async with httpx.AsyncClient() as client: - res = await client.get(join(base_url, 'quotas/?order=id&with_availability=true'), headers=headers) + res = await client.get(join(base_url_event, 'quotas/?order=id&with_availability=true'), headers=headers) res = res.json() return Quotas(res) @@ -194,9 +229,20 @@ async def get_order(request: Request=None): class OrderManager: def __init__(self): + self.lastCacheUpdate = 0 + self.empty() + + def empty(self): self.cache = {} self.order_list = [] + async def updateCache(self): + t = time() + if(t - self.lastCacheUpdate > CACHE_EXPIRE_TIME): + print("Re-filling cache!") + await self.fill_cache() + self.lastCacheUpdate = t + def add_cache(self, order): self.cache[order.code] = order if not order.code in self.order_list: @@ -208,12 +254,14 @@ class OrderManager: self.order_list.remove(code) async def fill_cache(self): + await loadQuestions() + self.empty() p = 0 async with httpx.AsyncClient() as client: while 1: p += 1 - res = await client.get(join(base_url, f"orders/?page={p}"), headers=headers) + res = await client.get(join(base_url_event, f"orders/?page={p}"), headers=headers) if res.status_code == 404: break @@ -233,6 +281,7 @@ class OrderManager: if order.nfc_id == nfc_id: return order + await self.updateCache() # If a cached order is needed, just get it if available if code and cached and code in self.cache and time()-self.cache[code].time < 3600: return self.cache[code] @@ -246,7 +295,7 @@ class OrderManager: print('Fetching', code, 'with secret', secret) async with httpx.AsyncClient() as client: - res = await client.get(join(base_url, f"orders/{code}/"), headers=headers) + res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers) if res.status_code != 200: if request: raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.") diff --git a/propic.py b/propic.py index 25852f1..7eef9b4 100644 --- a/propic.py +++ b/propic.py @@ -9,6 +9,15 @@ from time import time bp = Blueprint("propic", url_prefix="/manage/propic") +async def resetDefaultPropic(request, order: Order, isFursuiter, sendAnswer=True): + s = "_fursuiter" if isFursuiter else "" + print("Resetting default propic") + with open("res/propic/default.png", "rb") as f: + data = f.read() + await order.edit_answer_fileUpload(f'propic{s}_file', f'propic{s}_file_{order.code}_default.png', 'image/png', data) + if(sendAnswer): + await order.send_answers() + @bp.post("/upload") async def upload_propic(request, order: Order): if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") @@ -21,8 +30,10 @@ async def upload_propic(request, order: Order): if request.form.get('submit') == 'Delete main image': await order.edit_answer('propic', None) + await resetDefaultPropic(request, order, False, sendAnswer=False) elif request.form.get('submit') == 'Delete fursuit image': await order.edit_answer('propic_fursuiter', None) + await resetDefaultPropic(request, order, True, sendAnswer=False) else: for fn, body in request.files.items(): if fn not in ['propic', 'propic_fursuiter']: @@ -49,8 +60,17 @@ async def upload_propic(request, order: Order): img = img.convert('RGB') img.thumbnail((512,512)) - img.save(f"res/propic/{fn}_{order.code}_{h}.jpg") - except: + imgBytes = BytesIO() + img.save(imgBytes, format='jpeg') + imgBytes = imgBytes.getvalue() + + with open(f"res/propic/{fn}_{order.code}_{h}.jpg", "wb") as f: + f.write(imgBytes) + + await order.edit_answer_fileUpload(f'{fn}_file', f'{fn}_file_{order.code}_{h}.jpg', 'image/jpeg', imgBytes) + except Exception: + import traceback + print(traceback.format_exc()) raise exceptions.BadRequest("The image you uploaded is not valid.") else: await order.edit_answer(fn, f"{fn}_{order.code}_{h}.jpg") diff --git a/reg.furizon.net/BannertVertical.png b/reg.furizon.net/BannertVertical.png new file mode 100644 index 0000000..ba187e8 Binary files /dev/null and b/reg.furizon.net/BannertVertical.png differ diff --git a/reg.furizon.net/bg.jpg b/reg.furizon.net/bg.jpg new file mode 100644 index 0000000..4b968c9 Binary files /dev/null and b/reg.furizon.net/bg.jpg differ diff --git a/reg.furizon.net/bgVert.jpg b/reg.furizon.net/bgVert.jpg new file mode 100644 index 0000000..edeb9db Binary files /dev/null and b/reg.furizon.net/bgVert.jpg differ diff --git a/reg.furizon.net/fz23.html b/reg.furizon.net/fz23.html new file mode 100644 index 0000000..9f9865d --- /dev/null +++ b/reg.furizon.net/fz23.html @@ -0,0 +1,21 @@ + + + Thanks! + + + +
+

Thanks for participating in Furizon ENABLE_JAVASCRIPT!

+

We just came back home and we need some time to regain energy and get back to normal! The registration system is currently offline, and we are working on making it better!

+

When will it be back online?

+

The registration system was hosted in a server in the convention network itself. After the end of the convention, the network was disassembled. We will soon setup the server again!

+

Can i contact you?

+

Yes. Just write to info@furizon.net

+
+ + + diff --git a/reg.furizon.net/index.html b/reg.furizon.net/index.html new file mode 100644 index 0000000..c07a871 --- /dev/null +++ b/reg.furizon.net/index.html @@ -0,0 +1,162 @@ + + + Countdown to Furizon + + + + + + + + + + + +
+
+
+ +

Overlord Registration

+ Enjoy furizon~ + +
+
+ + + + diff --git a/reg.furizon.net/index_beyond.html b/reg.furizon.net/index_beyond.html new file mode 100644 index 0000000..e244324 --- /dev/null +++ b/reg.furizon.net/index_beyond.html @@ -0,0 +1,132 @@ + + + Countdown to Furizon + + + + + + + + + + + +
+
+ +

Thank you to everybody who registered for Furizon 2023!

+

Too late to register? You can still sign up to the waiting list! We will contact you as soon as a spot is available for you~

+ + Join waiting list + Redeem code +
+
+ + + + diff --git a/reg.furizon.net/reg.furizon.net.zip b/reg.furizon.net/reg.furizon.net.zip new file mode 100644 index 0000000..b7b512e Binary files /dev/null and b/reg.furizon.net/reg.furizon.net.zip differ diff --git a/reg.furizon.net/riverside.jpg b/reg.furizon.net/riverside.jpg new file mode 100644 index 0000000..e9cb261 Binary files /dev/null and b/reg.furizon.net/riverside.jpg differ diff --git a/res/boopbox.js b/res/boopbox.js index 055110f..dee60d8 100644 --- a/res/boopbox.js +++ b/res/boopbox.js @@ -94,13 +94,13 @@ function clock() { currentTime = new Date(); let ts = currentTime.toString(); ts = ts.replace("GMT+0200 (Central European Summer Time)", ""); - ts = ts.replace("2023", "
"); + ts = ts.replace("2024", "
"); document.getElementById("clock").innerHTML = ts; }, 1000); } function fastForward() { - currentTime = new Date("2023-05-29T18:00Z"); + currentTime = new Date("2024-06-03T18:00Z"); setInterval(() => { updateDivs(cachedData); currentTime.setMinutes(currentTime.getMinutes()+1); diff --git a/res/propic/default.png b/res/propic/default.png index 5be81dc..5e6415a 100644 Binary files a/res/propic/default.png and b/res/propic/default.png differ diff --git a/room.py b/room.py index 32c9a63..fd5760f 100644 --- a/room.py +++ b/room.py @@ -18,6 +18,9 @@ async def room_create_post(request, order: Order): if order.room_id: error = "You are already in another room. You need to delete (if it's yours) or leave it before creating another." + + if order.daily: + raise exceptions.BadRequest("You cannot create a room if you have a daily ticket!") if not error: await order.edit_answer('room_name', name) @@ -35,6 +38,9 @@ async def room_create(request, order: Order): if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") + if order.daily: + raise exceptions.BadRequest("You cannot create a room if you have a daily ticket!") + tpl = request.app.ctx.tpl.get_template('create_room.html') return html(tpl.render(order=order)) @@ -70,6 +76,9 @@ async def join_room(request, order: Order): if order.room_id: raise exceptions.BadRequest("You are in another room already. Why would you join another?") + + if order.daily: + raise exceptions.BadRequest("You cannot join a room if you have a daily ticket!") code = request.form.get('code').strip() room_secret = request.form.get('room_secret').strip() @@ -87,6 +96,9 @@ async def join_room(request, order: Order): if room_owner.room_confirmed: raise exceptions.BadRequest("The room you're trying to join has been confirmed already") + + if room_owner.bed_in_room != order.bed_in_room: + raise exceptions.BadRequest("This room's ticket is of a different type than yours!") #if room_owner.pending_roommates and (order.code in room_owner.pending_roommates): #raise exceptions.BadRequest("What? You should never reach this check, but whatever...") @@ -252,9 +264,11 @@ async def confirm_room(request, order: Order, quotas: Quotas): if order.room_id != order.code: raise exceptions.BadRequest("You are not allowed to confirm rooms of others.") - if quotas.get_left(len(order.room_members)) == 0: - raise exceptions.BadRequest("There are no more rooms of this size to reserve.") + # This is not needed anymore you buy tickets already + #if quotas.get_left(len(order.room_members)) == 0: + # raise exceptions.BadRequest("There are no more rooms of this size to reserve.") + bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room room_members = [] for m in order.room_members: if m == order.code: @@ -267,8 +281,18 @@ async def confirm_room(request, order: Order, quotas: Quotas): if res.status != 'paid': raise exceptions.BadRequest("Somebody hasn't paid.") + + if res.bed_in_room != bed_in_room: + raise exceptions.BadRequest("Somebody has a ticket for a different type of room!") + + if res.daily: + raise exceptions.BadRequest("Somebody in your room has a daily ticket!") room_members.append(res) + + + if len(room_members) != order.room_person_no and order.room_person_no != None: + raise exceptions.BadRequest("The number of people in your room mismatches your type of ticket!") for rm in room_members: await rm.edit_answer('room_id', order.code) @@ -276,28 +300,19 @@ async def confirm_room(request, order: Order, quotas: Quotas): await rm.edit_answer('pending_roommates', None) await rm.edit_answer('pending_room', None) - thing = { - 'order': order.code, - 'addon_to': order.position_positionid, - 'item': ITEM_IDS['room'], - 'variation': ROOM_MAP[len(room_members)] - } - - async with httpx.AsyncClient() as client: - res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing) - - if res.status_code != 201: - raise exceptions.BadRequest("Something has gone wrong! Please contact support immediately") - - '''for rm in room_members: - if rm.code == order.code: continue - thing = { - 'order': rm.code, - 'addon_to': rm.position_positionid, - 'item': ITEM_IDS['room'], - 'variation': ROOM_MAP[len(room_members)] - } - res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing) ''' + # This should now be useless because in the ticket there already is the ticket/room type + # thing = { + # 'order': order.code, + # 'addon_to': order.position_positionid, + # 'item': ITEM_IDS['room'], + # 'variation': ROOM_MAP[len(room_members)] + # } + # + # async with httpx.AsyncClient() as client: + # res = await client.post(join(base_url_event, "orderpositions/"), headers=headers, json=thing) + # + # if res.status_code != 201: + # raise exceptions.BadRequest("Something has gone wrong! Please contact support immediately") for rm in room_members: await rm.send_answers() diff --git a/stats.py b/stats.py index 2025490..a97ef9b 100644 --- a/stats.py +++ b/stats.py @@ -6,7 +6,16 @@ bp = Blueprint("stats", url_prefix="/manage") @bp.route("/nosecount") async def nose_count(request, order: Order): + await request.app.ctx.om.updateCache() orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e']} tpl = request.app.ctx.tpl.get_template('nosecount.html') return html(tpl.render(orders=orders, order=order)) + +@bp.route("/fursuitcount") +async def fursuit_count(request, order: Order): + await request.app.ctx.om.updateCache() + orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e']} + + tpl = request.app.ctx.tpl.get_template('fursuitcount.html') + return html(tpl.render(orders=orders, order=order)) \ No newline at end of file diff --git a/tpl/base.html b/tpl/base.html index 66da42b..bc5dc0f 100644 --- a/tpl/base.html +++ b/tpl/base.html @@ -86,9 +86,11 @@ Your Booking {% endif %} Nose Count + Fursuit Count {% if order %} Carpooling Logout + Logged in as {{order.ans('fursona_name')}} {% endif %}
diff --git a/tpl/blocks/badge.html b/tpl/blocks/badge.html index e7f8f7b..ab1bbd1 100644 --- a/tpl/blocks/badge.html +++ b/tpl/blocks/badge.html @@ -5,7 +5,7 @@ {% if order.propic_locked %}

⚠️ You have been limited from further editing your profile pic.

{% endif %} - {% if (not order.ans('propic')) or (order.ans('is_fursuiter') != 'No' and not order.ans('propic_fursuiter')) %} + {% if not order.ans('propic') or (order.is_fursuiter and not order.ans('propic_fursuiter')) %}

⚠️ One or more badge pictures are missing! This will cause you badge to be empty, so make sure to upload something before the deadline!

{% endif %}
@@ -20,7 +20,7 @@ {% endif %}

Normal Badge

- {% if order.ans('is_fursuiter') != 'No' %} + {% if order.is_fursuiter %}
{% if not order.ans('propic_fursuiter') %} @@ -50,7 +50,7 @@ {% if order.ans('propic_fursuiter') %} PROPIC_DEADLINE else ''}} /> {% endif %} - {% if (not order.ans('propic')) or (order.ans('is_fursuiter') != 'No' and not order.ans('propic_fursuiter')) %} + {% if not order.ans('propic') or (order.is_fursuiter and not order.ans('propic_fursuiter')) %} {% endif %}
diff --git a/tpl/blocks/payment.html b/tpl/blocks/payment.html index 2bf3b89..de75305 100644 --- a/tpl/blocks/payment.html +++ b/tpl/blocks/payment.html @@ -18,7 +18,7 @@ {% if order.status == 'paid' and order.room_confirmed %} -

Download ticket

+

Download ticket

{% endif %} {% if order.status != 'paid' %} diff --git a/tpl/blocks/room.html b/tpl/blocks/room.html index 0b8f05f..37efed7 100644 --- a/tpl/blocks/room.html +++ b/tpl/blocks/room.html @@ -1,12 +1,14 @@
Accomodation & Roommates {% if not order.room_confirmed %}⚠️{% endif %} -

Your room {% if room_members %}- {{room_members[0].ans('room_name')}}{% endif %}

+

Your room {% if room_members %}- {{room_members[0].ans('room_name')}}{% endif %}

+

Room's type: {{ROOM_TYPE_NAMES[order.bed_in_room]}}.

+

Note! Only people with the same room type can be roommates. If you need help, you can check the support guide clicking here.

{# Show alert if room owner has wrong people inside #} - {% if room_members and quota.get_left(len(room_members)) == 0 and (not order.room_confirmed) %} -

⚠️ Your room contains {{len(room_members)}} people inside, but sadly there are no more {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} rooms. You need to add or remove people until you reach the size of an available room if you want to confirm it.

- {% endif %} + {# {% if room_members and quota.get_left(len(room_members)) == 0 and (not order.room_confirmed) %} #} + {#

⚠️ Your room contains {{len(room_members)}} people inside, but sadly there are no more {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} rooms. You need to add or remove people until you reach the size of an available room if you want to confirm it.

#} + {# {% endif %} #} {# Show alert if room was not confirmed #} {% if order.room_id and not order.room_confirmed %} @@ -15,7 +17,8 @@ {# Show notice if the room is confirmed #} {% if order.room_confirmed %} -

✅ Your {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} room has been confirmed

+ {#

✅ Your {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} room has been confirmed

#} +

✅ Your {{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}} room has been confirmed

{% endif %} {# Show roommates if room is set #} @@ -30,7 +33,7 @@

{{person.ans('fursona_name')}}

{% if person.code == order.room_id %}

ROOM OWNER

{% endif %} -

{{person.ans('staff_title') if person.ans('staff_title') else ''}} {{'Fursuiter' if person.ans('is_fursuiter') != 'No'}}

+

{{person.ans('staff_title') if person.ans('staff_title') else ''}} {{'Fursuiter' if person.is_fursuiter}}

{% if person.status == 'pending' %}

UNPAID

{% endif %} @@ -42,7 +45,8 @@ {% endif %} {% endfor %} - {% if order.room_id == order.code and not order.room_confirmed and len(room_members) < 5 %} + {# {% if order.room_id == order.code and not order.room_confirmed and len(room_members) < 5%} #} + {% if order.room_id == order.code and not order.room_confirmed and len(room_members) < order.room_person_no %}
@@ -79,7 +83,8 @@ {% endif %} {% if not order.room_confirmed %} - 0 %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} room + {# 0 %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} room #} + Confirm {{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}} room {% endif %} {% else %} {% if order.room_id and not order.room_confirmed %} @@ -113,13 +118,13 @@ {% endif %} {# Room availability is always shown #} -

Room availability

- - {% for q in quota.data['results'] if 'Room' in q['name'] %} - - - - - {% endfor %} -
{{q['name']}}{{q['available_number']}} left
+ {#

Room availability

#} + {# #} + {# {% for q in quota.data['results'] if 'Room' in q['name'] %} #} + {# #} + {# #} + {# #} + {# #} + {# {% endfor %} #} + {#
{{q['name']}}{{q['available_number']}} left
#}
diff --git a/tpl/blocks/room_extra.html b/tpl/blocks/room_extra.html index 0739e29..2245a81 100644 --- a/tpl/blocks/room_extra.html +++ b/tpl/blocks/room_extra.html @@ -29,16 +29,18 @@ - - - - - + {# #} + + {# #} + {# #} + {# #} + {# #}
Room type{{[None,'Single','Double','Triple','Quadruple','Quintuple'][len(room_members)]}} Room
Rooms left of this type{{quota.get_left(len(room_members))}}{{[None,'Single','Double','Triple','Quadruple','Quintuple'][len(room_members)]}} Room{{[None,'Single','Double','Triple','Quadruple','Quintuple'][order.room_person_no]}} Room
Rooms left of this type{{quota.get_left(len(room_members))}}
diff --git a/tpl/carpooling.html b/tpl/carpooling.html index cd49590..d4a60f2 100644 --- a/tpl/carpooling.html +++ b/tpl/carpooling.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}Furizon 2023 Carpooling{% endblock %} +{% block title %}Furizon 2024 Carpooling{% endblock %} {% block main %}
@@ -42,13 +42,13 @@ Day of departure diff --git a/tpl/fursuitcount.html b/tpl/fursuitcount.html new file mode 100644 index 0000000..e0fba67 --- /dev/null +++ b/tpl/fursuitcount.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block title %}Furizon 2024 Fursuitcount{% endblock %} +{% block main %} +
+
+ + + + +
+ +

Welcome to the fursuit-count page! Here you can see all of the fursuits that you'll find at Furizon!

+ {% for person in orders.values() if person.is_fursuiter%} + {% if loop.first %} +
+ {% endif %} +
+
+ + +
+
{{person.ans('fursona_name')}}
+
+ {% if loop.last %}
{% endif %} + {% endfor %} + +
+{% endblock %} diff --git a/tpl/karaoke_admin.html b/tpl/karaoke_admin.html index 14002f5..b791631 100644 --- a/tpl/karaoke_admin.html +++ b/tpl/karaoke_admin.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}Furizon 2023 Karaoke Admin{% endblock %} +{% block title %}Furizon 2024 Karaoke Admin{% endblock %} {% block main %}

Karaoke Admin

diff --git a/tpl/nosecount.html b/tpl/nosecount.html index f5826b7..f4afb6a 100644 --- a/tpl/nosecount.html +++ b/tpl/nosecount.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}Furizon 2023 Nosecount{% endblock %} +{% block title %}Furizon 2024 Nosecount{% endblock %} {% block main %}
@@ -29,13 +29,14 @@ {% endif %} {% endfor %} - {% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1) %} + {% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed) %} {% if loop.first %}

Unconfirmed rooms

These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates

{% endif %} -

{{o.room_name}}

+

{{o.room_name}}

+ {% if o.room_person_no - len(o.room_members) > 0 %}

- Remaining slots: {{o.room_person_no - len(o.room_members)}}

{% endif %}
{% for m in o.room_members %} {% if m in orders %} @@ -54,7 +55,7 @@ {% if loop.last %}
{% endif %} {% endfor %} - {% for person in orders.values() if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed)%} + {% for person in orders.values() if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed) and not person.daily %} {% if loop.first %}

Roomless furs

@@ -69,7 +70,24 @@
{{person.ans('fursona_name')}}
{% if loop.last %}{% endif %} - {% endfor %} + {% endfor %} + + {% for person in orders.values() if person.daily %} + {% if loop.first %} +
+

Daily furs!

+

These furs will not stay in our hotels, but may be there with us just a few days!

+
+ {% endif %} +
+
+ + +
+
{{person.ans('fursona_name')}}
+
+ {% if loop.last %}
{% endif %} + {% endfor %}
{% endblock %} diff --git a/tpl/privacy.html b/tpl/privacy.html index 2ebfa0a..42be31b 100644 --- a/tpl/privacy.html +++ b/tpl/privacy.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}Furizon 2023 Nosecount{% endblock %} +{% block title %}Furizon 2024 Nosecount{% endblock %} {% block main %}
@@ -8,7 +8,7 @@
-

⚠️ This privacy policy is a courtesy explanation of the one you signed up when registering to the con

+ {#

⚠️ This privacy policy is a courtesy explanation of the one you signed up when registering to the con

#}

Privacy policy of this private area

We collect only the data that is needed by law and to make sure that your convention experience is up to your expectations. Keep reading to know why and how we collect this data.

diff --git a/tpl/welcome.html b/tpl/welcome.html index 1019f16..b7bdbed 100644 --- a/tpl/welcome.html +++ b/tpl/welcome.html @@ -12,7 +12,7 @@

From here, you can easily manage all aspects of your booking, including composing your hotel room, designing your badge, and updating your payment information. Simply use the buttons below to navigate between the different sections.

Buttons marked with ⚠️ require your attention

-

If you have any questions or issues while using this page, please don't hesitate to contact us for assistance. We look forward to seeing you at Furizon Riverside!

+

If you have any questions or issues while using this page, please don't hesitate to contact us for assistance. We look forward to seeing you at Furizon Overlord!


Useful information

@@ -24,13 +24,14 @@ - + {# This should be early/late excluded! #} + {% if order.has_early or order.has_late %} @@ -61,7 +62,7 @@

Manage your booking

{% include 'blocks/payment.html' %} - {% if order.position_positiontypeid not in ITEM_IDS['daily'] %} + {% if not order.daily %} {% include 'blocks/room.html' %} {% endif %} {% include 'blocks/badge.html' %} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..7c0350e --- /dev/null +++ b/utils.py @@ -0,0 +1,35 @@ +from os.path import join +from config import * +import httpx + +QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.html + "number": "N", + "one_line_string": "S", + "multi_line_string": "T", + "boolean": "B", + "choice_from_list": "C", + "multiple_choice_from_list": "M", + "file_upload": "F", + "date": "D", + "time": "H", + "date_time": "W", + "country_code": "CC", + "telephone_number": "TEL" +} +TYPE_OF_QUESTIONS = {} # maps questionId -> type + + +async def loadQuestions(): + global TYPE_OF_QUESTIONS + TYPE_OF_QUESTIONS.clear() + async with httpx.AsyncClient() as client: + p = 0 + while 1: + p += 1 + res = await client.get(join(base_url_event, f"questions/?page={p}"), headers=headers) + + if res.status_code == 404: break + + data = res.json() + for q in data['results']: + TYPE_OF_QUESTIONS[q['id']] = q['type'] \ No newline at end of file
When{{' (convention)' if order.has_early or order.has_late else ''}}?13 October → 15 October 20234 June → 8 June 2024
When (check-in)? - {{('12' if order.has_early else '13')|safe}} October → {{('16' if order.has_late else '15')|safe}} October 2023 + {{('3' if order.has_early else '4')|safe}} October → {{('9' if order.has_late else '8')|safe}} June 2024 {% if order.has_early %} EARLY {% endif %} @@ -53,7 +54,7 @@ {% if order.status == 'paid' and order.room_confirmed %}
- Download ticket PDF + Download ticket PDF {% endif %}