File size: 28,322 Bytes
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
737dbae
7434f34
 
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
 
 
 
 
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
bc516e8
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aeb301d
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
1c152e2
aeb301d
1c152e2
 
 
 
aeb301d
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aeb301d
 
 
 
 
a6ff157
aeb301d
1c152e2
 
a6ff157
1c152e2
a6ff157
 
 
aae4e9f
a6ff157
 
aae4e9f
a6ff157
1c152e2
a6ff157
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
1c152e2
 
 
f4a355a
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd0aa34
 
1c152e2
 
 
f081fc3
 
 
1c152e2
 
 
 
 
 
 
 
f081fc3
1c152e2
fb2e9f6
 
1c152e2
 
f081fc3
1c152e2
fb2e9f6
f081fc3
1c152e2
 
 
 
 
 
 
 
 
 
f081fc3
1c152e2
fb2e9f6
 
1c152e2
 
f081fc3
1c152e2
fb2e9f6
f081fc3
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c152e2
 
f4a355a
1c152e2
f4a355a
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f081fc3
1c152e2
f081fc3
 
1c152e2
 
 
 
 
f4a355a
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f081fc3
1c152e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4a355a
 
 
 
 
 
 
 
 
 
1c152e2
 
 
 
 
 
 
dc8c07b
1c152e2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
import pandas as pd 
import copy
import os 
import gradio as gr
from collections import Counter
import random
import re
from datetime import date
import supabase
import json


### LOAD PRIVATE SPACE ###
url = "Juggling/Schedule_Buddy_Updated"
token = os.environ['TOKEN']
generator = gr.load(url, src="spaces", token=token)



### CONSTANTS ###
NAME_COL = 'Juggler_Name'
NUM_WORKSHOPS_COL = 'Num_Workshops'
AVAIL_COL = 'Availability'
DESCRIP_COL = 'Workshop_Descriptions'
EMAIL_COL = 'Email'
DELIMITER = ';'
ALERT_TIME = None # leave warnings on screen indefinitely
FORM_NOT_FOUND = 'Form not found'
INCORRECT_PASSWORD = "The password is incorrect. Please check the password and try again. If you don't remember your password, please email jugglinggym@gmail.com."
NUM_ROWS = 1
NUM_COLS_SCHEDULES = 2
NUM_COLS_ALL_RESPONSES = 4
MIN_LENGTH = 6
NUM_RESULTS = 10 # randomly get {NUM_RESULTS} results


theme = gr.themes.Soft(
    primary_hue="cyan",
    secondary_hue="pink",
    font=[gr.themes.GoogleFont('sans-serif'), 'ui-sans-serif', 'system-ui', 'Montserrat'],
)

### Connect to Supabase ###
#URL = os.environ['URL']
#API_KEY = os.environ['API_KEY']
# TODO 
URL = 'https://ubngctgvhjgxkvimdmri.supabase.co'
API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVibmdjdGd2aGpneGt2aW1kbXJpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQ5MjAwOTQsImV4cCI6MjA1MDQ5NjA5NH0.NtGdfP8GYNuYdPdsaLW5GjgfB0_7Q1kNBIDJtPhO8nY'
client = supabase.create_client(URL, API_KEY)


### DEFINE FUNCTIONS ###
## Multi-purpose function ##
'''
Returns a lowercased and stripped version of the schedule name. 
Returns: str
'''
def standardize(schedule_name: str): 
    return schedule_name.lower().strip()


