2024-01-08 21:02:27 +00:00
from sanic import response , redirect , Blueprint , exceptions
2024-02-29 10:41:49 +00:00
from email_util import send_missing_propic_message
2024-05-19 22:35:25 +00:00
from random import choice
2024-01-18 17:03:32 +00:00
from room import unconfirm_room_by_order
2024-01-08 21:02:27 +00:00
from config import *
from utils import *
from ext import *
2024-05-13 11:30:21 +00:00
from io import StringIO
2024-01-21 23:35:44 +00:00
from sanic . log import logger
2024-05-13 11:30:21 +00:00
import csv
import time
2024-05-18 22:06:36 +00:00
import json
import math
2024-01-08 21:02:27 +00:00
bp = Blueprint ( " admin " , url_prefix = " /manage/admin " )
2024-01-21 23:35:44 +00:00
@bp.middleware
async def credentials_check ( request : Request ) :
order = await get_order ( request )
2024-01-08 21:02:27 +00:00
if not order :
raise exceptions . Forbidden ( " You have been logged out. Please access the link in your E-Mail to login again! " )
2024-01-13 15:59:24 +00:00
if EXTRA_PRINTS :
2024-01-21 23:35:44 +00:00
logger . info ( f " Checking admin credentials of { order . code } with secret { order . secret } " )
2024-01-08 21:02:27 +00:00
if not order . isAdmin ( ) : raise exceptions . Forbidden ( " Birichino :) " )
2024-01-13 15:59:24 +00:00
2024-01-08 21:02:27 +00:00
@bp.get ( ' /cache/clear ' )
2024-01-18 17:03:32 +00:00
async def clear_cache ( request , order : Order ) :
2024-02-22 18:15:33 +00:00
success = await request . app . ctx . om . fill_cache ( )
if not success : raise exceptions . ServerError ( " An error occurred while loading the cache " )
2024-01-08 21:02:27 +00:00
return redirect ( f ' /manage/admin ' )
2024-01-13 15:59:24 +00:00
@bp.get ( ' /loginas/<code> ' )
2024-01-18 17:03:32 +00:00
async def login_as ( request , code , order : Order ) :
dOrder = await get_order_by_code ( request , code , throwException = True )
2024-01-13 15:59:24 +00:00
if ( dOrder . isAdmin ( ) ) :
raise exceptions . Forbidden ( " You can ' t login as another admin! " )
if EXTRA_PRINTS :
2024-01-21 23:35:44 +00:00
logger . info ( f " Swapping login: { order . secret } { order . code } -> { dOrder . secret } { code } " )
2024-01-13 15:59:24 +00:00
r = redirect ( f ' /manage/welcome ' )
r . cookies [ ' foxo_code_ORG ' ] = order . code
r . cookies [ ' foxo_secret_ORG ' ] = order . secret
r . cookies [ ' foxo_code ' ] = code
r . cookies [ ' foxo_secret ' ] = dOrder . secret
return r
2024-01-21 23:35:44 +00:00
@bp.get ( ' /room/verify ' )
async def verify_rooms ( request , order : Order ) :
2024-05-13 11:30:21 +00:00
await clear_cache ( request , order )
2024-02-22 18:15:33 +00:00
already_checked , success = await request . app . ctx . om . update_cache ( )
if not already_checked and success :
2024-01-21 23:35:44 +00:00
orders = filter ( lambda x : x . status not in [ ' c ' , ' e ' ] and x . room_id == x . code , request . app . ctx . om . cache . values ( ) )
await validate_rooms ( request , orders , None )
return redirect ( f ' /manage/admin ' )
2024-01-08 21:02:27 +00:00
@bp.get ( ' /room/unconfirm/<code> ' )
2024-01-18 17:03:32 +00:00
async def unconfirm_room ( request , code , order : Order ) :
dOrder = await get_order_by_code ( request , code , throwException = True )
2024-01-21 23:35:44 +00:00
await unconfirm_room_by_order ( order = dOrder , throw = True , request = request )
2024-01-08 21:02:27 +00:00
return redirect ( f ' /manage/nosecount ' )
2024-05-13 10:01:47 +00:00
@bp.get ( ' /room/autoconfirm ' )
async def autoconfirm_room ( request , code , order : Order ) :
2024-05-13 11:30:21 +00:00
await clear_cache ( request , order )
2024-05-13 10:01:47 +00:00
orders = request . app . ctx . om . cache . values ( )
for order in orders :
if ( order . code == order . room_id and not order . room_confirmed and len ( order . room_members ) == order . room_person_no ) :
logger . info ( f " Auto-Confirming room { order . room_id } " )
await confirm_room_by_order ( order , request )
return redirect ( f ' /manage/admin ' )
2024-01-08 21:02:27 +00:00
@bp.get ( ' /room/delete/<code> ' )
2024-01-18 17:03:32 +00:00
async def delete_room ( request , code , order : Order ) :
2024-05-13 11:30:21 +00:00
await clear_cache ( request , order )
2024-01-18 17:03:32 +00:00
dOrder = await get_order_by_code ( request , code , throwException = True )
2024-01-08 21:02:27 +00:00
2024-01-21 23:35:44 +00:00
ppl = await get_people_in_room_by_code ( request , code )
2024-01-08 21:02:27 +00:00
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/<code> ' )
2024-01-18 17:03:32 +00:00
async def rename_room ( request , code , order : Order ) :
2024-05-13 11:30:21 +00:00
await clear_cache ( request , order )
2024-01-18 17:03:32 +00:00
dOrder = await get_order_by_code ( request , code , throwException = True )
2024-01-08 21:02:27 +00:00
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 ( )
2024-02-29 10:41:49 +00:00
return redirect ( f ' /manage/nosecount ' )
2024-05-14 16:03:19 +00:00
@bp.get ( ' /room/wizard ' )
async def room_wizard ( request , order : Order ) :
''' Tries to autofill unconfirmed rooms and other matches together '''
# Clear cache first
await clear_cache ( request , order )
#Separate orders which have incomplete rooms and which have no rooms
2024-05-18 22:06:36 +00:00
all_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 ' ] and not value . daily }
2024-05-16 21:52:09 +00:00
orders = { key : value for key , value in sorted ( all_orders . items ( ) , key = lambda x : x [ 1 ] . ans ( ' fursona_name ' ) ) if not value . room_confirmed }
2024-05-14 16:03:19 +00:00
# Orders with incomplete rooms
2024-05-14 21:31:31 +00:00
incomplete_orders = { key : value for key , value in orders . items ( ) if value . code == value . room_id and ( value . room_person_no - len ( value . room_members ) ) > 0 }
# Roomless furs
roomless_orders = { key : value for key , value in orders . items ( ) if not value . room_id and not value . daily }
# Result map
2024-05-15 16:02:38 +00:00
result_map = { }
2024-05-19 19:07:31 +00:00
# Check overflows
2024-05-19 09:03:48 +00:00
room_quota_overflow = { }
2024-05-18 22:06:36 +00:00
for key , value in ITEM_VARIATIONS_MAP [ ' bed_in_room ' ] . items ( ) :
2024-05-19 19:07:31 +00:00
room_quota = get_quota ( ITEMS_ID_MAP [ ' bed_in_room ' ] , value )
2024-05-18 22:06:36 +00:00
capacity = ROOM_CAPACITY_MAP [ key ] if key in ROOM_CAPACITY_MAP else 1
2024-05-19 09:03:48 +00:00
current_quota = len ( list ( filter ( lambda y : y . bed_in_room == value and y . room_owner == True , orders . values ( ) ) ) )
2024-05-19 19:07:31 +00:00
room_quota_overflow [ value ] = current_quota - int ( room_quota . size / capacity ) if room_quota else 0
2024-05-19 09:03:48 +00:00
# Init rooms to remove
result_map [ " void " ] = [ ]
2024-05-19 19:07:31 +00:00
# Remove rooms that are over quota
2024-05-19 09:03:48 +00:00
for room_type , overflow_qty in { key : value for key , value in room_quota_overflow . items ( ) if value > 0 } . items ( ) :
sorted_rooms = sorted ( incomplete_orders . values ( ) , key = lambda r : len ( r . room_members ) )
for room_to_remove in sorted_rooms [ : overflow_qty ] :
# Room codes to remove
result_map [ " void " ] . append ( room_to_remove . code )
# Move room members to the roomless list
for member_code in room_to_remove . room_members :
roomless_orders [ member_code ] = all_orders [ member_code ]
del incomplete_orders [ room_to_remove . code ]
2024-05-18 22:06:36 +00:00
2024-05-14 21:31:31 +00:00
# Fill already existing rooms
for room_order in incomplete_orders . items ( ) :
2024-05-15 16:02:38 +00:00
room = room_order [ 1 ]
2024-05-14 21:31:31 +00:00
to_add = [ ]
missing_slots = room . room_person_no - len ( room . room_members )
2024-05-15 16:02:38 +00:00
for i in range ( missing_slots ) :
2024-05-14 21:31:31 +00:00
compatible_roomates = { key : value for key , value in roomless_orders . items ( ) if value . bed_in_room == room . bed_in_room }
if len ( compatible_roomates . items ( ) ) == 0 : break
# Try picking a roomate that's from the same country and room type
country = room . country . lower ( )
roomless_by_country = { key : value for key , value in compatible_roomates . items ( ) if value . country . lower ( ) == country }
if len ( roomless_by_country . items ( ) ) > 0 :
2024-05-15 16:02:38 +00:00
code_to_add = list ( roomless_by_country . keys ( ) ) [ 0 ]
2024-05-14 21:31:31 +00:00
to_add . append ( code_to_add )
del roomless_orders [ code_to_add ]
else :
# If not, add first roomless there is
2024-05-15 16:02:38 +00:00
code_to_add = list ( compatible_roomates . keys ( ) ) [ 0 ]
2024-05-14 21:31:31 +00:00
to_add . append ( code_to_add )
del roomless_orders [ code_to_add ]
result_map [ room . code ] = {
' type ' : ' add_existing ' ,
' to_add ' : to_add
}
2024-05-16 21:52:09 +00:00
generated_counter = 0
2024-05-14 21:31:31 +00:00
# Create additional rooms
2024-05-15 16:02:38 +00:00
while len ( roomless_orders . items ( ) ) > 0 :
room = list ( roomless_orders . items ( ) ) [ 0 ] [ 1 ]
to_add = [ ]
missing_slots = room . room_person_no - len ( room . room_members )
for i in range ( missing_slots ) :
compatible_roomates = { key : value for key , value in roomless_orders . items ( ) if value . bed_in_room == room . bed_in_room }
if len ( compatible_roomates . items ( ) ) == 0 : break
# Try picking a roomate that's from the same country and room type
country = room . country . lower ( )
roomless_by_country = { key : value for key , value in compatible_roomates . items ( ) if value . country . lower ( ) == country }
if len ( roomless_by_country . items ( ) ) > 0 :
code_to_add = list ( roomless_by_country . keys ( ) ) [ 0 ]
to_add . append ( code_to_add )
del roomless_orders [ code_to_add ]
else :
# If not, add first roomless there is
code_to_add = list ( compatible_roomates . keys ( ) ) [ 0 ]
to_add . append ( code_to_add )
del roomless_orders [ code_to_add ]
2024-05-16 21:52:09 +00:00
generated_counter + = 1
2024-05-15 16:02:38 +00:00
result_map [ room . code ] = {
' type ' : ' new ' ,
2024-05-16 21:52:09 +00:00
' room_name ' : f ' Generated Room { generated_counter } ' ,
2024-05-15 16:02:38 +00:00
' room_type ' : room . bed_in_room ,
' to_add ' : to_add
}
2024-05-18 22:06:36 +00:00
result_map [ " infinite " ] = { ' to_add ' : [ ] }
2024-05-15 16:02:38 +00:00
tpl = request . app . ctx . tpl . get_template ( ' wizard.html ' )
2024-05-18 22:06:36 +00:00
return html ( tpl . render ( order = order , all_orders = all_orders , unconfirmed_orders = orders , data = result_map , jsondata = json . dumps ( result_map , skipkeys = True , ensure_ascii = False ) ) )
2024-05-14 16:03:19 +00:00
2024-05-19 19:07:31 +00:00
@bp.post ( ' /room/wizard/submit ' )
2024-05-19 22:35:25 +00:00
async def submit_from_room_wizard ( request : Request , order : Order ) :
2024-05-19 19:07:31 +00:00
''' Will apply changes to the rooms '''
2024-05-19 22:35:25 +00:00
await request . app . ctx . om . fill_cache ( )
data = json . loads ( request . body )
# Phase 1 - Delete all rooms in void
if ' void ' in data :
for room_code in data [ ' void ' ] :
ppl = await get_people_in_room_by_code ( request , room_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 ( )
logger . info ( f " Deleted rooms { ' , ' . join ( data [ ' void ' ] ) } " )
# Phase 2 - Join roomless to other rooms or add new rooms
for room_code , value in { key : value for key , value in data . items ( ) if key . lower ( ) not in [ ' void ' , ' infinite ' ] } . items ( ) :
if not value [ ' to_add ' ] or len ( value [ ' to_add ' ] ) == 0 : continue
room_order = await request . app . ctx . om . get_order ( code = room_code )
# Preconditions
if not room_order : raise exceptions . BadRequest ( f " Order { room_code } does not exist. " )
if room_order . daily == True : raise exceptions . BadRequest ( f " Order { room_code } is daily. " )
if room_order . status != ' paid ' : raise exceptions . BadRequest ( f " Order { room_code } hasn ' t paid. " )
if room_order . room_owner :
if room_order . room_person_no < len ( room_order . room_members ) + ( len ( value [ ' to_add ' ] ) if value [ ' to_add ' ] else 0 ) :
raise exceptions . BadRequest ( f " Input exceeds room { room_order . code } capacity. " )
elif room_order . room_person_no < ( len ( value [ ' to_add ' ] ) if value [ ' to_add ' ] else 0 ) :
raise exceptions . BadRequest ( f " Input exceeds room { room_order . code } capacity. " )
# Adding roomless orders to existing rooms
if value [ ' type ' ] == ' add_existing ' or value [ ' type ' ] == ' new ' :
if value [ ' type ' ] == ' new ' :
if room_order . room_owner : exceptions . BadRequest ( f " Order { room_code } is already a room owner. " )
# Create room data
await room_order . edit_answer ( ' room_name ' , value [ ' room_name ' ] )
await room_order . edit_answer ( ' room_id ' , room_order . code )
await room_order . edit_answer ( ' room_secret ' , ' ' . join ( choice ( ' 0123456789 ' ) for _ in range ( 6 ) ) )
elif not room_order . room_owner :
raise exceptions . BadRequest ( f " Order { room_code } is not a room owner. " )
# Add members
for new_member_code in value [ ' to_add ' ] :
pending_member = await request . app . ctx . om . get_order ( code = new_member_code )
# Preconditions
if pending_member . daily == True : raise exceptions . BadRequest ( f " Order { pending_member . code } is daily. " )
if pending_member . status != ' paid ' : raise exceptions . BadRequest ( f " Order { new_member_code } hasn ' t paid. " )
if pending_member . bed_in_room != room_order . bed_in_room : raise exceptions . BadRequest ( f " Order { new_member_code } has a different room type than { room_code } . " )
if pending_member . room_owner : exceptions . BadRequest ( f " Order { new_member_code } is already a room owner. " )
if pending_member . room_id and pending_member . room_id not in data [ ' void ' ] : exceptions . BadRequest ( f " Order { new_member_code } is in another room. " )
await pending_member . edit_answer ( ' room_id ' , room_order . code )
await pending_member . edit_answer ( ' room_confirmed ' , " True " )
await pending_member . edit_answer ( ' pending_room ' , None )
await pending_member . send_answers ( )
logger . info ( f " { ' Created ' if value [ ' type ' ] == ' new ' else ' Edited ' } { str ( room_order ) } " )
# Confirm members that were already inside the room
if value [ ' type ' ] == ' add_existing ' :
for already_member in list ( filter ( lambda rm : rm . code in room_order . room_members and rm . code != room_order . code , request . app . ctx . om . cache . values ( ) ) ) :
await already_member . edit_answer ( ' room_confirmed ' , " True " )
await already_member . send_answers ( )
else : raise exceptions . BadRequest ( f " Unexpected type ( { value [ ' type ' ] } ) " )
await room_order . edit_answer ( ' pending_room ' , None )
await room_order . edit_answer ( ' pending_roommates ' , None )
await room_order . edit_answer ( ' room_confirmed ' , " True " )
await room_order . edit_answer ( ' room_members ' , ' , ' . join ( list ( set ( [ * room_order . room_members , room_order . code , * value [ ' to_add ' ] ] ) ) ) )
await room_order . send_answers ( )
await request . app . ctx . om . fill_cache ( )
return text ( ' done ' , status = 200 )
2024-05-19 19:07:31 +00:00
2024-02-29 10:41:49 +00:00
@bp.get ( ' /propic/remind ' )
async def propic_remind_missing ( request , order : Order ) :
await clear_cache ( request , order )
orders = request . app . ctx . om . cache . values ( )
order : Order
for order in orders :
missingPropic = order . propic is None
missingFursuitPropic = order . is_fursuiter and order . propic_fursuiter is None
if ( missingPropic or missingFursuitPropic ) :
# print(f"{order.code}: prp={missingPropic} fpr={missingFursuitPropic} - {order.name}")
await send_missing_propic_message ( order , missingPropic , missingFursuitPropic )
2024-05-13 11:30:21 +00:00
return redirect ( f ' /manage/admin ' )
@bp.get ( ' /export/export ' )
async def export_export ( request , order : Order ) :
await clear_cache ( request , order )
data = StringIO ( )
w = csv . writer ( data )
w . writerow ( [ ' Status ' , ' Code ' , ' First name ' , ' Last name ' , ' Nick ' , ' State ' , ' Card ' , ' Artist ' , ' Fursuiter ' , ' Sponsorhip ' , ' Early ' , ' Late ' , ' Daily ' , ' Daily days ' , ' Shirt ' , ' Room type ' , ' Room count ' , ' Room members ' , ' Payment ' , ' Price ' , ' Refunds ' , ' Staff ' ] )
orders = request . app . ctx . om . cache . values ( )
order : Order
for order in orders :
w . writerow ( [
order . status ,
order . code ,
order . first_name ,
order . last_name ,
order . name ,
order . country ,
order . has_card ,
order . is_artist ,
order . is_fursuiter ,
order . sponsorship ,
order . has_early ,
order . has_late ,
order . daily ,
' , ' . join ( order . dailyDays ) ,
order . shirt_size ,
ROOM_TYPE_NAMES [ order . bed_in_room ] if order . bed_in_room in ROOM_TYPE_NAMES else " - " ,
len ( order . room_members ) ,
' , ' . join ( order . room_members ) ,
order . payment_provider ,
order . total - order . fees ,
order . refunds ,
order . ans ( ' staff_role ' ) or ' attendee ' ,
] )
data . seek ( 0 )
str = data . read ( ) #data.read().decode("UTF-8")
data . flush ( )
data . close ( )
return raw ( str , status = 200 , headers = { ' Content-Disposition ' : f ' attachment; filename= " export_ { int ( time . time ( ) ) } .csv " ' , " Content-Type " : " text/csv; charset=UTF-8 " } )
@bp.get ( ' /export/hotel ' )
async def export_hotel ( request , order : Order ) :
await clear_cache ( request , order )
data = StringIO ( )
w = csv . writer ( data )
w . writerow ( [ ' Room type ' , ' Room name ' , ' Room code ' , ' First name ' , ' Last name ' , ' Birthday ' , ' Address ' , ' Email ' , ' Phone number ' , ' Status ' ] )
orders = sorted ( request . app . ctx . om . cache . values ( ) , key = lambda d : ( d . room_id if d . room_id != None else " ~ " ) )
order : Order
for order in orders :
w . writerow ( [
ROOM_TYPE_NAMES [ order . bed_in_room ] if order . bed_in_room in ROOM_TYPE_NAMES else " - " ,
order . room_name ,
order . room_id ,
order . first_name ,
order . last_name ,
order . birth_date ,
order . address ,
order . email ,
order . phone ,
order . status ,
order . code
] )
data . seek ( 0 )
str = data . read ( ) #data.read().decode("UTF-8")
data . flush ( )
data . close ( )
return raw ( str , status = 200 , headers = { ' Content-Disposition ' : f ' attachment; filename= " hotel_ { int ( time . time ( ) ) } .csv " ' , " Content-Type " : " text/csv; charset=UTF-8 " } )