Juggling commited on
Commit
8989bd9
·
verified ·
1 Parent(s): dd0aa34

More formatting changes

Browse files
Files changed (1) hide show
  1. workshops.py +978 -0
workshops.py ADDED
@@ -0,0 +1,978 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import copy
3
+ import os
4
+ import gradio as gr
5
+ from collections import Counter
6
+ import random
7
+ import re
8
+ from datetime import date
9
+ import supabase
10
+ import json
11
+
12
+ ###### OG FUNCTIONS TO GENERATE SCHEDULES ######
13
+ # CONSTANTS
14
+ NAME_COL = 'Juggler_Name'
15
+ NUM_WORKSHOPS_COL = 'Num_Workshops'
16
+ AVAIL_COL = 'Availability'
17
+ DESCRIP_COL = 'Workshop_Descriptions'
18
+ DELIMITER = ';'
19
+
20
+ class Schedule:
21
+ def __init__(self, timeslots: dict):
22
+ self.num_timeslots_filled = 0
23
+ self.total_num_workshops = 0
24
+
25
+ for time,instructors in timeslots.items():
26
+ curr_len = len(instructors)
27
+ if curr_len > 0:
28
+ self.num_timeslots_filled += 1
29
+ self.total_num_workshops += curr_len
30
+
31
+ self.timeslots = timeslots
32
+
33
+ def add(self, person: str, time: str):
34
+ self.total_num_workshops += 1
35
+ if len(self.timeslots[time]) == 0:
36
+ self.num_timeslots_filled += 1
37
+ self.timeslots[time].append(person)
38
+
39
+ def remove(self, person: str, time: str):
40
+ self.total_num_workshops -= 1
41
+ if len(self.timeslots[time]) == 1:
42
+ self.num_timeslots_filled -= 1
43
+ self.timeslots[time].remove(person)
44
+
45
+
46
+ # Returns True if the person can teach during the slot, and False otherwise
47
+ def can_teach(person: str, slot: list, capacity: int) -> bool:
48
+ if len(slot) == capacity or len(slot) > capacity:
49
+ return False
50
+
51
+ # No one can teach two workshops at once
52
+ if person in slot:
53
+ return False
54
+
55
+ return True
56
+
57
+
58
+ # Extracts relevant information from the df with availability and puts it into a useable format
59
+ def convert_df(df):
60
+ people = []
61
+ # Key: person's name
62
+ # Value: a list of their availability
63
+ availability = {}
64
+ seen = set()
65
+ for row in range(len(df)):
66
+ # TODO: make sure no people with the same name fill out the form
67
+ name = df.loc[row, NAME_COL]
68
+
69
+ number = df.loc[row, NUM_WORKSHOPS_COL]
70
+ if number == 1:
71
+ people.append(name)
72
+
73
+ # Add people who are teaching multiple workshops to the list more than once
74
+ else:
75
+ for i in range(number):
76
+ people.append(name)
77
+
78
+ curr_avail = df.loc[row, AVAIL_COL]
79
+ curr_avail = curr_avail.split(DELIMITER)
80
+ curr_avail = [elem.strip() for elem in curr_avail]
81
+ availability[name] = curr_avail
82
+
83
+ return people, availability
84
+
85
+
86
+ # Returns False if curr is NaN, and True otherwise
87
+ def is_defined(curr):
88
+ # if curr != curr, then curr is NaN for some reason
89
+ if curr != curr:
90
+ return False
91
+ else:
92
+ return True
93
+
94
+ # Returns True if curr is defined and its length is greater than 0
95
+ def is_valid(curr):
96
+ return (is_defined(curr) and len(curr) > 0)
97
+
98
+ # Makes a dictionary where each key is a timeslot and each value is a list.
99
+ # If there's no partial schedule, each list will be empty.
100
+ # If there's a partial schedule, each list will include the people teaching during that slot.
101
+ def initialize_timeslots(df) -> dict:
102
+ all_timeslots = set()
103
+ availability = df[AVAIL_COL]
104
+ for elem in availability:
105
+ curr_list = elem.split(DELIMITER)
106
+ for inner in curr_list:
107
+ all_timeslots.add(inner.strip())
108
+
109
+ to_return = {}
110
+ for slot in all_timeslots:
111
+ to_return[slot] = []
112
+
113
+ return to_return
114
+
115
+
116
+ # Recursive function that generates all possible schedules
117
+ def find_all_schedules(people: list, availability: dict, schedule_obj: Schedule, capacity: int, schedules: list, max_list: list) -> None:
118
+ if schedule_obj.num_timeslots_filled > max_list[0] or schedule_obj.num_timeslots_filled == max_list[0]:
119
+ schedules.append(copy.deepcopy(schedule_obj))
120
+ max_list[0] = schedule_obj.num_timeslots_filled
121
+
122
+ # Base case
123
+ if len(people) == 0:
124
+ return
125
+
126
+
127
+ # Recursive cases
128
+ person = people[0]
129
+
130
+ for time in availability[person]:
131
+ if can_teach(person, schedule_obj.timeslots[time], capacity):
132
+ # Choose (put that person in that timeslot)
133
+ schedule_obj.add(person, time)
134
+
135
+ # Explore (assign everyone else to timeslots based on that decision)
136
+ if len(people) == 1:
137
+ find_all_schedules([], availability, schedule_obj, capacity, schedules, max_list)
138
+
139
+ else:
140
+ find_all_schedules(people[1:len(people)], availability, schedule_obj, capacity, schedules, max_list)
141
+
142
+ # Unchoose (remove that person from the timeslot)
143
+ schedule_obj.remove(person, time)
144
+ # NOTE: this will not generate a full timeslot, but could still lead to a good schedule
145
+ else:
146
+ if len(people) == 1:
147
+ find_all_schedules([], availability, schedule_obj, capacity, schedules, max_list)
148
+ else:
149
+ find_all_schedules(people[1:len(people)], availability, schedule_obj, capacity, schedules, max_list)
150
+
151
+
152
+ return
153
+
154
+
155
+ # Makes an organized DataFrame given a list of schedules
156
+ def make_df(schedules: list, descrip_dict: dict):
157
+ all_times = []
158
+ all_instructors = []
159
+ seen = []
160
+
161
+ count = 1
162
+
163
+ for i in range (len(schedules)):
164
+ curr_sched = schedules[i]
165
+
166
+ if curr_sched in seen:
167
+ continue
168
+ else:
169
+ seen.append(curr_sched)
170
+
171
+ # Sort dictionary by keys
172
+ sorted_dict = dict(sorted(curr_sched.items(), key=lambda item: item[0]))
173
+ curr_times = sorted_dict.keys()
174
+ curr_instructors = sorted_dict.values()
175
+
176
+ # Include an empty row between schedules
177
+ if count != 1:
178
+ all_times.append("")
179
+ all_instructors.append("")
180
+
181
+ if len(schedules) > 1:
182
+ all_times.append(f"Schedule #{count}")
183
+ all_instructors.append("")
184
+ count += 1
185
+
186
+ for slot in curr_times:
187
+ all_times.append(slot)
188
+
189
+ for instructors in curr_instructors:
190
+ if len(descrip_dict) == 0:
191
+ all_instructors.append("; ". join(instructors))
192
+
193
+ if len(descrip_dict) > 0:
194
+ big_str = ""
195
+
196
+ for person in instructors:
197
+ if person in descrip_dict:
198
+ descrip = descrip_dict[person]
199
+ else:
200
+ descrip = "Workshop"
201
+
202
+ new_str = ''
203
+ separated = descrip.split(DELIMITER)
204
+ for elem in separated:
205
+ new_str += f"- {elem.strip()} OR\n"
206
+ big_str += f"\n\n- {person}:\n" + new_str.strip(' OR\n')
207
+
208
+ all_instructors.append(big_str.strip())
209
+
210
+
211
+ new_df = pd.DataFrame({
212
+ "Schedule": all_times,
213
+ "Instructor(s)": all_instructors
214
+ })
215
+ new_df['Instructor(s)'] = new_df['Instructor(s)'].astype(str)
216
+
217
+ return new_df, count - 1
218
+
219
+
220
+
221
+
222
+
223
+ # Makes a dictionary where each key is the instructor's name and
224
+ # the value is the workshop(s) they're teaching
225
+ def get_description_dict(df):
226
+ new_dict = {}
227
+ for row in range(len(df)):
228
+ name = df.loc[row, NAME_COL]
229
+ new_dict[name] = df.loc[row, DESCRIP_COL]
230
+ return new_dict
231
+
232
+
233
+ # Classifies schedules into two categories: complete and incomplete:
234
+ # Complete = everyone is teaching desired number of timeslots and each timeslot is filled
235
+ # NOTE: I'm using "valid" instead of "complete" as a variable name so that I don't mix it up
236
+ # Incomplete = not complete
237
+ def classify_schedules(people: list, schedules: list, partial_names: list, total_timeslots: int, max_timeslots_filled: int) -> tuple:
238
+ valid_schedules = []
239
+
240
+ # Key: score
241
+ # Value: schedules with that score
242
+ incomplete_schedules = {}
243
+
244
+ # Get frequency of items in the list
245
+ # Key: person
246
+ # Value: number of workshops they WANT to teach
247
+ pref_dict = Counter(people)
248
+
249
+ pref_dict.update(Counter(partial_names))
250
+
251
+ all_names = pref_dict.keys()
252
+
253
+ # Evaluate each schedule
254
+ overall_max = 0
255
+ for sched in schedules:
256
+ if sched.num_timeslots_filled != max_timeslots_filled:
257
+ continue
258
+ # Key: person
259
+ # Value: how many workshops they're ACTUALLY teaching in this schedule
260
+ freq_dict = {}
261
+ for name in all_names:
262
+ freq_dict[name] = 0
263
+
264
+ for timeslot, instructor_list in sched.timeslots.items():
265
+ for instructor in instructor_list:
266
+ if instructor in freq_dict:
267
+ freq_dict[instructor] += 1
268
+ else:
269
+ print("there is a serious issue!!!!")
270
+
271
+ # See if everyone is teaching their desired number of workshops
272
+ everyone_is_teaching = True
273
+ for teacher, freq in freq_dict.items():
274
+ if freq != pref_dict[teacher]:
275
+ #print(f"teacher: {teacher}. preference: {pref_dict[teacher]}. actual frequency: {freq}")
276
+ everyone_is_teaching = False
277
+ break
278
+
279
+ filled_all_timeslots = (sched.num_timeslots_filled == total_timeslots)
280
+ if everyone_is_teaching and filled_all_timeslots:
281
+ valid_schedules.append(sched)
282
+ else:
283
+ # No need to add to incomplete_schedules if there's at least one valid schedule
284
+ if len(valid_schedules) > 0:
285
+ continue
286
+ #print(f"teaching desired number of timeslots: {everyone_is_teaching}. At least one workshop per slot: {filled_all_timeslots}.\n{sched}\n")
287
+ if sched.num_timeslots_filled > overall_max:
288
+ overall_max = sched.num_timeslots_filled
289
+
290
+ if sched.num_timeslots_filled not in incomplete_schedules:
291
+ incomplete_schedules[sched.num_timeslots_filled] = []
292
+ incomplete_schedules[sched.num_timeslots_filled].append(sched)
293
+
294
+
295
+
296
+ if len(valid_schedules) > 0:
297
+ return valid_schedules, []
298
+ else:
299
+ return [], incomplete_schedules[overall_max]
300
+
301
+
302
+
303
+ # Parameters: schedules that have the max number of timeslots filled
304
+ # Returns: a list of all schedules that have the max number of workshops
305
+ # To make it less overwhelming, it will return {cutoff} randomly
306
+ def get_best_schedules(schedules: list, cutoff: str) -> list:
307
+ cutoff = int(cutoff)
308
+ overall_max = 0
309
+ best_schedules = {}
310
+ for sched in schedules:
311
+ if sched.total_num_workshops not in best_schedules:
312
+ best_schedules[sched.total_num_workshops] = []
313
+ best_schedules[sched.total_num_workshops].append(sched.timeslots)
314
+ if sched.total_num_workshops > overall_max:
315
+ overall_max = sched.total_num_workshops
316
+ all_best_schedules = best_schedules[overall_max]
317
+ if cutoff == -1:
318
+ return all_best_schedules
319
+ else:
320
+ if len(all_best_schedules) > cutoff:
321
+ # Sample without replacement
322
+ return random.sample(all_best_schedules, cutoff)
323
+ else:
324
+ return all_best_schedules
325
+
326
+
327
+ # Big wrapper function that calls the other functions
328
+ def main(df, capacity:int, num_results: int):
329
+ descrip_dict = get_description_dict(df)
330
+
331
+ # Convert the df with everyone's availability to a usable format
332
+ res = convert_df(df)
333
+ people = res[0]
334
+ availability = res[1]
335
+
336
+ partial_names = []
337
+
338
+ timeslots = initialize_timeslots(df)
339
+
340
+ schedules = []
341
+ schedule_obj = Schedule(timeslots)
342
+ max_list = [0]
343
+
344
+ find_all_schedules(people, availability, schedule_obj, capacity, schedules, max_list)
345
+
346
+ total_timeslots = len(timeslots)
347
+
348
+
349
+ res = classify_schedules(people, schedules, partial_names, total_timeslots, max_list[0])
350
+ valid_schedules = res[0]
351
+ decent_schedules = res[1]
352
+
353
+
354
+ # Return schedules
355
+ if len(valid_schedules) > 0:
356
+ best_schedules = get_best_schedules(valid_schedules, num_results)
357
+ res = make_df(best_schedules, descrip_dict)
358
+ new_df = res[0]
359
+ count = res[1]
360
+ if count == 1:
361
+ results = "Good news! I was able to make a schedule."
362
+ else:
363
+ results = "Good news! I was able to make multiple schedules."
364
+
365
+ else:
366
+ best_schedules = get_best_schedules(decent_schedules, num_results)
367
+ res = make_df(best_schedules, descrip_dict)
368
+ new_df = res[0]
369
+ count = res[1]
370
+ beginning = "Unfortunately, I wasn't able to make a complete schedule, but here"
371
+ if count == 1:
372
+ results = f"{beginning} is the best option."
373
+ else:
374
+ results = f"{beginning} are the best options."
375
+
376
+
377
+ directory = os.path.abspath(os.getcwd())
378
+ path = directory + "/schedule.csv"
379
+ new_df.to_csv(path, index=False)
380
+ return results, new_df, path
381
+
382
+
383
+
384
+
385
+
386
+ ##### ALL THE NEW STUFF WITH SUPABASE ETC. #####
387
+ ### CONSTANTS ###
388
+ SIGN_IN_SUCCESS = 'Sign-In Successful!'
389
+ NAME_COL = 'Juggler_Name'
390
+ NUM_WORKSHOPS_COL = 'Num_Workshops'
391
+ AVAIL_COL = 'Availability'
392
+ DESCRIP_COL = 'Workshop_Descriptions'
393
+ EMAIL_COL = 'Email'
394
+ DELIMITER = ';'
395
+ ALERT_TIME = None # leave warnings on screen indefinitely
396
+ FORM_NOT_FOUND = 'Form not found'
397
+ INCORRECT_PASSWORD = "The password is incorrect. Please check the password and try again. If you don't remember your password, please email [email protected]."
398
+ NUM_ROWS = 1
399
+ NUM_COLS_SCHEDULES = 2
400
+ NUM_COLS_ALL_RESPONSES = 4
401
+ MIN_LENGTH = 6
402
+ NUM_RESULTS = 10 # randomly get {NUM_RESULTS} results
403
+
404
+
405
+ theme = gr.themes.Soft(
406
+ primary_hue="cyan",
407
+ secondary_hue="pink",
408
+ font=[gr.themes.GoogleFont('sans-serif'), 'ui-sans-serif', 'system-ui', 'Montserrat'],
409
+ )
410
+
411
+ ### Connect to Supabase ###
412
+ URL = os.environ['URL']
413
+ API_KEY = os.environ['API_KEY']
414
+ client = supabase.create_client(URL, API_KEY)
415
+
416
+
417
+
418
+
419
+ ### DEFINE FUNCTIONS ###
420
+ ## Multi-purpose function ##
421
+ '''
422
+ Returns a lowercased and stripped version of the schedule name.
423
+ Returns: str
424
+ '''
425
+ def standardize(schedule_name: str):
426
+ return schedule_name.lower().strip()
427
+
428
+
429
+ ## Function to make a form ##
430
+ '''
431
+ Makes a form and pushes it to Supabase.
432
+ Returns: None
433
+ '''
434
+ def make_form(email: str, schedule_name: str, password_1: str, password_2: str, capacity: int, slots: list) -> str:
435
+ # Error handling
436
+ if len(email) == 0:
437
+ return gr.Warning('', ALERT_TIME, title="Please enter an email address")
438
+
439
+ if len(schedule_name) == 0:
440
+ return gr.Warning('', ALERT_TIME, title=f"Please enter the form name.")
441
+
442
+ if password_1 != password_2:
443
+ return gr.Warning('', ALERT_TIME, title=f"The passwords don't match. Password 1 is \"{password_1}\" and Password 2 is \"{password_2}\".")
444
+
445
+ if len(password_1) < MIN_LENGTH:
446
+ return gr.Warning('', ALERT_TIME, title=f"Please make a password that is at least {MIN_LENGTH} characters.")
447
+
448
+ if capacity == 0:
449
+ return gr.Warning('', ALERT_TIME, title=f"Please enter the capacity (how many people can teach per timeslot). It must be greater than zero.")
450
+
451
+ if capacity < 0:
452
+ return gr.Warning('', ALERT_TIME, title=f"The capacity (number of people who can teach per timeslot) must be greater than zero.")
453
+
454
+ if len(slots) == 0:
455
+ return gr.Warning('', ALERT_TIME, title="Please enter at least one timeslot. Make sure to press \"Enter\" after each one!")
456
+
457
+
458
+ # Check if schedule name already exists
459
+ existing_forms = []
460
+ response = client.table('Forms').select('form_name').execute()
461
+ for elem in response.data:
462
+ existing_forms.append(elem['form_name'])
463
+
464
+ if schedule_name in existing_forms:
465
+ return gr.Warning('', ALERT_TIME, title=f"The form name \"{schedule_name}\" already exists. Please choose a different name.")
466
+
467
+
468
+ # Push to Supabase
469
+ new_slots = [elem['name'] for elem in slots]
470
+
471
+ my_obj = {
472
+ 'form_name': standardize(schedule_name),
473
+ 'password': password_1,
474
+ 'email': email,
475
+ 'capacity': capacity,
476
+ 'slots': new_slots,
477
+ 'status': 'open',
478
+ 'date_created': str(date.today()),
479
+ 'responses': json.dumps({
480
+ NAME_COL: [],
481
+ EMAIL_COL: [],
482
+ NUM_WORKSHOPS_COL: [],
483
+ AVAIL_COL: [],
484
+ DESCRIP_COL: [],
485
+ }),
486
+ }
487
+
488
+ client.table('Forms').insert(my_obj).execute()
489
+ gr.Info('', ALERT_TIME, title="Form made successfully!")
490
+
491
+
492
+
493
+ ## Functions to fill out a form @@
494
+ '''
495
+ Gets the timeslots for a given schedule and makes form elements visible.
496
+ Returns:
497
+ gr.Button: corresponds to schedule_name_btn
498
+ gr.CheckboxGroup: corresponds to checkboxes
499
+ gr.Column: corresponds to main_col
500
+ gr.Button: corresponds to submit_preferences_btn
501
+ gr.Textbox: corresponds to new_description
502
+ '''
503
+ def get_timeslots(schedule_name: str):
504
+ # Leave everything as it was
505
+ skip_output = gr.Button(), gr.CheckboxGroup(),gr.Column(), gr.Button(), gr.Textbox()
506
+
507
+ if len(schedule_name) == 0:
508
+ gr.Warning('', ALERT_TIME, title='Please type a form name.')
509
+ return skip_output
510
+
511
+ response = client.table('Forms').select('status', 'slots').eq('form_name', standardize(schedule_name)).execute()
512
+ data = response.data
513
+
514
+ if len(data) > 0:
515
+ my_dict = data[0]
516
+ if my_dict['status'] == 'closed':
517
+ gr.Warning('', ALERT_TIME, title="This form is closed. Please contact the form administrator.")
518
+ return skip_output
519
+ else:
520
+ 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)
521
+ else:
522
+ gr.Warning('', ALERT_TIME, title=f"There was no form called \"{schedule_name}\". Please check the spelling and try again.")
523
+ return skip_output
524
+
525
+
526
+ '''
527
+ Submits the form that the person filled out to Supabase.
528
+ Returns: None
529
+ '''
530
+ def submit_preferences(schedule_name: str, curr_juggler_name: str, curr_email: str, curr_num_workshops: int, curr_availability: str, curr_descriptions: list):
531
+ # Error handling
532
+ if len(curr_juggler_name) == 0:
533
+ return gr.Warning('', ALERT_TIME, title="Please enter your name.")
534
+
535
+ if len(curr_email) == 0:
536
+ return gr.Warning('', ALERT_TIME, title="Please enter your email address.")
537
+
538
+ if curr_num_workshops == 0:
539
+ return gr.Warning('', ALERT_TIME, title=f"Please enter how many workshops you want to teach.")
540
+
541
+ elif curr_num_workshops < 0:
542
+ return gr.Warning('', ALERT_TIME, title="The number of workshops you want to teach must be positive.")
543
+
544
+ if len(curr_availability) == 0:
545
+ return gr.Warning('', ALERT_TIME, title="Please select at least one timeslot when you are able to teach.")
546
+
547
+ if curr_num_workshops > len(curr_availability):
548
+ 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.")
549
+
550
+ if len(curr_descriptions) == 0:
551
+ 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!")
552
+
553
+ response = client.table('Forms').select('responses').eq('form_name', standardize(schedule_name)).execute()
554
+ data = response.data
555
+
556
+ if len(data) > 0:
557
+ form = json.loads(data[0]['responses'])
558
+
559
+ # Add current preferences to dictionary lists
560
+ curr_juggler_name = curr_juggler_name.strip()
561
+ names = form[NAME_COL]
562
+ if curr_juggler_name in names:
563
+ 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.")
564
+ names.extend([curr_juggler_name])
565
+
566
+ emails = form[EMAIL_COL]
567
+ emails.extend([curr_email])
568
+
569
+ bandwidths = form[NUM_WORKSHOPS_COL]
570
+ bandwidths.extend([curr_num_workshops])
571
+
572
+ availabilities = form[AVAIL_COL]
573
+ curr_availability = f"{DELIMITER}".join(curr_availability)
574
+ availabilities.extend([curr_availability])
575
+
576
+ curr_descriptions = [elem['name'] for elem in curr_descriptions]
577
+ curr_descriptions = f"{DELIMITER}".join(curr_descriptions)
578
+ descriptions = form[DESCRIP_COL]
579
+ descriptions.extend([curr_descriptions])
580
+
581
+ # Update Supabase
582
+ my_obj = json.dumps({
583
+ NAME_COL: names,
584
+ EMAIL_COL: emails,
585
+ NUM_WORKSHOPS_COL: bandwidths,
586
+ AVAIL_COL: availabilities,
587
+ DESCRIP_COL: descriptions
588
+ })
589
+ client.table('Forms').update({'responses': my_obj}).eq('form_name', standardize(schedule_name)).execute()
590
+ return gr.Info('', ALERT_TIME, title='Form submitted successfully!')
591
+
592
+ # I don't think it's possible to get here because I checked the schedule name earlier
593
+ else:
594
+ return gr.Warning('', ALERT_TIME, title=f"There was no form called \"{schedule_name}\". Please check the spelling and try again.")
595
+
596
+
597
+ ## Functions to manage/generate schedules ##
598
+ '''
599
+ Uses the name and password to get the form.
600
+ Makes the buttons and other elements visible on the page.
601
+ Returns:
602
+ gr.Button: corresponds to find_form_btn
603
+ gr.Column: corresponds to all_responses_group
604
+ gr.Column: generate_schedules_explanation
605
+ gr.Row: corresponds to generate_btns
606
+ gr.Column: corresponds to open_close_btn_col
607
+ gr.Button: corresponds to open_close_btn
608
+ '''
609
+ def make_visible(schedule_name:str, password: str):
610
+ skip_output = gr.Button(), gr.Column(), gr.Column(), gr.Row(), gr.Column(), gr.Button()
611
+
612
+ if len(schedule_name) == 0:
613
+ gr.Warning('Please enter the form name.', ALERT_TIME)
614
+ return skip_output
615
+ if len(password) == 0:
616
+ gr.Warning('Please enter the password.', ALERT_TIME)
617
+ return skip_output
618
+
619
+
620
+ response = client.table('Forms').select('password', 'status').eq('form_name', standardize(schedule_name)).execute()
621
+ data = response.data
622
+
623
+ if len(data) > 0:
624
+ my_dict = data[0]
625
+ if password != my_dict['password']:
626
+ gr.Warning(INCORRECT_PASSWORD, ALERT_TIME)
627
+ return skip_output
628
+ else:
629
+ if my_dict['status'] == 'open':
630
+ gr.Info('', ALERT_TIME, title='Btw, the form is currently OPEN.')
631
+ 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)
632
+
633
+ elif my_dict['status'] == 'closed':
634
+ gr.Info('', ALERT_TIME, title='Btw, the form is currently CLOSED.')
635
+ 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)
636
+
637
+ else:
638
+ gr.Warning(f"There is no form called \"{schedule_name}\". Please check the spelling and try again.", ALERT_TIME)
639
+ return skip_output
640
+
641
+
642
+
643
+
644
+ '''
645
+ Makes a blank schedule that we can return to prevent things from breaking.
646
+ Returns: tuple with 3 elements:
647
+ 0: str indicating that the form wasn't found
648
+ 1: the DataFrame
649
+ 2: the path to the DataFrame
650
+ '''
651
+ def make_blank_schedule():
652
+ df = pd.DataFrame({
653
+ 'Schedule': [],
654
+ 'Instructors': []
655
+ })
656
+
657
+ directory = os.path.abspath(os.getcwd())
658
+ path = directory + "/schedule.csv"
659
+ df.to_csv(path, index=False)
660
+ return FORM_NOT_FOUND, df, path
661
+
662
+
663
+ '''
664
+ Gets a the form responses from Supabase and converts them to a DataFrame
665
+ Returns:
666
+ if found: a dictionary with two keys, capacity (int) and df (DataFrame)
667
+ if not found: a string indicating the form was not found
668
+ '''
669
+ def get_df_from_db(schedule_name: str, password: str):
670
+ response = client.table('Forms').select('password', 'capacity', 'responses').eq('form_name', standardize(schedule_name)).execute()
671
+ data = response.data
672
+
673
+ if len(data) > 0:
674
+ my_dict = data[0]
675
+ if password != my_dict['password']:
676
+ gr.Warning(INCORRECT_PASSWORD, ALERT_TIME)
677
+ return FORM_NOT_FOUND
678
+
679
+ # Convert to df
680
+ df = pd.DataFrame(json.loads(my_dict['responses']))
681
+ return {'capacity': my_dict['capacity'], 'df': df}
682
+
683
+ else:
684
+ gr.Warning(f"There is no form called \"{schedule_name}\". Please check the spelling and try again.", ALERT_TIME)
685
+ return FORM_NOT_FOUND
686
+
687
+
688
+ '''
689
+ Puts all of the form responses into a DataFrame.
690
+ Returns this DF along with the filepath.
691
+ '''
692
+ def get_all_responses(schedule_name:str, password:str):
693
+ res = get_df_from_db(schedule_name, password)
694
+
695
+ if res == FORM_NOT_FOUND:
696
+ df = pd.DataFrame({
697
+ NAME_COL: [],
698
+ EMAIL_COL: [],
699
+ NUM_WORKSHOPS_COL: [],
700
+ AVAIL_COL: [],
701
+ DESCRIP_COL: []
702
+ })
703
+
704
+ else:
705
+ df = res['df']
706
+ # Add commas
707
+ for col in [AVAIL_COL, DESCRIP_COL]:
708
+ df[col] = [elem.replace(DELIMITER, f"{DELIMITER} ") for elem in df[col].to_list()]
709
+
710
+ directory = os.path.abspath(os.getcwd())
711
+ path = directory + "/all responses.csv"
712
+ df.to_csv(path, index=False)
713
+
714
+ if len(df) == 0:
715
+ gr.Warning('', ALERT_TIME, title='No one has filled out the form yet.')
716
+ return gr.DataFrame(df, visible=True), gr.File(path, visible=True)
717
+
718
+
719
+ '''
720
+ Calls the algorithm to generate the best possible schedules,
721
+ and returns a random subset of the results.
722
+ (The same as generate_schedules_wrapper_all_results, except that this function only returns a subset of them.
723
+ I had to make it into two separate functions in order to work with Gradio).
724
+ Returns:
725
+ DataFrame
726
+ Filepath to DF (str)
727
+ '''
728
+ def generate_schedules_wrapper_subset_results(schedule_name: str, password: str):
729
+ res = get_df_from_db(schedule_name, password)
730
+ # Return blank schedule (should be impossible to get to this condition btw)
731
+ if res == FORM_NOT_FOUND:
732
+ to_return = make_blank_schedule()
733
+ gr.Warning(FORM_NOT_FOUND, ALERT_TIME)
734
+
735
+ else:
736
+ df = res['df']
737
+ if len(df) == 0:
738
+ gr.Warning('', ALERT_TIME, title='No one has filled out the form yet.')
739
+ to_return = make_blank_schedule()
740
+ else:
741
+ gr.Info('', ALERT_TIME, title='Working on generating schedules! Please DO NOT click anything on this page.')
742
+ capacity = res['capacity']
743
+ to_return = main(df, capacity, NUM_RESULTS)
744
+ gr.Info('', ALERT_TIME, title=to_return[0])
745
+
746
+
747
+ return gr.Textbox(to_return[0]), gr.DataFrame(to_return[1], visible=True), gr.File(to_return[2], visible=True)
748
+
749
+
750
+ '''
751
+ Calls the algorithm to generate the best possible schedules,
752
+ and returns ALL of the results.
753
+ (The same as generate_schedules_wrapper_subset_results, except that this function returns all of them.
754
+ I had to make it into two separate functions in order to work with Gradio).
755
+ Returns:
756
+ DataFrame
757
+ Filepath to DF (str)
758
+ '''
759
+ def generate_schedules_wrapper_all_results(schedule_name: str, password: str):
760
+ res = get_df_from_db(schedule_name, password)
761
+ # Return blank schedule (should be impossible to get to this condition btw)
762
+ if res == FORM_NOT_FOUND:
763
+ to_return = make_blank_schedule()
764
+ gr.Warning(FORM_NOT_FOUND, ALERT_TIME)
765
+
766
+ else:
767
+ df = res['df']
768
+ if len(df) == 0:
769
+ gr.Warning('', ALERT_TIME, title='No one has filled out the form yet.')
770
+ to_return = make_blank_schedule()
771
+ else:
772
+ gr.Info('', ALERT_TIME, title='Working on generating schedules! Please DO NOT click anything on this page.')
773
+ capacity = res['capacity']
774
+ placeholder = -1
775
+ to_return = main(df, capacity, placeholder)
776
+ gr.Info('', ALERT_TIME, title=to_return[0])
777
+
778
+ return gr.Textbox(to_return[0]), gr.DataFrame(to_return[1], visible=True), gr.File(to_return[2], visible=True)
779
+
780
+
781
+
782
+
783
+ '''
784
+ Opens/closes a form and changes the button after opening/closing the form.
785
+ Returns: gr.Button
786
+ '''
787
+ def toggle_btn(schedule_name:str, password:str):
788
+ response = client.table('Forms').select('password', 'capacity', 'status').eq('form_name', standardize(schedule_name)).execute()
789
+ data = response.data
790
+
791
+ if len(data) > 0:
792
+ my_dict = data[0]
793
+ if password != my_dict['password']:
794
+ gr.Warning(INCORRECT_PASSWORD, ALERT_TIME)
795
+ return FORM_NOT_FOUND
796
+
797
+ curr_status = my_dict['status']
798
+ if curr_status == 'open':
799
+ client.table('Forms').update({'status': 'closed'}).eq('form_name', standardize(schedule_name)).execute()
800
+ gr.Info('', ALERT_TIME, title="The form was closed successfully!")
801
+ return gr.Button('Open Form')
802
+
803
+ elif curr_status == 'closed':
804
+ client.table('Forms').update({'status': 'open'}).eq('form_name', standardize(schedule_name)).execute()
805
+ gr.Info('', ALERT_TIME, title="The form was opened successfully!")
806
+ return gr.Button('Close Form')
807
+
808
+ else:
809
+ gr.Error('', ALERT_TIME, 'An unexpected error has ocurred.')
810
+ return gr.Button()
811
+
812
+ else:
813
+ gr.Warning('', ALERT_TIME, title=f"There was no form called \"{schedule_name}\". Please check the spelling and try again.")
814
+ return gr.Button()
815
+
816
+
817
+ ### MARKDOWN TEXT ###
818
+ generate_markdown = f"""
819
+ The app will attempt to create schedules where everyone is teaching their descired number of workshops AND all timeslots are filled.\n
820
+ If that is impossible, then the app will create schedules that maximize the number of timeslots that are filled.\n
821
+ You can either get a random selection of the best schedules (recommended), or ALL of the best schedules.\n
822
+ WARNING: It can sometimes take a LONG time to get all the best schedules!
823
+ """
824
+
825
+ about_markdown = f"""
826
+ # About the App\n
827
+ Hi! My name is Logan, and I created Schedule Buddy to be the one-stop-shop for making juggling workshop schedules.\n
828
+ 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
829
+
830
+ Schedule Buddy streamlines the process of the creating and filling out the forms, essentially replacing Google Forms.
831
+ 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.
832
+ If that is impossible, then the app will create schedules that maximize the number of workshops that are taught.
833
+ Essentially, Schedule Buddy removes the headache of trying to fit everyone into a timeslot. \n
834
+ For those who are curious and still reading, Schedule Buddy uses a recursive backtracking algorithm to make schedules.
835
+
836
+
837
+ # About Me
838
+ I've been juggling for the past 8 years, and 4 years ago I created a YouTube channel called Juggling Gym.\n
839
+ 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
840
+ and make sure that everyone was teaching their desired number of workshops.\n
841
+ 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
842
+ """
843
+
844
+
845
+ ### GRADIO ###
846
+ with gr.Blocks() as demo:
847
+ ### FILL OUT FORM ###
848
+ with gr.Tab('Fill Out Form'):
849
+ schedule_name = gr.Textbox(label="Form Name", info="What is the name of the form you want to fill out?")
850
+ schedule_name_btn = gr.Button('Submit', variant='primary')
851
+
852
+ with gr.Column(visible=False) as main_col:
853
+ juggler_name = gr.Textbox(label='Name (first and last)', visible=True)
854
+ email = gr.Textbox(label='Email Address', visible=True)
855
+ 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)
856
+
857
+ checkboxes = gr.CheckboxGroup([], label="Timeslots", info="Check the time(s) you can teach.", visible=False)
858
+
859
+ # Let the user dynamically describe their workshops
860
+ descriptions = gr.State([])
861
+ new_description = gr.Textbox(label='Workshop Descriptions', info='Describe the workshop(s) you want to teach. Hit "Enter" after each one.', visible=False)
862
+
863
+ def add_descrip(descriptions, new_description):
864
+ return descriptions + [{"name": new_description}], ""
865
+
866
+ new_description.submit(add_descrip, [descriptions, new_description], [descriptions, new_description])
867
+
868
+ @gr.render(inputs=descriptions)
869
+ def render_descriptions(descrip_list):
870
+ for elem in descrip_list:
871
+ with gr.Row():
872
+ gr.Textbox(elem['name'], show_label=False, container=False)
873
+ delete_btn = gr.Button("Delete", scale=0, variant="stop")
874
+ def delete(elem=elem):
875
+ descrip_list.remove(elem)
876
+ return descrip_list
877
+ delete_btn.click(delete, None, [descriptions])
878
+
879
+
880
+ submit_preferences_btn = gr.Button('Submit', variant='primary', visible=False)
881
+ schedule_name_btn.click(fn=get_timeslots, inputs=[schedule_name], outputs=[schedule_name_btn, checkboxes, main_col, submit_preferences_btn, new_description])
882
+ submit_preferences_btn.click(fn=submit_preferences, inputs=[schedule_name, juggler_name, email, num_workshops, checkboxes, descriptions])
883
+
884
+
885
+ ### MAKE FORM ###
886
+ with gr.Tab('Make Form'):
887
+ email = gr.Textbox(label="Email Adress")
888
+ schedule_name = gr.Textbox(label="Form Name", info='Keep it simple! Each person will have to type the form name to fill it out.')
889
+ 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.')
890
+ password_2 = gr.Textbox(label='Password Again', info='Enter your password again')
891
+ capacity = gr.Number(label="Capacity", info="Enter the maximum number of people who can teach per timeslot.")
892
+
893
+ # Dynamically render timeslots
894
+ # Based on: https://www.gradio.app/guides/dynamic-apps-with-render-decorator
895
+ slots = gr.State([])
896
+ new_slot = gr.Textbox(label='Enter Timeslots People Can Teach', info='Ex: Friday 7 pm, Saturday 11 am. Hit "Enter" after each one. Make sure to put them in CHRONOLOGICAL ORDER!')
897
+
898
+ def add_slot(slots, new_slot_name):
899
+ return slots + [{"name": new_slot_name}], ""
900
+
901
+ new_slot.submit(add_slot, [slots, new_slot], [slots, new_slot])
902
+
903
+ @gr.render(inputs=slots)
904
+ def render_slots(slot_list):
905
+ gr.Markdown(f"### Timeslots")
906
+ for slot in slot_list:
907
+ with gr.Row():
908
+ gr.Textbox(slot['name'], show_label=False, container=False)
909
+ delete_btn = gr.Button("Delete", scale=0, variant="stop")
910
+ def delete(slot=slot):
911
+ slot_list.remove(slot)
912
+ return slot_list
913
+ delete_btn.click(delete, None, [slots])
914
+
915
+
916
+ btn = gr.Button('Submit', variant='primary')
917
+ btn.click(
918
+ fn=make_form,
919
+ inputs=[email, schedule_name, password_1, password_2, capacity, slots],
920
+ )
921
+
922
+
923
+ ### VIEW FORM RESULTS ###
924
+ with gr.Tab('View Form Results'):
925
+ with gr.Column() as btn_group:
926
+ schedule_name = gr.Textbox(label="Form Name")
927
+ password = gr.Textbox(label="Password")
928
+ find_form_btn = gr.Button('Find Form', variant='primary')
929
+
930
+ # 1. Get all responses
931
+ with gr.Column(visible=False) as all_responses_col:
932
+ gr.Markdown('# Download All Form Responses')
933
+ gr.Markdown("Download everyone's responses to the form.")
934
+ all_responses_btn = gr.Button('Download All Form Responses', variant='primary')
935
+
936
+ with gr.Row() as all_responses_output_row:
937
+ 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)
938
+ file_out = gr.File(label = "Downloadable file", scale=1, visible=False)
939
+
940
+ all_responses_btn.click(fn=get_all_responses, inputs=[schedule_name, password], outputs=[df_out, file_out])
941
+
942
+
943
+ # 2. Generate schedules
944
+ with gr.Column(visible=False) as generate_schedules_explanation_col:
945
+ gr.Markdown('# Create Schedules based on Everyone\'s Preferences.')
946
+ with gr.Accordion('Details'):
947
+ gr.Markdown(generate_markdown)
948
+
949
+ with gr.Row(visible=False) as generate_btns_row:
950
+ generate_ten_results_btn = gr.Button('Generate a Subset of Schedules', variant='primary', visible=True)
951
+ generate_all_results_btn = gr.Button('Generate All Possible Schedules', visible=True)
952
+
953
+ with gr.Row(visible=True) as generated_schedules_output:
954
+ text_out = gr.Textbox(label='Results')
955
+ 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)
956
+ generated_file_out = gr.File(label = "Downloadable schedule file", scale=1, visible=False)
957
+
958
+ generate_ten_results_btn.click(fn=generate_schedules_wrapper_subset_results, inputs=[schedule_name, password], outputs=[text_out, generated_df_out, generated_file_out], api_name='generate_random_schedules')
959
+ generate_all_results_btn.click(fn=generate_schedules_wrapper_all_results, inputs=[schedule_name, password], outputs=[text_out, generated_df_out, generated_file_out], api_name='generate_all_schedules')
960
+
961
+
962
+ # 3. Open/close button
963
+ with gr.Column(visible=False) as open_close_btn_col:
964
+ gr.Markdown('# Open or Close Form')
965
+ open_close_btn = gr.Button(variant='primary')
966
+ open_close_btn.click(fn=toggle_btn, inputs=[schedule_name, password], outputs=[open_close_btn])
967
+
968
+
969
+ 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])
970
+
971
+
972
+ ### INFO ###
973
+ with gr.Tab('About'):
974
+ gr.Markdown(about_markdown)
975
+
976
+ directory = os.path.abspath(os.getcwd())
977
+ allowed = directory #+ "/schedules"
978
+ demo.launch(allowed_paths=[allowed], show_error=True)