## Function to make a form ##
'''
Makes a form and pushes it to Supabase. 
Returns: None
'''
def make_form(email: str, schedule_name: str, password_1: str, password_2: str, capacity: int, slots: list) -> str:
    # Error handling
    if len(email) == 0: 
        return gr.Warning('', ALERT_TIME, title="Please enter an email address")
    
    if len(schedule_name) == 0: 
        return gr.Warning('', ALERT_TIME, title=f"Please enter the form name.")
    
    if password_1 != password_2: 
        return gr.Warning('', ALERT_TIME, title=f"The passwords don't match. Password 1 is \"{password_1}\" and Password 2 is \"{password_2}\".")
    
    if len(password_1) < MIN_LENGTH: 
        return gr.Warning('', ALERT_TIME, title=f"Please make a password that is at least {MIN_LENGTH} characters.")
    
    if capacity == 0: 
        return gr.Warning('', ALERT_TIME, title=f"Please enter the capacity (how many people can teach per timeslot). It must be greater than zero.")
    
    if capacity < 0: 
        return gr.Warning('', ALERT_TIME, title=f"The capacity (number of people who can teach per timeslot) must be greater than zero.")

    if len(slots) == 0: 
        return gr.Warning('', ALERT_TIME, title="Please enter at least one timeslot. Make sure to press \"Enter\" after each one (\"Return\" if you're on mobile)!")


    # Check if schedule name already exists 
    existing_forms = []
    response = client.table('Forms').select('form_name').execute()
    for elem in response.data: 
        existing_forms.append(elem['form_name'])

    if schedule_name in existing_forms:
        return gr.Warning('', ALERT_TIME, title=f"The form name \"{schedule_name}\" already exists. Please choose a different name.")

    
    # Push to Supabase
    new_slots = [elem['name'].strip() for elem in slots]
    
    my_obj = {
        'form_name': standardize(schedule_name),
        'password': password_1,
        'email': email,
        'capacity': capacity, 
        'slots': new_slots,
        'status': 'open', 
        'date_created': str(date.today()), 
        'responses': json.dumps({
            NAME_COL: [], 
            EMAIL_COL: [],
            NUM_WORKSHOPS_COL: [], 
            AVAIL_COL: [],
            DESCRIP_COL: [],
        }),
    }

    client.table('Forms').insert(my_obj).execute()
    gr.Info('', ALERT_TIME, title="Form made successfully!")
    


## Functions to fill out a form @@
'''
Gets the timeslots for a given schedule and makes form elements visible.
Returns: 
    gr.Button: corresponds to schedule_name_btn
    gr.CheckboxGroup: corresponds to checkboxes
    gr.Column: corresponds to main_col
    gr.Button: corresponds to submit_preferences_btn
    gr.Textbox: corresponds to new_description
'''
def get_timeslots(schedule_name: str):
    # Leave everything as it was
    skip_output = gr.Button(), gr.CheckboxGroup(),gr.Column(), gr.Button(), gr.Textbox()

    if len(schedule_name) == 0: 
        gr.Warning('', ALERT_TIME, title='Please type a form name.')
        return skip_output

    response = client.table('Forms').select('status', 'slots').eq('form_name', standardize(schedule_name)).execute()
    data = response.data

    if len(data) > 0: 
        my_dict = data[0]
        if my_dict['status'] == 'closed':
            gr.Warning('', ALERT_TIME, title="This form is closed. Please contact the form administrator.")
            return skip_output
        else: 
            return gr.Button(variant='secondary'), gr.CheckboxGroup(my_dict['slots'], label="Timeslots", info="Check the time(s) you can teach", visible=True), gr.Column(visible=True), gr.Button(visible=True), gr.Textbox(visible=True)
    else: 
        gr.Warning('', ALERT_TIME, title=f"There was no form called \"{schedule_name}\". Please check the spelling and try again.")
        return skip_output
    

