Juggling commited on
Commit
161b7ad
·
verified ·
1 Parent(s): 084d4cd

Upload folder using huggingface_hub

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