From 53232de597c5a96bb8488f1b8fba0cf6197963e7 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 8 Jan 2024 22:02:27 +0100 Subject: [PATCH] Auto Id Indexing + Admin panel + Propic upload checks + Room editing Added auto indexing by metadata, instead of ID matching Added checks to propic image uploading Added an admin page, featuring: - Manual cache clearing Added admin controls in nosecount to unconfirm / rename or delete the room Co-authored-by: Luca Sorace "Stranck" --- admin.py | 72 ++++++++++++++++++++++++ app.py | 22 ++++++-- config.example.py | 109 +++++++++++++++++++++++++++---------- connector.py | 23 ++++++++ export.py | 2 +- ext.py | 40 +++++++++----- karaoke.py | 6 +- propic.py | 43 +++++++++++---- res/icons/delete.svg | 1 + res/icons/door_open.svg | 1 + res/icons/favicon.ico | Bin 0 -> 15406 bytes res/icons/pencil.svg | 1 + res/scripts/roomManager.js | 28 ++++++++++ res/styles/admin.css | 18 ++++++ res/styles/base.css | 2 + res/styles/room.css | 14 +++++ room.py | 34 +++++++++++- tpl/admin.html | 18 ++++++ tpl/base.html | 2 + tpl/blocks/badge.html | 21 +++---- tpl/blocks/room.html | 18 ++++-- tpl/blocks/room_extra.html | 13 +++++ tpl/nosecount.html | 51 ++++++++++++++--- utils.py | 107 +++++++++++++++++++++++++++++++++++- 24 files changed, 559 insertions(+), 87 deletions(-) create mode 100644 admin.py create mode 100644 connector.py create mode 100644 res/icons/delete.svg create mode 100644 res/icons/door_open.svg create mode 100644 res/icons/favicon.ico create mode 100644 res/icons/pencil.svg create mode 100644 res/scripts/roomManager.js create mode 100644 res/styles/admin.css create mode 100644 res/styles/room.css create mode 100644 tpl/admin.html diff --git a/admin.py b/admin.py new file mode 100644 index 0000000..df1d7fc --- /dev/null +++ b/admin.py @@ -0,0 +1,72 @@ +from email.mime.text import MIMEText +from sanic import response, redirect, Blueprint, exceptions +from config import * +from utils import * +from ext import * +import sqlite3 +import smtplib +import random +import string +import httpx +import json + +bp = Blueprint("admin", url_prefix="/manage/admin") + +def credentialsCheck (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 not order.isAdmin() : raise exceptions.Forbidden("Birichino :)") + +@bp.get('/cache/clear') +async def clearCache(request, order:Order): + credentialsCheck(request, order) + await request.app.ctx.om.fill_cache() + return redirect(f'/manage/admin') + +@bp.get('/room/unconfirm/') +async def unconfirmRoom(request, code, order:Order): + credentialsCheck(request, order) + dOrder = await getOrderByCode_safe(request, code) + + if(not dOrder.room_confirmed): + raise exceptions.BadRequest("Room is not confirmed!") + + ppl = getPeopleInRoomByRoomId(request, code) + for p in ppl: + await p.edit_answer('room_confirmed', "False") + await p.send_answers() + + return redirect(f'/manage/nosecount') + +@bp.get('/room/delete/') +async def deleteRoom(request, code, order:Order): + credentialsCheck(request, order) + dOrder = await getOrderByCode_safe(request, code) + + ppl = getPeopleInRoomByRoomId(request, code) + for p in ppl: + await p.edit_answer('room_id', None) + await p.edit_answer('room_confirmed', "False") + await p.edit_answer('room_name', None) + await p.edit_answer('pending_room', None) + await p.edit_answer('pending_roommates', None) + await p.edit_answer('room_members', None) + await p.edit_answer('room_owner', None) + await p.edit_answer('room_secret', None) + await p.send_answers() + + await dOrder.send_answers() + return redirect(f'/manage/nosecount') + +@bp.post('/room/rename/') +async def renameRoom(request, code, order:Order): + credentialsCheck(request, order) + dOrder = await getOrderByCode_safe(request, code) + + name = request.form.get('name') + if len(name) > 64 or len(name) < 4: + raise exceptions.BadRequest("Your room name is invalid. Please try another one.") + + await dOrder.edit_answer("room_name", name) + await dOrder.send_answers() + return redirect(f'/manage/nosecount') \ No newline at end of file diff --git a/app.py b/app.py index ccb1102..2633634 100644 --- a/app.py +++ b/app.py @@ -31,8 +31,9 @@ from stats import bp as stats_bp from api import bp as api_bp from carpooling import bp as carpooling_bp from checkin import bp as checkin_bp +from admin import bp as admin_bp -app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp]) +app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp]) @app.exception(exceptions.SanicException) async def clear_session(request, exception): @@ -40,8 +41,8 @@ async def clear_session(request, exception): r = html(tpl.render(exception=exception)) if exception.status_code == 403: - del r.cookies["foxo_code"] - del r.cookies["foxo_secret"] + r.delete_cookie("foxo_code") + r.delete_cookie("foxo_secret") return r @app.before_server_start @@ -63,8 +64,12 @@ async def main_start(*_): app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True) 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(ITEMS_ID_MAP=ITEMS_ID_MAP) + app.ctx.tpl.globals.update(ITEM_VARIATIONS_MAP=ITEM_VARIATIONS_MAP) app.ctx.tpl.globals.update(ROOM_TYPE_NAMES=ROOM_TYPE_NAMES) + app.ctx.tpl.globals.update(PROPIC_MIN_SIZE=PROPIC_MIN_SIZE) + app.ctx.tpl.globals.update(PROPIC_MAX_SIZE=PROPIC_MAX_SIZE) + app.ctx.tpl.globals.update(PROPIC_MAX_FILE_SIZE=sizeof_fmt(PROPIC_MAX_FILE_SIZE)) app.ctx.tpl.globals.update(int=int) app.ctx.tpl.globals.update(len=len) @@ -156,6 +161,15 @@ async def download_ticket(request, order: Order): raise exceptions.SanicException("You can download your ticket only after the order has been confirmed and paid. Try later!", status_code=400) return raw(res.content, content_type='application/pdf') + +@app.route("/manage/admin") +async def admin(request, order: Order): + await request.app.ctx.om.updateCache() + if not order: + raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") + if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)") + tpl = app.ctx.tpl.get_template('admin.html') + return html(tpl.render(order=order)) @app.route("/manage/logout") async def logour(request): diff --git a/config.example.py b/config.example.py index 98eb2fc..77cece6 100644 --- a/config.example.py +++ b/config.example.py @@ -8,47 +8,98 @@ base_url = "http://urlllllllllllllllllllll/api/v1/" base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/" PROPIC_DEADLINE = 9999999999 +PROPIC_MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB +PROPIC_MAX_SIZE = (2048, 2048) # (Width, Height) +PROPIC_MIN_SIZE = (125, 125) # (Width, Height) + FILL_CACHE = True CACHE_EXPIRE_TIME = 60 * 60 * 4 DEV_MODE = True -ITEM_IDS = { - '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 +# Metadata property for item-id mapping +METADATA_NAME = "item_name" +# Metadata property for internal category mapping (not related to pretix's category) +METADATA_CATEGORY = "category_name" + +# Maps Products metadata name <--> ID +ITEMS_ID_MAP = { + 'early_bird_ticket': 126, + 'regular_ticket': 127, + 'staff_ticket': 155, + 'daily_ticket': 162, + 'sponsorship_item': 129, + 'early_arrival_admission': 133, + 'late_departure_admission': 134, + 'membership_card_item': 128, + 'bed_in_room': 153, + 'room_type': 135, + 'room_guest': 136, + 'daily_1': 163, + 'daily_2': 164, + 'daily_3': 165, + 'daily_4': 166, + 'daily_5': None } +# Maps Products' variants metadata name <--> ID +ITEM_VARIATIONS_MAP = { + 'sponsorship_item': { + 'sponsorship_item_normal': 55, + 'sponsorship_item_super': 56 + }, + 'bed_in_room': { + 'bed_in_room_main_1': 83, + 'bed_in_room_main_2': 67, + 'bed_in_room_main_3': 68, + 'bed_in_room_main_4': 69, + 'bed_in_room_main_5': 70, + 'bed_in_room_overflow1_2': 75, + }, + 'room_type': { + 'single': 57, + 'double': 58, + 'triple': 59, + 'quadruple': 60, + 'quintuple': 61 + }, + 'room_guest': { + 'single': 57, + 'double': 58, + 'triple': 59, + 'quadruple': 60, + 'quintuple': 61 + } +} + +ADMINS_PRETIX_ROLE_NAMES = ["Reserved Area admin", "main staff"] + +# Links Products' variants' ids with the internal category name +CATEGORIES_LIST_MAP = { + 'tickets': [], + 'memberships': [], + 'sponsorships': [], + 'tshirts': [], + 'extra_days': [], + 'rooms': [], + 'dailys': [] +} # Create a bunch of "room" items which will get added to the order once somebody gets a room. -# Map variationId -> numberOfPeopleInRoom -ROOM_MAP = { +# Map item_name -> room capacity +ROOM_CAPACITY_MAP = { # SACRO CUORE - 83: 1, - 67: 2, - 68: 3, - 69: 4, - 70: 5, + 'bed_in_room_main_1': 1, + 'bed_in_room_main_2': 2, + 'bed_in_room_main_3': 3, + 'bed_in_room_main_4': 4, + 'bed_in_room_main_5': 5, - # OVERFLOW 1 - 75: 2 + # OVERFLOW 1 + 'bed_in_room_overflow1_2': 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" -} +# Autofilled +ROOM_TYPE_NAMES = { } # This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id. TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' diff --git a/connector.py b/connector.py new file mode 100644 index 0000000..20929b3 --- /dev/null +++ b/connector.py @@ -0,0 +1,23 @@ +# To connect with pretix's data +# WIP to not commit +# DrewTW +import string +import httpx +import json +from ext import * +from config import * +from sanic import response +from sanic import Blueprint +import logging + +log = logging.getLogger() + +def checkConfig(): + if (not DEV_MODE) and DUMMY_DATA: + log.warn('It is strongly unadvised to use dummy data in production') + +def getOrders(page): + return None + +def getOrder(code): + return None \ No newline at end of file diff --git a/export.py b/export.py index ad22b11..99277ab 100644 --- a/export.py +++ b/export.py @@ -8,7 +8,7 @@ bp = Blueprint("export", url_prefix="/manage/export") @bp.route("/export.csv") async def export_csv(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.code not in ADMINS: raise exceptions.Forbidden("Birichino :)") + if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)") page = 0 orders = {} diff --git a/ext.py b/ext.py index af339e8..345c6ee 100644 --- a/ext.py +++ b/ext.py @@ -45,7 +45,7 @@ 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 CATEGORIES_LIST_MAP['tickets']: self.position_id = p['id'] self.position_positionid = p['positionid'] self.position_positiontypeid = p['item'] @@ -55,32 +55,33 @@ class Order: 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']: + if p['item'] in CATEGORIES_LIST_MAP['dailys']: self.daily = True - self.dailyDays.append(ITEM_IDS['daily_addons'].index(p['item'])) + self.dailyDays.append(CATEGORIES_LIST_MAP['dailys'].index(p['item'])) - if p['item'] in ITEM_IDS['membership_card']: + if p['item'] in CATEGORIES_LIST_MAP['memberships']: self.has_card = True - if p['item'] in ITEM_IDS['sponsorship']: - self.sponsorship = 'normal' if p['variation'] == ITEMS_IDS['sponsorship'][0] else 'super' + if p['item'] == ITEMS_ID_MAP['sponsorship_item']: + sponsorshipType = keyFromValue(ITEM_VARIATIONS_MAP['sponsorship_item'], p['variation']) + self.sponsorship = sponsorshipType[0].replace ('sponsorship_item_', '') if len(sponsorshipType) > 0 else None if p['attendee_name']: self.first_name = p['attendee_name_parts']['given_name'] self.last_name = p['attendee_name_parts']['family_name'] - if p['item'] == ITEM_IDS['early_arrival']: + if p['item'] == ITEMS_ID_MAP['early_arrival_admission']: self.has_early = True - if p['item'] == ITEM_IDS['late_departure']: + if p['item'] == ITEMS_ID_MAP['late_departure_admission']: self.has_late = True - if p['item'] == ITEM_IDS['bed_in_room']: + if p['item'] == ITEMS_ID_MAP['bed_in_room']: + roomTypeLst = keyFromValue(ITEM_VARIATIONS_MAP['bed_in_room'], p['variation']) + roomTypeId = roomTypeLst[0] if len(roomTypeLst) > 0 else None 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.room_person_no = ROOM_CAPACITY_MAP[roomTypeId] if roomTypeId in ROOM_CAPACITY_MAP else None self.total = float(data['total']) self.fees = 0 @@ -96,6 +97,8 @@ class Order: self.payment_provider = data['payment_provider'] self.comment = data['comment'] self.phone = data['phone'] + self.loadAns() + def loadAns(self): self.shirt_size = self.ans('shirt_size') self.is_artist = True if self.ans('is_artist') != 'No' else False self.is_fursuiter = True if self.ans('is_fursuiter') != 'No' else False @@ -122,8 +125,8 @@ class Order: self.nfc_id = self.ans('nfc_id') self.can_scan_nfc = True if self.ans('can_scan_nfc') != 'No' else False self.actual_room = self.ans('actual_room') + self.staff_role = self.ans('staff_role') self.telegram_username = self.ans('telegram_username').strip('@') if self.ans('telegram_username') else None - def __getitem__(self, var): return self.data[var] @@ -135,6 +138,12 @@ class Order: return bool(a['answer'] == 'True') return a['answer'] return None + + def isBadgeValid (self): + return self.ans('propic') and (not self.is_fursuiter or self.ans('propic_fursuiter')) + + def isAdmin (self): + return self.code in ADMINS or self.staff_role in ADMINS_PRETIX_ROLE_NAMES async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes): if(mimeType != None and data != None): @@ -147,6 +156,7 @@ class Order: await self.edit_answer(name, res['id']) else: await self.edit_answer(name, None) + self.loadAns() async def edit_answer(self, name, new_answer): found = False @@ -168,7 +178,6 @@ class Order: async with httpx.AsyncClient() as client: res = await client.get(join(base_url_event, 'questions/'), headers=headers) res = res.json() - for r in res['results']: if r['identifier'] != name: continue @@ -178,6 +187,7 @@ class Order: 'answer': new_answer, 'options': r['options'] }) + self.loadAns() async def send_answers(self): async with httpx.AsyncClient() as client: @@ -204,6 +214,7 @@ class Order: self.pending_update = False self.time = -1 + self.loadAns() @dataclass class Quotas: @@ -254,6 +265,7 @@ class OrderManager: self.order_list.remove(code) async def fill_cache(self): + await loadItems() await loadQuestions() self.empty() p = 0 diff --git a/karaoke.py b/karaoke.py index 3b2a15b..cf92500 100644 --- a/karaoke.py +++ b/karaoke.py @@ -10,7 +10,7 @@ bp = Blueprint("karaoke", url_prefix="/manage/karaoke") @bp.get("/admin") async def show_songs(request, order: Order): - if order.code not in ADMINS: + if not order.isAdmin(): raise exceptions.Forbidden("Birichino") orders = [x for x in request.app.ctx.om.cache.values() if x.karaoke_songs] @@ -28,7 +28,7 @@ async def show_songs(request, order: Order): @bp.post("/approve") async def approve_songs(request, order: Order): - if order.code not in ADMINS: + if not order.isAdmin(): raise exceptions.Forbidden("Birichino") for song in request.form: @@ -44,7 +44,7 @@ async def sing_song(request, order: Order, songname): if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") - if order.code not in ADMINS: + if not order.isAdmin(): raise exceptions.Forbidden("Birichino") songname = unquote(songname) diff --git a/propic.py b/propic.py index 907caa9..c3fe26f 100644 --- a/propic.py +++ b/propic.py @@ -6,14 +6,19 @@ from PIL import Image from io import BytesIO from hashlib import sha224 from time import time +import os 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") + if (DEV_MODE): + print("Resetting {fn} picture for {orderCod}".format(fn="Badge" if not isFursuiter else "fursuit", orderCod = order.code)) with open("res/propic/default.png", "rb") as f: data = f.read() + f.close() + os.remove(f"res/propic/{order.ans(f'propic{s}')}") # converted file + os.remove(f"res/propic/{order.ans(f'propic{s}').split(".jpg")[0]}_original.jpg") # original file 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() @@ -41,17 +46,30 @@ async def upload_propic(request, order: Order): if not body[0].body: continue - h = sha224(body[0].body).hexdigest()[:32] + # Check max file size + if len(body[0].body) > PROPIC_MAX_FILE_SIZE: + raise exceptions.BadRequest("File size too large for " + ("Profile picture" if fn == 'propic' else 'Fursuit picture')) + h = sha224(body[0].body).hexdigest()[:32] + errorDetails = '' try: img = Image.open(BytesIO(body[0].body)) - if(img.size[0] > 2048 or img.size[1] > 2048): - raise exceptions.BadRequest("Maximum allowed dimensions: 2048x2048") - - with open(f"res/propic/{fn}_{order.code}_original", "wb") as f: - f.write(body[0].body) - width, height = img.size + # Checking for min / max size + if width < PROPIC_MIN_SIZE[0] or height < PROPIC_MIN_SIZE[1]: + errorDetails = "Image too small [{width}x{width}] for {pfpn}".format(width=width, pfpn=("Profile picture" if fn == 'propic' else 'Fursuit picture')) + raise exceptions.BadRequest(errorDetails) + + if width > PROPIC_MAX_SIZE[0] or height > PROPIC_MAX_SIZE[1]: + errorDetails = "Image too big [{width}x{width}] for {pfpn}".format(width=width, pfpn=("Profile picture" if fn == 'propic' else 'Fursuit picture')) + raise exceptions.BadRequest(errorDetails) + + + with open(f"res/propic/{fn}_{order.code}_original.jpg", "wb") as f: + f.write(body[0].body) + f.flush() + f.close() + aspect_ratio = width/height if aspect_ratio > 1: crop_amount = (width - height) / 2 @@ -61,19 +79,22 @@ async def upload_propic(request, order: Order): img = img.crop((0, crop_amount, width, height - crop_amount)) img = img.convert('RGB') + width, height = img.size + img.thumbnail((512,512)) 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) + f.flush() + f.close() 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.") + if DEV_MODE: print(traceback.format_exc()) + raise exceptions.BadRequest(errorDetails if errorDetails else "The image you uploaded is not valid.") else: await order.edit_answer(fn, f"{fn}_{order.code}_{h}.jpg") diff --git a/res/icons/delete.svg b/res/icons/delete.svg new file mode 100644 index 0000000..560d174 --- /dev/null +++ b/res/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/icons/door_open.svg b/res/icons/door_open.svg new file mode 100644 index 0000000..965f49e --- /dev/null +++ b/res/icons/door_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/icons/favicon.ico b/res/icons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..62d4f1090f9567623c249b12796adcb7eaf852c7 GIT binary patch literal 15406 zcmeHN2~?9u9{-X6qQwhEsRhAxiCSoy@u1`{oS1> zT-Tc=|FPS=ZKJxndVqwcZzVZYVqLomj;fY3h=%=yw@HNBRbw3JuXl@s7i{jN)OecTWA1HU+^;>mo0tD&&l3I7C znKBr(ARrI#0%B~pN&UP-6X5(pp`9iY>xA0)Q!PJCzkMe*-0S|$q7?*}bOtR4T}#52#T1@WHS59 zpc7V9>Jxx0bDa$0D2f?heT^BzeSGv>N~C{igdN0Vu!ME5Ll!K2vtayRf(jX43MoE! zY6tC3s$1oj(vabnSX002V)ng5XMf%9?;rG+uHOcnc7*w(5fqbDLxBa;oU@V6S`J?C z%n@X*%WQ1klv-W0J3b@M^IV*BKtRCf5EC^zo!&?K-?*|OAB|kiZFgQ-D7d|{uGvylUb-E#(0To7b#=9uIyyR2-*EKJ+mcE=#J&{U*_WdWODhCV?91EG?9*}h z6K!q5w}IhldoEu2-I z95t;%P-RmlXtF6o$hJ&?XOy(G+vnCcuSzekaY-q>;dUj{Z(l_0_sr06OE%<0QpEO( zEbRaFYTDXUZ25Tjlv2ESG9Lw)^ALX`54B7zL}(%pV~auop3H4URs~J1Uvp~t^OL!{ zkektLlPfh{;y43Hj>hu!HHJJ5vU1srNI!6xcXDCO_zL>$mqpn6%UtaJMIOFtl#8Al zWg}#igYoBCFYg;9fPN_M!u$|kY3`w+L$+MX?UjDuBjc|go^SL+MZ7>>sWmzqn;3Ag zo&H^PQtwthdsGZhT0Sby+T*)K|9jvv2u;?MF*of&Qpcp}ox4<6k zzcd>cP%@;WWz6Y6h+7?3%rSAc!X5s@6#^GyWE0s=?0 z)Ea>5oCJcPDyj&mBJjaQKv8?>_vM&haDKjglK_@cDpd8KCh!K;rwJ+9W|d0eS}wm&wn;!tE-)UmL*%|5IU z>k|YXTfl7r5O*3*l>Qf|MDZ99VvBlw%ly=bG`Mla#PB) zrmfc>Jl%OIKhyI}e9#FN%h5k1M4z1dVmPnrVxJboem~@WEs%FcDcgRLE$(p7_cqvPopO=AQyvapm5&Pkk<3!_Fqzc6>u14kaOB)-;d;1GCw6w6X(|Y8XRo1dDr}Cj|W>LoWq&w&L#$NU~#Ex}5 z9C^vYE99(x5R<7*?iJpfKj6pmYSe$}1Ee?q5uQ7{`1x_;U-nml{m5YLMm&pOhv!zc z;{_GXXkqD#c6yD8F#*96xuBq9kyUgi-Q}b(cY%2w|wiSX_fWWo_;Th!uJhQ9~%`9nZw<~O9Yo@JT)_*4VJHFlDZR17ZC=z))i}_tX6} zZhU0&^r>ao)w&G(Tb1HFmc_V!N-=7l!pDLs#TZ-i5w_;HW9y<;G>zBXIxnxTX?fP8 z2FF`v55G;~W^cHj8|iv2d%g>cBc6|uHK%jkAvq^mQx%kZlJ%FYU(Mjd*@2@*4I}@7 znZ!fGEckemMKM}o&PSebkHee5LyhALajO{@p$UZ;6TsaLJOSKxWX^3B{5AJggH6Vx zotEd*;fxp4Ot@T)Rd-4TZAz{j;x4@72?_|99CYm1oRi0oPf@^te`7y`b_V3ZKfJ^W z+yfbdFQ!G<)Rd3seZ@sbO!6_uBoE&=&PC4v?Z&wni!d&yt#RDVM?V|Dom7VuxEnI( zaMWD@A+e8>mn5WqcnCqy(UPY&cB zN5NY17kFhCar<9^55K`VO0@_Kv3C(o!m|?WKM95__3TI(D3SfGVEbM8&-8bI)xE?j zH8pA)$m^%<;DbK#`STpq$vG%oY03p)Q+g4%|E>A6(k+us;YV-3D*ut~L-$cD(fspA z$RAYWw~so>Tf}zGKd`PwL%UG`axS0 \ No newline at end of file diff --git a/res/scripts/roomManager.js b/res/scripts/roomManager.js new file mode 100644 index 0000000..1eebee1 --- /dev/null +++ b/res/scripts/roomManager.js @@ -0,0 +1,28 @@ +function confirmAction (intent, sender) { + if (['rename', 'unconfirm', 'delete'].includes (intent) == false) return + let href = sender.getAttribute('action') + let intentTitle = document.querySelector("#intentText") + let intentEdit = document.querySelector("#intentRename") + let intentEditPanel = document.querySelector("#intentEditPanel") + let intentFormAction = document.querySelector("#intentFormAction") + let intentSend = document.querySelector("#intentSend") + // Resetting ui + intentEdit.setAttribute('required', false) + intentFormAction.setAttribute('method', 'GET') + intentEditPanel.style.display = 'none'; + + intentTitle.innerText = intent + ' room' + intentFormAction.setAttribute('action', href) + switch (intent){ + case 'rename': + intentEditPanel.style.display = 'block'; + intentEdit.setAttribute('required', true) + intentFormAction.setAttribute('method', 'POST') + break + case 'unconfirm': + break + case 'delete': + break + } + document.getElementById('modalRoomconfirm').setAttribute('open', 'true'); +} \ No newline at end of file diff --git a/res/styles/admin.css b/res/styles/admin.css new file mode 100644 index 0000000..665f287 --- /dev/null +++ b/res/styles/admin.css @@ -0,0 +1,18 @@ +div.room-actions { + float: right; +} + +div.room-actions > a { + background-color: var(--card-background-color); + font-size: 12pt; + padding: 0.7rem; + border-radius: 1rem; +} + +div.room-actions > a:hover { + background-color: var(--primary-focus); +} + +div.room-actions > a.act-del:hover { + background-color: var(--del-color); +} \ No newline at end of file diff --git a/res/styles/base.css b/res/styles/base.css index 810f5e5..890dd71 100644 --- a/res/styles/base.css +++ b/res/styles/base.css @@ -1,5 +1,7 @@ /* Other blocks' styles */ @import url('propic.css'); +@import url('admin.css'); +@import url('room.css'); summary:has(span.status) { background-color: #ffaf0377; diff --git a/res/styles/room.css b/res/styles/room.css new file mode 100644 index 0000000..700af6c --- /dev/null +++ b/res/styles/room.css @@ -0,0 +1,14 @@ +span.nsc-room-counter { + font-size: medium; + color: var(--color); +} + +#intentFormAction #intentText { + text-transform: capitalize; +} + +@media only screen and (max-width: 500px) { + div.room-actions a>span { + display: none; + } +} \ No newline at end of file diff --git a/room.py b/room.py index fd5760f..4f0f118 100644 --- a/room.py +++ b/room.py @@ -49,7 +49,7 @@ async def delete_room(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.room_id != order.code: + if order.room_owner: raise exceptions.BadRequest("You are not allowed to delete room of others.") if order.ans('room_confirmed'): @@ -176,6 +176,9 @@ async def cancel_request(request, order: Order): async def approve_roomreq(request, code, 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 not order.room_owner: + raise exceptions.BadRequest("You are not the owner of the room!") if not code in order.pending_roommates: raise exceptions.BadRequest("You cannot accept people that didn't request to join your room") @@ -230,6 +233,9 @@ async def leave_room(request, order: Order): async def reject_roomreq(request, code, 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 not order.room_owner: + raise exceptions.BadRequest("You are not the owner of the room!") if not code in order.pending_roommates: raise exceptions.BadRequest("You cannot reject people that didn't request to join your room") @@ -252,6 +258,32 @@ async def reject_roomreq(request, code, order: Order): await order.send_answers() return redirect('/manage/welcome') + +@bp.post("/rename") +async def rename_room(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 not order.room_owner: + raise exceptions.BadRequest("You are not the owner of the room!") + + if not order.room_id: + raise exceptions.BadRequest("Try joining a room before renaming it.") + + if order.room_confirmed: + raise exceptions.BadRequest("You can't rename a confirmed room!") + + if order.room_id != order.code: + raise exceptions.BadRequest("You are not allowed to rename rooms of others.") + + name = request.form.get('name') + if len(name) > 64 or len(name) < 4: + raise exceptions.BadRequest("Your room name is invalid. Please try another one.") + + await order.edit_answer("room_name", name) + await order.send_answers() + + return redirect('/manage/welcome') @bp.route("/confirm") async def confirm_room(request, order: Order, quotas: Quotas): diff --git a/tpl/admin.html b/tpl/admin.html new file mode 100644 index 0000000..2fd08ce --- /dev/null +++ b/tpl/admin.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Admin panel{% endblock %} +{% block main %} +
+
+ + + + +
+ +

Admin panel

+ Clear cache + Manage rooms +
+
+ +{% endblock %} diff --git a/tpl/base.html b/tpl/base.html index 850b16c..3dfc06c 100644 --- a/tpl/base.html +++ b/tpl/base.html @@ -7,6 +7,7 @@ +