'''
Submits the form that the person filled out to Supabase. 
Returns: None
'''
def submit_preferences(schedule_name: str, curr_juggler_name: str, curr_email: str, curr_num_workshops: int, curr_availability: list, curr_descriptions: list): 
    # Error handling
    if len(curr_juggler_name) == 0: 
        return gr.Warning('', ALERT_TIME, title="Please enter your name.")
    
    if len(curr_email) == 0: 
        return gr.Warning('', ALERT_TIME, title="Please enter your email address.")
    
    if curr_num_workshops == 0: 
        return gr.Warning('', ALERT_TIME, title=f"Please enter how many workshops you want to teach.")

    elif curr_num_workshops < 0: 
        return gr.Warning('', ALERT_TIME, title="The number of workshops you want to teach must be positive.")
    
    if len(curr_availability) == 0: 
        return gr.Warning('', ALERT_TIME, title="Please select at least one timeslot when you are able to teach.")
    
    if curr_num_workshops > len(curr_availability): 
        return gr.Warning('', ALERT_TIME, title=f"You only selected {len(curr_availability)} timeslots. However, you said you wanted to teach {curr_num_workshops} workshops. Please make sure that you are available to teach during at least {curr_num_workshops} timeslots.")
    
    if len(curr_descriptions) == 0: 
        return gr.Warning('', ALERT_TIME, title=f"Please describe at least one workshop that you want to teach. You must hit \"Enter\" after each one (\"Return\" if you're on mobile)!")

    response = client.table('Forms').select('responses', 'slots').eq('form_name', standardize(schedule_name)).execute()
    data = response.data

    if len(data) > 0: 
        form = json.loads(data[0]['responses'])
        og_slots = data[0]['slots']

        # Add current preferences to dictionary lists
        curr_juggler_name = curr_juggler_name.strip()
        names = form[NAME_COL]
        if curr_juggler_name in names: 
            return gr.Warning('', ALERT_TIME, title=f"Someone already named \"{curr_juggler_name}\" filled out the form. Please use your last name or middle initial.")
        names.extend([curr_juggler_name])

        emails = form[EMAIL_COL]
        emails.extend([curr_email])

        bandwidths = form[NUM_WORKSHOPS_COL]
        bandwidths.extend([curr_num_workshops])

        availabilities = form[AVAIL_COL]
        # Put the checkboxes in order (rather than the order people click them!)
        final_checkboxes = []
        for elem in og_slots: 
            if elem in curr_availability: 
                final_checkboxes.append(elem)

        curr_availability = f"{DELIMITER}".join(final_checkboxes)
        availabilities.extend([curr_availability])

        # Format descriptions
        curr_descriptions = [elem['name'] for elem in curr_descriptions]
        new_str = ''
        if len(curr_descriptions) > 1:
            for i, elem in enumerate(curr_descriptions): 
                new_str += f"{i + 1}. {elem.strip()}\n"
            new_str = new_str.strip()
        else: 
            new_str += f"{curr_descriptions[0]}"

        descriptions = form[DESCRIP_COL]
        descriptions.extend([new_str])

        # Update Supabase
        my_obj = json.dumps({
            NAME_COL: names, 
            EMAIL_COL: emails, 
            NUM_WORKSHOPS_COL: bandwidths, 
            AVAIL_COL: availabilities, 
            DESCRIP_COL: descriptions
        })
        client.table('Forms').update({'responses': my_obj}).eq('form_name', standardize(schedule_name)).execute()
        return gr.Info('', ALERT_TIME, title='Form submitted successfully!')

    # I don't think it's possible to get here because I checked the schedule name earlier
    else:
        return gr.Warning('', ALERT_TIME, title=f"There was no form called \"{schedule_name}\". Please check the spelling and try again.")
    

## Functions to manage/generate schedules ## 
'''
Uses the name and password to get the form. 
Makes the buttons and other elements visible on the page. 
Returns: 
    gr.Button: corresponds to find_form_btn
    gr.Column: corresponds to all_responses_group
    gr.Column: generate_schedules_explanation
    gr.Row: corresponds to generate_btns
    gr.Column: corresponds to open_close_btn_col
    gr.Button: corresponds to open_close_btn
'''
def make_visible(schedule_name:str, password: str):
    skip_output = gr.Button(), gr.Column(), gr.Column(), gr.Row(), gr.Column(), gr.Button(), gr.Column
    
    if len(schedule_name) == 0: 
        gr.Warning('Please enter the form name.', ALERT_TIME)
        return skip_output
    if len(password) == 0: 
        gr.Warning('Please enter the password.', ALERT_TIME)
        return skip_output


    response = client.table('Forms').select('password', 'status').eq('form_name', standardize(schedule_name)).execute()
    data = response.data

    if len(data) > 0: 
        my_dict = data[0]
        if password != my_dict['password']: 
            gr.Warning(INCORRECT_PASSWORD, ALERT_TIME)
            return skip_output
        else: 
            if my_dict['status'] == 'open':
                gr.Info('', ALERT_TIME, title='Btw, the form is currently OPEN.')
                return gr.Button(variant='secondary'), gr.Column(visible=True), gr.Column(visible=True), gr.Row(visible=True), gr.Column(visible=True), gr.Button("Close Form", visible=True), gr.Column(visible=True)
            
            elif my_dict['status'] == 'closed':
                gr.Info('', ALERT_TIME, title='Btw, the form is currently CLOSED.')
                return gr.Button(variant='secondary'), gr.Column(visible=True), gr.Column(visible=True), gr.Row(visible=True),gr.Column(visible=True), gr.Button("Open Form", visible=True), gr.Column(visible=True)

    else: 
        gr.Warning(f"There is no form called \"{schedule_name}\". Please check the spelling and try again.", ALERT_TIME)
        return skip_output




