-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
450 lines (370 loc) · 22 KB
/
app.py
File metadata and controls
450 lines (370 loc) · 22 KB
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
from flask import Flask, request, jsonify, render_template
import random
from datetime import datetime, timedelta
import json
import os
app = Flask(__name__)
def validate_input(subjects, working_days, periods_per_day, break_period_after_str, lunch_period_after_str, break_duration_str, lunch_duration_str):
total_periods = sum(int(subject['periodsPerWeek']) for subject in subjects)
available_slots = working_days * periods_per_day # This is the number of actual class slots
break_period_after = int(break_period_after_str) if break_period_after_str else -1
lunch_period_after = int(lunch_period_after_str) if lunch_period_after_str else -1
# Check if any subject requires consecutive periods
for subject in subjects:
if subject.get('requiresConsecutivePeriods', False):
periods = int(subject['periodsPerWeek'])
if periods > periods_per_day:
return False, f"Subject '{subject['name']}' requires {periods} consecutive periods but there are only {periods_per_day} periods per day."
# Validate break and lunch periods are not the same and within valid range
if break_period_after != -1 and lunch_period_after != -1 and break_period_after == lunch_period_after:
return False, "Break and lunch cannot be after the same period."
# Ensure break/lunch periods are not after non-existent periods (e.g. after period 5 when there are only 5 periods)
# The value is "after period X", so X can be periods_per_day (meaning after the last period)
if break_period_after != -1 and break_period_after > periods_per_day:
return False, "Break period is set after a period that doesn't exist."
if lunch_period_after != -1 and lunch_period_after > periods_per_day:
return False, "Lunch period is set after a period that doesn't exist."
# Calculate potential period duration to check for negative values
# Need to get startTime and endTime from request.json to perform this check
# This part should be moved to where data is available or pass more parameters
# For now, let's assume we are passing all necessary data from the generate function.
# However, since validate_input is called *before* parsing start_time and end_time,
# I'll move this check to the `generate` function where all data is available, or add more params.
# Let's add parameters for start_time_str and end_time_str to this function for proper validation.
if total_periods > available_slots:
return False, f"Total periods ({total_periods}) exceeds available class slots ({available_slots})."
return True, ""
def generate_time_slots(
start_time_str, end_time_str, periods_per_day, break_after_period, break_duration, lunch_after_period, lunch_duration
):
start_time = datetime.strptime(start_time_str, '%I:%M %p')
end_time = datetime.strptime(end_time_str, '%I:%M %p')
time_slots = []
current_time = start_time
period_count = 0
# If end_time is chronologically before start_time, assume it's on the next day
if end_time < start_time:
end_time += timedelta(days=1)
break_period_after = int(break_after_period) if break_after_period else -1
lunch_period_after = int(lunch_after_period) if lunch_after_period else -1
break_duration = int(break_duration)
lunch_duration = int(lunch_duration)
total_duration_minutes = (end_time - start_time).total_seconds() / 60
# Use periods_per_day as the number of class periods
num_class_periods = periods_per_day
# Calculate total time for breaks and lunch that are *actually inserted* as separate rows
total_break_lunch_duration_for_calculation = 0
if break_period_after != -1 and 0 <= break_period_after <= periods_per_day:
total_break_lunch_duration_for_calculation += break_duration
if lunch_period_after != -1 and 0 <= lunch_period_after <= periods_per_day:
total_break_lunch_duration_for_calculation += lunch_duration
actual_teaching_minutes = total_duration_minutes - total_break_lunch_duration_for_calculation
if actual_teaching_minutes <= 0 or num_class_periods == 0: # Ensure positive duration for periods
calculated_period_duration = 0 # Rename to avoid conflict
else:
calculated_period_duration = actual_teaching_minutes / num_class_periods
final_time_slots_with_actual_times = []
current_calc_time = start_time
class_period_counter = 0
break_inserted = False
lunch_inserted = False
# This loop will now explicitly control the sequence of insertions
for i in range(periods_per_day + 2): # Max possible iterations to include both breaks if scheduled
# Check if we need to insert a break BEFORE the current class period
if break_period_after != -1 and class_period_counter == break_period_after and not break_inserted:
final_time_slots_with_actual_times.append("BREAK")
current_calc_time += timedelta(minutes=break_duration)
break_inserted = True
# Check if we need to insert lunch BEFORE the current class period
if lunch_period_after != -1 and class_period_counter == lunch_period_after and not lunch_inserted:
final_time_slots_with_actual_times.append("LUNCH")
current_calc_time += timedelta(minutes=lunch_duration)
lunch_inserted = True
# Insert the actual class period if there are still class periods to add
if class_period_counter < periods_per_day:
slot_start_str = current_calc_time.strftime('%I:%M %p')
current_calc_time += timedelta(minutes=calculated_period_duration)
slot_end_str = current_calc_time.strftime('%I:%M %p')
final_time_slots_with_actual_times.append(f"{slot_start_str} - {slot_end_str}")
class_period_counter += 1
elif class_period_counter >= periods_per_day and break_inserted and lunch_inserted: # Optimization: exit if all periods and breaks inserted
break
# Final check for breaks/lunch that might occur exactly after the last class period and haven't been inserted
if periods_per_day == break_period_after and not break_inserted:
final_time_slots_with_actual_times.append("BREAK")
if periods_per_day == lunch_period_after and not lunch_inserted:
final_time_slots_with_actual_times.append("LUNCH")
return final_time_slots_with_actual_times
def can_place_consecutive(timetable, day, start_timetable_row_idx, num_periods, time_slots_display):
# Check if we can place consecutive periods starting from start_timetable_row_idx
if start_timetable_row_idx + num_periods > len(timetable) or start_timetable_row_idx < 0:
return False
# Check if all required slots are empty and are not break/lunch slots
for period_idx in range(start_timetable_row_idx, start_timetable_row_idx + num_periods):
# Ensure the period_idx is within bounds of time_slots_display
if period_idx >= len(time_slots_display) or period_idx < 0:
return False
if timetable[period_idx]['slots'][day] != '': # Slot already occupied
return False
if time_slots_display[period_idx] == "BREAK" or time_slots_display[period_idx] == "LUNCH":
return False # Cannot place a subject during break/lunch
return True
def is_valid_placement(timetable, day, timetable_row_idx, subject_name, requires_consecutive=False, time_slots_display=None):
# Check if timetable_row_idx is valid
if timetable_row_idx < 0 or timetable_row_idx >= len(timetable):
return False
# Check if this is a break/lunch slot based on the time_slots_display
if time_slots_display[timetable_row_idx] == "BREAK" or time_slots_display[timetable_row_idx] == "LUNCH":
return False
# The check for preventing a subject from being scheduled multiple times in the same day is removed
# as the current logic with processed_subjects handles individual period placements,
# and the user expects multiple occurrences of a subject on the same day.
# For non-consecutive subjects, check if they're scheduled in consecutive periods
if not requires_consecutive:
# Check period before
if timetable_row_idx > 0 and \
timetable[timetable_row_idx-1]['slots'][day] == subject_name and \
time_slots_display[timetable_row_idx-1] not in ["BREAK", "LUNCH"]:
pass
# Check period after
if timetable_row_idx < len(timetable)-1 and \
timetable[timetable_row_idx+1]['slots'][day] == subject_name and \
time_slots_display[timetable_row_idx+1] not in ["BREAK", "LUNCH"]:
pass
return True
def generate_timetable(subjects, working_days, periods_per_day, time_slots_display):
# Create empty timetable based on the length of time_slots_display
timetable = []
for time_slot_str in time_slots_display:
row = {'time': time_slot_str, 'slots': [''] * working_days}
timetable.append(row)
# Prepare subjects: ensure preferred_slots is always a list and flat structure for regular periods
processed_subjects = []
for subject in subjects:
periods = int(subject['periodsPerWeek'])
is_consecutive = subject.get('requiresConsecutivePeriods', False)
preferred_slots = subject.get('preferredSlots', [])
if not isinstance(preferred_slots, list):
preferred_slots = []
if is_consecutive:
processed_subjects.append({
'name': subject['name'],
'periods': periods,
'preferred_slots': preferred_slots,
'is_consecutive': True
})
else:
for _ in range(periods):
processed_subjects.append({
'name': subject['name'],
'periods': 1, # Each instance is 1 period
'preferred_slots': preferred_slots,
'is_consecutive': False
})
# Separate into queues based on preference and type for phased placement
consecutive_preferred_queue = sorted([s for s in processed_subjects if s['is_consecutive'] and s['preferred_slots']], key=lambda x: len(x['preferred_slots']), reverse=True)
consecutive_other_queue = [s for s in processed_subjects if s['is_consecutive'] and not s['preferred_slots']]
regular_preferred_queue = sorted([s for s in processed_subjects if not s['is_consecutive'] and s['preferred_slots']], key=lambda x: len(x['preferred_slots']), reverse=True)
regular_other_queue = [s for s in processed_subjects if not s['is_consecutive'] and not s['preferred_slots']]
# Iterative placement to ensure all subjects are placed if possible
unplaced_subjects = list(processed_subjects) # Start with all subjects unplaced
max_placement_attempts = 100 # Safety break for unlikely infinite loops
current_attempt = 0
while unplaced_subjects and current_attempt < max_placement_attempts:
current_attempt += 1
made_progress_in_this_attempt = False
random.shuffle(unplaced_subjects) # Re-shuffle for variety in each attempt
# Create temporary queues for this iteration based on current unplaced subjects
current_consecutive_preferred = sorted([s for s in unplaced_subjects if s['is_consecutive'] and s['preferred_slots']], key=lambda x: len(x['preferred_slots']), reverse=True)
current_regular_preferred = sorted([s for s in unplaced_subjects if not s['is_consecutive'] and s['preferred_slots']], key=lambda x: len(x['preferred_slots']), reverse=True)
current_consecutive_other = [s for s in unplaced_subjects if s['is_consecutive'] and not s['preferred_slots']]
current_regular_other = [s for s in unplaced_subjects if not s['is_consecutive'] and not s['preferred_slots']]
# Attempt to place consecutive preferred subjects
for subject_data in current_consecutive_preferred:
if subject_data.get('placed'): continue
placed_current_subject = False
shuffled_preferred_slots = list(subject_data['preferred_slots'])
random.shuffle(shuffled_preferred_slots)
for day, preferred_class_period_idx in shuffled_preferred_slots:
actual_timetable_row_idx = -1
current_class_period_count = 0
for row_idx, slot_str in enumerate(time_slots_display):
if slot_str not in ["BREAK", "LUNCH"]:
if current_class_period_count == preferred_class_period_idx:
actual_timetable_row_idx = row_idx
break
current_class_period_count += 1
if actual_timetable_row_idx != -1:
can_place = can_place_consecutive(timetable, day, actual_timetable_row_idx, subject_data['periods'], time_slots_display)
is_valid = is_valid_placement(timetable, day, actual_timetable_row_idx, subject_data['name'], True, time_slots_display)
if can_place and is_valid:
for p_offset in range(subject_data['periods']):
timetable[actual_timetable_row_idx + p_offset]['slots'][day] = subject_data['name']
placed_current_subject = True
subject_data['placed'] = True
made_progress_in_this_attempt = True
break # Break from preferred slot loop
if placed_current_subject: continue # Move to next subject
# Attempt to place regular preferred subjects
for subject_data in current_regular_preferred:
if subject_data.get('placed'): continue
placed_current_subject = False
shuffled_preferred_slots = list(subject_data['preferred_slots'])
random.shuffle(shuffled_preferred_slots)
for day, preferred_class_period_idx in shuffled_preferred_slots:
actual_timetable_row_idx = -1
current_class_period_count = 0
for row_idx, slot_str in enumerate(time_slots_display):
if slot_str not in ["BREAK", "LUNCH"]:
if current_class_period_count == preferred_class_period_idx:
actual_timetable_row_idx = row_idx
break
current_class_period_count += 1
if actual_timetable_row_idx != -1:
is_valid = is_valid_placement(timetable, day, actual_timetable_row_idx, subject_data['name'], subject_data['is_consecutive'], time_slots_display)
if timetable[actual_timetable_row_idx]['slots'][day] == '' and is_valid:
timetable[actual_timetable_row_idx]['slots'][day] = subject_data['name']
placed_current_subject = True
subject_data['placed'] = True
made_progress_in_this_attempt = True
break # Break from preferred slot loop
if placed_current_subject: continue # Move to next subject
# Attempt to place remaining consecutive subjects (non-preferred)
for subject_data in current_consecutive_other:
if subject_data.get('placed'): continue
placed_current_subject = False
days_shuffled = list(range(working_days))
random.shuffle(days_shuffled)
for day in days_shuffled:
rows_to_check = []
for r_idx, s_str in enumerate(time_slots_display):
if s_str not in ["BREAK", "LUNCH"]:
rows_to_check.append(r_idx)
random.shuffle(rows_to_check)
for row_idx in rows_to_check:
can_place = can_place_consecutive(timetable, day, row_idx, subject_data['periods'], time_slots_display)
is_valid = is_valid_placement(timetable, day, row_idx, subject_data['name'], True, time_slots_display)
if (row_idx + subject_data['periods'] <= len(timetable) and
can_place and is_valid):
for p_offset in range(subject_data['periods']):
timetable[row_idx + p_offset]['slots'][day] = subject_data['name']
placed_current_subject = True
subject_data['placed'] = True
made_progress_in_this_attempt = True
break # Break from row_idx loop
if placed_current_subject: break # Break from day loop
if placed_current_subject: continue # Move to next subject
# Attempt to place remaining regular subjects (non-preferred)
for subject_data in current_regular_other:
if subject_data.get('placed'): continue
placed_current_subject = False
days_shuffled = list(range(working_days))
random.shuffle(days_shuffled)
for day in days_shuffled:
rows_to_check = []
for r_idx, s_str in enumerate(time_slots_display):
if s_str not in ["BREAK", "LUNCH"]:
rows_to_check.append(r_idx)
random.shuffle(rows_to_check)
for row_idx in rows_to_check:
is_valid = is_valid_placement(timetable, day, row_idx, subject_data['name'], False, time_slots_display)
if (timetable[row_idx]['slots'][day] == '' and is_valid):
timetable[row_idx]['slots'][day] = subject_data['name']
placed_current_subject = True
subject_data['placed'] = True
made_progress_in_this_attempt = True
break # Break from row_idx loop
if placed_current_subject: break # Break from day loop
if placed_current_subject: continue # Move to next subject
# Update unplaced_subjects for the next iteration
unplaced_subjects = [s for s in processed_subjects if not s.get('placed')]
# If no subjects were placed in this entire attempt, break to prevent infinite loop
if not made_progress_in_this_attempt and unplaced_subjects:
print(f"Warning: No subjects placed in attempt {current_attempt}. Remaining unplaced: {len(unplaced_subjects)}")
break # No progress, likely impossible to place remaining
if unplaced_subjects:
print(f"ERROR: Could not place all subjects! Remaining unplaced: {[s['name'] for s in unplaced_subjects]}")
else:
print("All subjects successfully placed.")
return timetable
@app.route('/')
def index():
return render_template('index.html')
@app.route('/generate', methods=['POST'])
def generate():
data = request.json
periods_per_day = int(data['periodsPerDay'])
# Perform duration validation before calling validate_input for other checks
start_time_obj = datetime.strptime(data['startTime'], '%I:%M %p')
end_time_obj = datetime.strptime(data['endTime'], '%I:%M %p')
if end_time_obj < start_time_obj:
end_time_obj += timedelta(days=1)
total_duration_minutes = (end_time_obj - start_time_obj).total_seconds() / 60
break_duration = int(data['breakDuration'])
lunch_duration = int(data['lunchDuration'])
break_period_after = int(data.get('breakPeriod')) if data.get('breakPeriod') else -1
lunch_period_after = int(data.get('lunchPeriod')) if data.get('lunchPeriod') else -1
total_break_lunch_duration_for_validation = 0
if break_period_after != -1 and 0 <= break_period_after <= periods_per_day:
total_break_lunch_duration_for_validation += break_duration
if lunch_period_after != -1 and 0 <= lunch_period_after <= periods_per_day:
total_break_lunch_duration_for_validation += lunch_duration
if total_break_lunch_duration_for_validation >= total_duration_minutes:
return jsonify({'error': "Total break and lunch durations exceed or equal the total college time. Please reduce durations or increase college time."}), 400
# Validate input (now also passes durations for checks)
is_valid, error_message = validate_input(
data['subjects'],
int(data['workingDays']),
periods_per_day,
data.get('breakPeriod'),
data.get('lunchPeriod'),
data['breakDuration'],
data['lunchDuration']
)
if not is_valid:
return jsonify({'error': error_message}), 400
# Generate time slots (will now include 'BREAK' and 'LUNCH' strings)
time_slots_display = generate_time_slots(
data['startTime'],
data['endTime'],
periods_per_day,
data.get('breakPeriod'),
data['breakDuration'],
data.get('lunchPeriod'),
data['lunchDuration']
)
# Generate 4 different timetables
timetables = []
for _ in range(4):
timetable = generate_timetable(
data['subjects'],
int(data['workingDays']),
periods_per_day,
time_slots_display
)
timetables.append({
'workingDays': int(data['workingDays']),
'schedule': timetable
})
return jsonify(timetables)
@app.route('/save', methods=['POST'])
def save_timetable():
data = request.json
if not os.path.exists('saved_timetables'):
os.makedirs('saved_timetables')
filename = f"saved_timetables/timetable_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, 'w') as f:
json.dump(data, f)
return jsonify({'message': 'Timetable saved successfully'})
@app.route('/load', methods=['GET'])
def load_timetables():
if not os.path.exists('saved_timetables'):
return jsonify([])
timetables = []
for filename in os.listdir('saved_timetables'):
if filename.endswith('.json'):
with open(os.path.join('saved_timetables', filename), 'r') as f:
timetables.append(json.load(f))
return jsonify(timetables)
if __name__ == '__main__':
app.run(debug=True)