'''
Gets a the form responses from Supabase and converts them to a DataFrame
Returns: 
    if found: a dictionary with two keys, capacity (int) and df (DataFrame)
    if not found: a string indicating the form was not found
'''
def get_df_from_db(schedule_name: str, password: str): 
    response = client.table('Forms').select('password', 'capacity', 'responses').eq('form_name', standardize(schedule_name)).execute()
    data = response.data

    if len(data) > 0: 
        my_dict = data[0]
        if password != my_dict['password']: 
            gr.Warning(INCORRECT_PASSWORD, ALERT_TIME)
            return FORM_NOT_FOUND
        
        # Convert to df
        df = pd.DataFrame(json.loads(my_dict['responses']))
        return {'capacity': my_dict['capacity'], 'df': df}

    else: 
        gr.Warning(f"There is no form called \"{schedule_name}\". Please check the spelling and try again.", ALERT_TIME)
        return FORM_NOT_FOUND
    

'''
Puts all of the form responses into a DataFrame. 
Returns this DF along with the filepath. 
'''
def get_all_responses(schedule_name:str, password:str): 
    res = get_df_from_db(schedule_name, password)

    if res == FORM_NOT_FOUND: 
        df = pd.DataFrame({
            NAME_COL: [], 
            EMAIL_COL: [],
            NUM_WORKSHOPS_COL: [], 
            AVAIL_COL: [],
            DESCRIP_COL: []
        })

    else:
        df = res['df']
        # Add spaces
        for col in [AVAIL_COL, DESCRIP_COL]:
            df[col] = [elem.replace(DELIMITER, f"{DELIMITER} ") for elem in df[col].to_list()]

    directory = os.path.abspath(os.getcwd())
    path = directory + "/all responses.csv" 
    df.to_csv(path, index=False)

    if len(df) == 0: 
        gr.Warning('', ALERT_TIME, title='No one has filled out the form yet.')
        return gr.DataFrame(df, visible=False), gr.File(path, visible=False)
    else: 
        return gr.DataFrame(df, visible=True), gr.File(path, visible=True)



'''
Calls private function to randomly generate 10 of the best schedules. 
Returns: DataFrame, filepath
'''
def random_schedules_wrapper(schedule_name: str, password: str):
    gr.Info('', ALERT_TIME, title='Working on generating schedules! Please do NOT click anything on this page.')
    res = generator(schedule_name, password, api_name='generate_random_schedules')
    df = res[1]['value']
    file = res[2]['value']
    if len(df['data'][0]) == 0: 
        gr.Warning('', ALERT_TIME, title='No one has filled out the form yet.')
        return gr.DataFrame(df, visible=False), gr.File(file, visible=False)
    else: 
        gr.Info('', ALERT_TIME, title=res[0]['value'])
        return gr.DataFrame(df, visible=True), gr.File(file, visible=True)



'''
Calls private function to generate all of the best schedules.
(The same as random_schedules_wrapper) with a different argument for num_results when calling generator. 
Gradio requires these to be two separate functions. 
Returns: DataFrame, filepath
'''
def all_schedules_wrapper(schedule_name: str, password: str): 
    gr.Info('', ALERT_TIME, title='Working on generating schedules! Please do NOT click anything on this page.')
    res = generator(schedule_name, password, api_name='generate_all_schedules')
    df = res[1]['value']
    file = res[2]['value']
    if len(df['data'][0]) == 0: 
        gr.Warning('', ALERT_TIME, title='No one has filled out the form yet.')
        return gr.DataFrame(df, visible=False), gr.File(file, visible=False)
    else: 
        gr.Info('', ALERT_TIME, title=res[0]['value'])
        return gr.DataFrame(df, visible=True), gr.File(file, visible=True)



'''
Opens/closes a form and changes the button after opening/closing the form.
Returns: gr.Button
'''
def toggle_btn(schedule_name:str, password:str): 
    response = client.table('Forms').select('password', 'capacity', 'status').eq('form_name', standardize(schedule_name)).execute()
    data = response.data

    if len(data) > 0:
        my_dict = data[0]
        if password != my_dict['password']: 
            gr.Warning(INCORRECT_PASSWORD, ALERT_TIME)
            return FORM_NOT_FOUND
        
        curr_status = my_dict['status']
        if curr_status == 'open': 
            client.table('Forms').update({'status': 'closed'}).eq('form_name', standardize(schedule_name)).execute()
            gr.Info('', ALERT_TIME, title="The form was closed successfully!")
            return gr.Button('Open Form')
        
        elif curr_status == 'closed': 
            client.table('Forms').update({'status': 'open'}).eq('form_name', standardize(schedule_name)).execute()
            gr.Info('', ALERT_TIME, title="The form was opened successfully!")
            return gr.Button('Close Form')
        
        else: 
            gr.Error('', ALERT_TIME, 'An unexpected error has ocurred.')
            return gr.Button()

    else: 
        gr.Warning('', ALERT_TIME, title=f"There was no form called \"{schedule_name}\". Please check the spelling and try again.") 
        return gr.Button()
    

'''
Delete a response
Returns: None 
'''
def my_delete(schedule_name: str, curr_juggler_name: str): 
    response = client.table('Forms').select('responses', 'slots').eq('form_name', standardize(schedule_name)).execute()
    data = response.data

    if len(data) > 0: 
        form = json.loads(data[0]['responses'])

        # Get current lists
        curr_juggler_name = curr_juggler_name.strip()
        names = form[NAME_COL]
        emails = form[EMAIL_COL]
        bandwidths = form[NUM_WORKSHOPS_COL]
        availabilities = form[AVAIL_COL]
        descriptions = form[DESCRIP_COL]

        # Get index of the juggler's name
        try: 
            index = names.index(curr_juggler_name)
        except: 
            return gr.Warning('', ALERT_TIME, title=f"\"{curr_juggler_name}\" is not in the form responses. Please check the spelling and try again.")
    
        # Remove juggler's responses from the lists 
        del names[index]
        del emails[index]
        del bandwidths[index]
        del availabilities[index]
        del descriptions[index]

        # Update Supabase
        my_obj = json.dumps({
            NAME_COL: names, 
            EMAIL_COL: emails, 
            NUM_WORKSHOPS_COL: bandwidths, 
            AVAIL_COL: availabilities, 
            DESCRIP_COL: descriptions
        })
        client.table('Forms').update({'responses': my_obj}).eq('form_name', standardize(schedule_name)).execute()
        return gr.Info('', ALERT_TIME, title='Response deleted successfully!')

    # I don't think it's possible to get here because I checked the schedule name earlier
    else:
        return gr.Warning('', ALERT_TIME, title=f"There was no form called \"{schedule_name}\". Please check the spelling and try again.")

    

### MARKDOWN TEXT ### 
generate_markdown = f"""
The app will attempt to create schedules where everyone is teaching their desired number of workshops AND all timeslots are filled.\n
If that is impossible, then the app will create schedules that maximize the number of timeslots that are filled.\n 
If someone can teach at every possible timeslot, then they willl NOT be in any of the schedules (this helps the code run faster).\n
You can either get a random selection of the best schedules (recommended), or ALL of the best schedules.\n 
WARNING: It can sometimes take a LONG time to get all the best schedules!
"""

about_markdown = f"""
# About the App\n
Hi! My name is Logan, and I created Schedule Buddy to be the one-stop-shop for making juggling workshop schedules.\n
Making a juggling workshop schedule involves 3 parts: making the form, having people fill it out, and putting the schedule together. Schedule Buddy supports all three of these aspects!\n

Schedule Buddy streamlines the process of the creating and filling out the forms, essentially replacing Google Forms. 
In terms of putting the schedule togther, Schedule Buddy will attempt to create schedules where everyone is teaching their desired number of workshops AND all timeslots are filled.
If that is impossible, then the app will create schedules that maximize the number of workshops that are taught. 
Essentially, Schedule Buddy removes the headache of trying to fit everyone into a timeslot. \n
For those who are curious and still reading, Schedule Buddy uses a recursive backtracking algorithm to make schedules.


# About Me
I've been juggling for the past 8 years, and 4 years ago I created a YouTube channel called Juggling Gym.\n
I love going to juggling festivals and attending workshops. When I was planning the workshops for the Atlanta Juggling Festival, I noticed how hard it was to plan the workshops
and make sure that everyone was teaching their desired number of workshops.\n
Since workshops are entirely run by volunteers, I wanted to make the process easier for everyone! Thus, I created Schedule Buddy as a free resource for jugglers to plan workshops.\n
"""


### GRADIO ###
with gr.Blocks() as demo:
    ### FILL OUT  FORM ###
    with gr.Tab('Fill Out Form'): 
        schedule_name = gr.Textbox(label="Form Name", info="What is the name of the form you want to fill out?")
        schedule_name_btn = gr.Button('Submit', variant='primary')
        
        with gr.Column(visible=False) as main_col:
            juggler_name = gr.Textbox(label='Name (first and last)', visible=True)
            email = gr.Textbox(label='Email Address', visible=True)
            num_workshops = gr.Number(label="Number of Workshops", info="Enter how many workshops you want to teach, e.g., \"1\", \"2\", etc.", interactive=True, visible=True)
              
        checkboxes = gr.CheckboxGroup([], label="Timeslots", info="Check the time(s) you can teach.", visible=False)

        # Let the user dynamically describe their workshops
        descriptions = gr.State([])
        new_description = gr.Textbox(label='Workshop Descriptions', info='Describe the workshop(s) you want to teach. Include the title, prereqs, and difficulty level for each workshop. Hit "Enter" after each one ("Return" if you\'re on mobile).', visible=False)

        def add_descrip(descriptions, new_description):
            return descriptions + [{"name": new_description}], ""

        new_description.submit(add_descrip, [descriptions, new_description], [descriptions, new_description])

        @gr.render(inputs=descriptions)
        def render_descriptions(descrip_list):
            for elem in descrip_list:        
                with gr.Row():
                        gr.Textbox(elem['name'], show_label=False, container=False)
                        delete_btn = gr.Button("Delete", scale=0, variant="stop")
                        def delete(elem=elem):
                            descrip_list.remove(elem)
                            return descrip_list
                        delete_btn.click(delete, None, [descriptions])

        
        submit_preferences_btn = gr.Button('Submit', variant='primary', visible=False) 
        schedule_name_btn.click(fn=get_timeslots, inputs=[schedule_name], outputs=[schedule_name_btn, checkboxes, main_col, submit_preferences_btn, new_description])
        submit_preferences_btn.click(fn=submit_preferences, inputs=[schedule_name, juggler_name, email, num_workshops, checkboxes, descriptions])


    ### MAKE FORM ###
    with gr.Tab('Make Form'):
        email =  gr.Textbox(label="Email Adress", type='email')
        schedule_name = gr.Textbox(label="Form Name", info='Keep it simple! Each person will have to type the form name to fill it out.')
        password_1 = gr.Textbox(label='Password', info='You MUST remember your password to access the schedule results. There is currently no way to reset your password.', type='password')
        password_2 = gr.Textbox(label='Password Again', info='Enter your password again', type='password')
        capacity = gr.Number(label="Capacity", info="Enter the maximum number of people who can teach per timeslot.")

        # Dynamically render timeslots
        # Based on: https://www.gradio.app/guides/dynamic-apps-with-render-decorator
        slots = gr.State([])
        new_slot = gr.Textbox(label='Enter Timeslots People Can Teach', info='Ex: Friday 7 pm, Saturday 11 am. Hit "Enter" after each one ("Return" if you\'re on mobile). Make sure to put them in CHRONOLOGICAL ORDER!')

        def add_slot(slots, new_slot_name):
            return slots + [{"name": new_slot_name}], ""

        new_slot.submit(add_slot, [slots, new_slot], [slots, new_slot])

        @gr.render(inputs=slots)
        def render_slots(slot_list):
            gr.Markdown(f"### Timeslots")
            for slot in slot_list:        
                with gr.Row():
                        gr.Textbox(slot['name'], show_label=False, container=False)
                        delete_btn = gr.Button("Delete", scale=0, variant="stop")
                        def delete(slot=slot):
                            slot_list.remove(slot)
                            return slot_list
                        delete_btn.click(delete, None, [slots])


        btn = gr.Button('Submit', variant='primary')
        btn.click(
            fn=make_form,
            inputs=[email, schedule_name, password_1, password_2, capacity, slots],
        )

    
    ### VIEW FORM RESULTS ### 
    with gr.Tab('View Form Results'):
        with gr.Column() as btn_group:
            schedule_name = gr.Textbox(label="Form Name")
            password = gr.Textbox(label="Password", type='password')
            find_form_btn = gr.Button('Find Form', variant='primary')

        # 1. Get all responses
        with gr.Column(visible=False) as all_responses_col: 
            gr.Markdown('# Download All Form Responses')
            gr.Markdown("Download everyone's responses to the form.")
            all_responses_btn = gr.Button('Download All Form Responses', variant='primary')

        with gr.Row() as all_responses_output_row:
            df_out = gr.DataFrame(row_count = (NUM_ROWS, "dynamic"),col_count = (NUM_COLS_ALL_RESPONSES, "dynamic"),headers=[NAME_COL, NUM_WORKSHOPS_COL, AVAIL_COL, DESCRIP_COL],wrap=True,scale=4,visible=False)
            file_out = gr.File(label = "Downloadable file", scale=1, visible=False)

        all_responses_btn.click(fn=get_all_responses, inputs=[schedule_name, password], outputs=[df_out, file_out])

        
        # 2. Generate schedules
        with gr.Column(visible=False) as generate_schedules_explanation_col:
            gr.Markdown('# Create Schedules based on Everyone\'s Preferences.') 
            with gr.Accordion('Details'):
                gr.Markdown(generate_markdown)
 
        with gr.Row(visible=False) as generate_btns_row:
            generate_ten_results_btn = gr.Button('Generate a Subset of Schedules', variant='primary', visible=True) 
            generate_all_results_btn = gr.Button('Generate All Possible Schedules', visible=True)
        
        with gr.Row(visible=True) as generated_schedules_output:
            generated_df_out = gr.DataFrame(row_count = (NUM_ROWS, "dynamic"),col_count = (NUM_COLS_SCHEDULES, "dynamic"),headers=["Schedule", "Instructors"],wrap=True,scale=3, visible=False)
            generated_file_out = gr.File(label = "Downloadable schedule file", scale=1, visible=False)

        generate_ten_results_btn.click(fn=random_schedules_wrapper, inputs=[schedule_name, password], outputs=[generated_df_out, generated_file_out])
        generate_all_results_btn.click(fn=all_schedules_wrapper, inputs=[schedule_name, password], outputs=[generated_df_out, generated_file_out], api_name='generate_all_schedules')


        # 3. Open/close button 
        with gr.Column(visible=False) as open_close_btn_col: 
            gr.Markdown('# Open or Close Form')
            open_close_btn = gr.Button(variant='primary')
            open_close_btn.click(fn=toggle_btn, inputs=[schedule_name, password], outputs=[open_close_btn])

        
        # 4. Delete response
        with gr.Column(visible=False) as delete_btn_col: 
            gr.Markdown('# Delete a Response')
            gr.Markdown('Pressing this button will delete the juggler\'s response entirely. Warning: this CANNOT be undone!!')
            juggler_name = gr.Textbox(label="Juggler's Name: Enter the name of the juggler's response that you want to delete")
            delete_btn = gr.Button('Delete Response')
            delete_btn.click(fn=my_delete, inputs=[schedule_name, juggler_name])


        find_form_btn.click(fn=make_visible, inputs=[schedule_name, password], outputs=[find_form_btn, all_responses_col, generate_schedules_explanation_col, generate_btns_row, open_close_btn_col, open_close_btn, delete_btn_col])


    ### INFO ### 
    with gr.Tab('About'): 
        gr.Markdown(about_markdown)

directory = os.path.abspath(os.getcwd())
allowed = directory #+ "/schedules"
demo.launch(allowed_paths=[allowed])