-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathschedulerProcess.py
688 lines (552 loc) · 28.4 KB
/
schedulerProcess.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
from schedule.rabbitConnectionManager import RabbitConnectionManager
from json import loads, JSONDecodeError
from schedule import scheduler4_3
from schedule.ra_sched import RA, Schedule
from scheduleServer import app
import copy as cp
import calendar
import psycopg2
import logging
import atexit
import os
# import the needed functions from other parts of the application
from helperFunctions.helperFunctions import getSchoolYear
from staff.staff import getRAStats, addRAPointModifier
# Connect to RabbitMQ for both the consumer and the error queue.
rabbitConsumerManager = RabbitConnectionManager(
os.getenv('CLOUDAMQP_URL', 'amqp://guest:guest@localhost:5672/%2f'),
os.getenv('RABBITMQ_SCHEDULER_QUEUE', 'genSched')
)
rabbitErrorQueueManager = RabbitConnectionManager(
os.getenv('CLOUDAMQP_URL', 'amqp://guest:guest@localhost:5672/%2f'),
os.getenv('RABBITMQ_SCHEDULER_FAILURE_QUEUE', 'genSchedErrs'),
durable=True
)
logging.info("Connected to '{}' message queue channel.".format(rabbitConsumerManager.rabbitQueueName))
logging.info("Connected to '{}' message queue channel.".format(rabbitErrorQueueManager.rabbitQueueName))
# Establish DB connection
psqlConnectionStr = os.getenv('DATABASE_URL', 'postgres:///ra_sched')
dbConn = psycopg2.connect(psqlConnectionStr)
def startup():
# Start up the worker process and begin consuming RabbitMQ messages.
# Configure the server app so that we can use parts of it in this worker process
# Disable the login_required decorator for this instance
app.config["LOGIN_DISABLED"] = True
# Reinitialize the Login Manager to accept the new configuration
app.login_manager.init_app(app)
logging.info("Begin consuming messages from '{}'".format(rabbitConsumerManager.rabbitQueueName))
# Begin consuming messages from the message queue!
rabbitConsumerManager.consumeMessages(parseMessage, rabbitQueueLimit=1)
def teardown():
# Close the necessary connections and clean up after ourselves
dbConn.close()
rabbitConsumerManager.closeConnection()
rabbitErrorQueueManager.closeConnection()
def parseMessage(ch, method, properties, body):
# Parse the received rabbitMQ message for the information needed to
# run the scheduler.
#
# This function accepts the following parameters:
#
# channel <pika.channel.Channel> - The channel which the message was received from.
# method <pika.spec.Basic.Deliver> - The method used to retrieve the message.
# properties <pika.spec.BasicProperties> - Metadata about the message. We expect to have a
# header with key "sqid" which is the id of the
# corresponding record in the scheduler_queue
# table.
# body <bytes> - A bytes array containing the received message.
# |
# |- resHallID <int>
# |- monthNum <int>
# |- year <int>
# |- eligibleRAList <lst<int>>
# |- noDutyList <lst<int>>
logging.info("Message Received.")
# Convert the body from a bytes array to a string
strBody = body.decode("utf-8")
# Set the default state to NOT run the scheduler
clearToRunScheduler = False
# Set the default state to update the scheduler_queue record
updateSQRecord = True
# Create a default status and reason in case weird things happen.
# These should be overridden by the end of processing this message.
status = -99
reason = ""
# Attempt to parse the body into a JSON object
try:
# Grab the Scheduler_Queue ID
msgSQID = properties.headers["sqid"]
# Parse the json from the body
parsedParams = loads(strBody)
# With all of the necessary information, we can run the scheduler
clearToRunScheduler = True
except (TypeError, KeyError):
# If we receive a KeyError, then that means we could not find the
# expected "sqid" header.
# Log the occurrence
logging.exception(
"Received message from '{}' with no 'sqid' header.".format(rabbitConsumerManager.rabbitQueueName)
)
# Since the SQID is the only way for us to know what record in the DB
# is associated with this message, and we were unable to get the SQID,
# then send this message to the error queue with a sqid of -1.
forwardMsgToErrorQueue(
"Received message from '{}' with no 'sqid' header.".format(rabbitConsumerManager.rabbitQueueName),
strBody,
-1
)
# Set the state to NOT update the scheduler_queue record since we don't have a SQID
updateSQRecord = False
except JSONDecodeError:
# If we receive a JSONDecodeError, then consider the message to be
# invalid for one reason or another.
# Log the occurrence
logging.exception("Unable to decode message body into JSON for SQID: {}".format(msgSQID))
# Update the status and reason to reflect what occurred here.
status = -2
reason = "Unable to parse scheduler request."
# Forward the message to the Error Queue where it can be looked into in further detail.
forwardMsgToErrorQueue(
"Unable to decode message body into JSON for SQID: {}"
.format(rabbitConsumerManager.rabbitQueueName),
strBody,
msgSQID
)
# If we have been cleared to run the scheduler...
if clearToRunScheduler:
# Then do so!
status, reason = runScheduler(**parsedParams)
# If we should update the scheduler_queue record with these results
if updateSQRecord:
# Update the status and reason of the corresponding scheduler_queue record
cur = dbConn.cursor()
cur.execute("""
UPDATE scheduler_queue
SET status = %s,
reason = %s
WHERE id = %s
""", (status, reason, msgSQID))
dbConn.commit()
cur.close()
# Acknowledge that we have received and handled the message.
ch.basic_ack(delivery_tag=method.delivery_tag)
logging.info("Message Processing Complete.")
def forwardMsgToErrorQueue(reason, forwardedMsg, sqid):
# Forward the provided message to the error queue for future review.
# Create the message body
msgBody = {
"failure_reason": reason,
"forwarded_message_body": forwardedMsg
}
# Publish a persistent message to the error queue.
rabbitErrorQueueManager.publishMsg(msgBody, {"sqid": sqid}, deliveryMode=2)
logging.info("Forwarded message to '{}' failure queue.".format(rabbitErrorQueueManager.rabbitQueueName))
def getSchedulerRunTimeout():
# Grab the timeout for a single scheduler run from the environment. Doing it
# this way means that we can update it in the environment without restarting
# the process.
defaultTimeout = 5
try:
# Grab the value from the environment
timeout = int(os.getenv('SINGLE_SCHEDULER_RUN_TIMEOUT', defaultTimeout))
except ValueError as ex:
# If a ValueError was encountered, then that means that there was an issue
# while attempting to parse the value of the environment variable
# Log the occurrence
logging.exception(
"Error Parsing ENV Variable for Single Scheduler Run Timeout. " +
"Default value of '{}' is being used.".format(defaultTimeout) +
"Please check configuration."
)
logging.exception("getSchedulerRunTimeout - ValueError: {}".format(ex))
# Set the default timeout
timeout = defaultTimeout
return timeout
def runScheduler(resHallID, monthNum, year, noDutyList, eligibleRAList):
# Run the duty scheduler for the given Res Hall and month. Any users associated with the staff
# that have an auth_level of HD will NOT be scheduled.
#
# When called, the following parameters are required:
#
# resHallID <int> - an integer representing the res_hall.id of the Res Hall
# that the scheduler should be run for.
# monthNum <int> - an integer representing the numeric month number for
# the desired month using the standard gregorian
# calendar convention.
# year <int> - an integer denoting the year for the desired time period
# using the standard gregorian calendar convention.
# noDutyList <str> - a string containing comma separated integers that represent
# a date in the month in which no duty should be scheduled.
# If set to an empty string, then all days in the month will
# be scheduled.
# eligibleRAList <str> - a string containing comma separated integers that represent
# the ra.id for all RAs that should be considered for duties.
# If set to an empty string, then all ras with an auth_level
# of less than HD will be scheduled.
# requestID <int> - an integer representing the scheduler_queue.id for the
# corresponding request record in the DB.
#
# This method returns the following items:
#
# status <int> - an integer denoting what the result of the schedule process was.
# |
# |- 1 : the duty scheduling was successful
# |- -1 : the duty scheduling was unsuccessful
# |- -2 : an error occurred while scheduling
#
# reason <str> - a string containing a brief explanation of why the result occurred.
# Check to see if values have been passed through the
# eligibeRAs parameter
if len(eligibleRAList) != 0:
# Create a formatted psql string to add to the query that loads
# the necessary information from the DB
eligibleRAStr = "AND ra.id IN ({});".format(str(eligibleRAList)[1:-1])
else:
# Otherwise if there are no values passed in eligibleRAs, then
# set eligibleRAStr to ";" to end the query string
eligibleRAStr = ";"
# Create a DB cursor
cur = dbConn.cursor()
# Query the DB for the given month
cur.execute("SELECT id, year FROM month WHERE num = %s AND EXTRACT(YEAR FROM year) = %s",
(monthNum, year))
# Load the results from the DB
monthRes = cur.fetchone()
# Check to see if the query returned a result
if monthRes is None:
# If not, then log the occurrence
logging.warning("Unable to find month {}/{} in DB.".format(monthNum, year))
# Return the appropriate status and reason
return -1, "Unable to find month {}/{} in DB.".format(monthNum, year)
else:
# Otherwise, unpack the monthRes into monthId and year
monthId, date = monthRes
logging.debug("MonthId: {}".format(monthId))
# -- Get all eligible RAs and their conflicts --
# Query the DB for all of the eligible RAs for a given hall, and their
# conflicts for the given month excluding any ra table records with
# an auth_level of 3 or higher.
cur.execute("""
SELECT ra.first_name, ra.last_name, ra.id, sm.res_hall_id, sm.start_date,
COALESCE(cons.array_agg, ARRAY[]::date[])
FROM ra LEFT OUTER JOIN (
SELECT ra_id, ARRAY_AGG(days.date)
FROM conflicts JOIN (
SELECT id, date
FROM day
WHERE month_id = %s
) AS days
ON (conflicts.day_id = days.id)
GROUP BY ra_id
) AS cons
ON (ra.id = cons.ra_id)
JOIN staff_membership AS sm ON (sm.ra_id = ra.id)
WHERE sm.res_hall_id = %s
AND sm.auth_level < 3 {}
""".format(eligibleRAStr), (monthId, resHallID))
# Load the result from the DB
partialRAList = cur.fetchall()
# Get the start date for the school year. This will be used
# to calculate how many points each RA has up to the month
# being scheduled.
start, _ = getSchoolYear(date.month, date.year, resHallID)
# Get the end from the DB. The end date will be the first day
# of the month being scheduled. This will prevent the scheduler
# from using the number of points an RA had during a previous
# run of the scheduler for this month.
cur.execute("SELECT year FROM month WHERE id = %s", (monthId,))
# Load the value from the DB and convert it to a string in the expected format
end = cur.fetchone()[0].isoformat()
# Get the number of days in the given month
_, dateNum = calendar.monthrange(date.year, date.month)
# Calculate and format maxBreakDay which is the latest break duty that should be
# included for the duty points calculation.
maxBreakDuty = "{:04d}-{:02d}-{:02d}".format(date.year, date.month, dateNum)
with app.test_request_context():
# Get the RA statistics for the given hall within the flask app context
ptsDict = getRAStats(resHallID, start, end, maxBreakDay=maxBreakDuty)
logging.debug("ptsDict: {}".format(ptsDict))
# Assemble the RA list with RA objects that have the individual RAs' information
ra_list = []
for res in partialRAList:
# Add up the RA's duty points and any point modifier
pts = ptsDict[res[2]]["pts"]["dutyPts"] + ptsDict[res[2]]["pts"]["modPts"]
# Parse out the date information since we only use the day in this implementation
parsedConflictDays = [dateObject.day for dateObject in res[5]]
# Append the newly created RA to the ra_list
ra_list.append(
RA(
res[0], # First Name
res[1], # Last Name
res[2], # RA ID
res[3], # Hall ID
res[4], # Start Date
parsedConflictDays, # Conflicts
pts # Points
)
)
# Set the Last Duty Assigned Tolerance based on floor dividing the number of
# RAs by 2 then adding 1. For example, with a staff_manager of 15, the LDA Tolerance
# would be 8 days.
# Calculate the last date assigned tolerance (LDAT) which is the number of days
# before an RA is to be considered again for a duty. This should start as
# one more than half of the number of RAs in the list. The scheduler algorithm
# will adjust this as needed if the value passed in does not generate a schedule.
ldat = (len(ra_list) // 2) + 1
# Query the DB for the last 'x' number of duties from the previous month so that we
# do not schedule RAs back-to-back between months.
# 'x' is currently defined to be the last day assigned tolerance
# Create a startMonthStr that can be used as the earliest boundary for the duties from the
# last 'x' duties from the previous month.
# If the monthNum is 1 (If the desired month is January)
if monthNum == 1:
# Then the previous month is 12 (December) of the previous year
startMonthStr = '{}-12'.format(date.year - 1)
else:
# Otherwise the previous month is going to be from the same year
startMonthStr = '{}-{}'.format(date.year, "{0:02d}".format(monthNum - 1))
# Generate the endMonthStr which is simply a dateStr that represents the
# first day of the month in which the scheduler should run.
endMonthStr = '{}-{}'.format(date.year, "{0:02d}".format(monthNum))
# Log this information for the debugger!
logging.debug("StartMonthStr: {}".format(startMonthStr))
logging.debug("EndMonthStr: {}".format(endMonthStr))
logging.debug("Hall Id: {}".format(resHallID))
logging.debug("Year: {}".format(date.year))
logging.debug('MonthNum: {0:02d}'.format(monthNum))
logging.debug("LDAT: {}".format(ldat))
# Query the DB for the last 'x' number of duties from the previous month so that we
# do not schedule RAs back-to-back between months.
cur.execute("""SELECT ra.first_name, ra.last_name, ra.id, sm.res_hall_id,
sm.start_date, day.date - TO_DATE(%s, 'YYYY-MM-DD'),
duties.flagged
FROM duties JOIN day ON (day.id=duties.day_id)
JOIN ra ON (ra.id=duties.ra_id)
JOIN staff_membership AS sm ON (sm.ra_id = ra.id)
WHERE duties.hall_id = %s
AND duties.sched_id IN (
SELECT DISTINCT ON (schedule.month_id) schedule.id
FROM schedule
WHERE schedule.hall_id = %s
AND schedule.month_id IN (
SELECT month.id
FROM month
WHERE month.year >= TO_DATE(%s, 'YYYY-MM')
AND month.year <= TO_DATE(%s, 'YYYY-MM')
)
ORDER BY schedule.month_id, schedule.created DESC, schedule.id DESC
)
AND day.date >= TO_DATE(%s,'YYYY-MM-DD') - %s
AND day.date <= TO_DATE(%s,'YYYY-MM-DD') - 1
ORDER BY day.date ASC;
""", (endMonthStr + "-01", resHallID, resHallID, startMonthStr, endMonthStr,
endMonthStr + "-01", ldat, endMonthStr + "-01"))
# Load the query results from the DB
prevDuties = cur.fetchall()
# Create shell RA objects that will hash to the same value as their respective RA objects.
# This hash is how we map the equivalent RA objects together. These shell RAs will be put
# in a tuple containing the RA, the number of days from the duty date to the beginning of
# the next month, and a boolean whether or not that duty was flagged.
# Ex: (RA Shell, No. days since last duty, Whether the duty is flagged)
prevRADuties = [(RA(d[0], d[1], d[2], d[3], d[4]), d[5], d[6]) for d in prevDuties]
logging.debug("PREVIOUS DUTIES: {}".format(prevRADuties))
# Query the DB for a list of break duties for the given month.
# In version 4.0 of the scheduler, break duties essentially are treated
# like noDutyDates and are skipped in the scheduling process. As a result,
# only the date is needed.
cur.execute("""
SELECT TO_CHAR(day.date, 'DD')
FROM break_duties JOIN day ON (break_duties.day_id = day.id)
WHERE break_duties.month_id = {}
AND break_duties.hall_id = {}
""".format(monthId, resHallID))
# Load the results from the DB and convert the value to an int.
breakDuties = [int(row[0]) for row in cur.fetchall()]
logging.debug("Break Duties: {}".format(breakDuties))
# Attempt to run the scheduler using deep copies of the raList and noDutyList.
# This is so that if the scheduler does not resolve on the first run, we
# can modify the parameters and try again with a fresh copy of the raList
# and noDutyList.
copy_raList = cp.deepcopy(ra_list)
copy_noDutyList = cp.copy(noDutyList)
# Load the Res Hall's settings for the scheduler
cur.execute("""SELECT duty_config, auto_adj_excl_ra_pts, flag_multi_duty
FROM hall_settings
WHERE res_hall_id = %s""", (resHallID,))
dutyConfig, autoExcAdj, flagMultiDuty = cur.fetchone()
# AutoExcAdj is a currently unused feature that allows the scheduler to
# automatically create point_modifiers for RAs that have been excluded from
# being scheduled for the given month. This feature is unreleased because
# if the user sets this to true and then runs the scheduler more than once
# for the same month, there is no way for the application to know if it
# created any point_modifiers for the excluded RAs of the previous run so
# that it can remove them from the system/algorithm. As a result, any
# point_modifiers created in this manner will just grow and grow until
# an HD goes in and manually alters the point_modifier.modifier value.
# TODO: Figure out how to address the above comment. Possibly implement a
# draft system so that AHD+ users can view a scheduler run before
# publishing it to the rest of staff?
regNumAssigned = dutyConfig["reg_duty_num_assigned"]
mulNumAssigned = dutyConfig["multi_duty_num_assigned"]
regDutyPts = dutyConfig["reg_duty_pts"]
mulDutyPts = dutyConfig["multi_duty_pts"]
mulDutyDays = dutyConfig["multi_duty_days"]
# Set completed to False and successful to False by default. These values
# will be manipulated in the while loop below as necessary.
completed = False
successful = False
while not completed:
# While we are not finished scheduling, create a candidate schedule
sched = scheduler4_3.schedule(
copy_raList, year, monthNum, doubleDateNum=mulNumAssigned, doubleDatePts=mulDutyPts,
noDutyDates=copy_noDutyList, doubleDays=mulDutyDays, doublePts=mulDutyPts,
ldaTolerance=ldat, doubleNum=mulNumAssigned, prevDuties=prevRADuties,
breakDuties=breakDuties, setDDFlag=flagMultiDuty, regDutyPts=regDutyPts,
regNumAssigned=regNumAssigned, timeout=getSchedulerRunTimeout()
)
# If the schedule's status encountered an error and cannot create a schedule
if sched.getStatus() == Schedule.ERROR:
# Stop the scheduling attempt and report the error back to the user
completed = True
continue
# If we were unable to schedule with the previous parameters,
if sched.getStatus() == Schedule.FAIL:
# Then we should attempt to modify the previous parameters
# and try again.
if ldat > 1:
# If the LDATolerance is greater than 1
# then decrement the LDATolerance by 1 and try again
logging.info("DECREASE LDAT: {}".format(ldat))
ldat -= 1
# Create new deep copies of the ra_list and noDutyList
copy_raList = cp.deepcopy(ra_list)
copy_noDutyList = cp.copy(noDutyList)
else:
# Otherwise the LDATolerance is not greater than 1. In this
# case, we were unable to successfully generate a schedule
# with the given parameters.
completed = True
else:
# Otherwise, we were able to successfully create a schedule!
# Mark that we have completed so we may exit the while loop
# and mark that we were successful.
completed = True
successful = True
logging.debug("Schedule: {}".format(sched))
# If we were not successful in generating a duty schedule.
if not successful:
# Log the occurrence
logging.info("Unable to Generate Schedule for Hall: {} MonthNum: {} Year: {}"
.format(resHallID, monthNum, year))
# Return the schedule object to the caller
return sched.getStatus(), "; ".join(str(note) for note in sched.getNotes())
# Add a record to the schedule table in the DB get its ID
cur.execute("INSERT INTO schedule (hall_id, month_id, created) VALUES (%s, %s, NOW()) RETURNING id;",
(resHallID, monthId))
# Load the query result
schedId = cur.fetchone()[0]
# Commit the changes to the DB
dbConn.commit()
logging.debug("Schedule ID: {}".format(schedId))
# Map the day.id to the numeric date value used for the scheduling algorithm
# Create the mapping object
days = {}
# Query the day table for all of the days within the given month.
cur.execute("SELECT EXTRACT(DAY FROM date), id FROM day WHERE month_id = %s;", (monthId,))
# Iterate through the results
for res in cur.fetchall():
# Map the date to the day.id
days[res[0]] = res[1]
# Create a dictionary to add up all of the averages
avgPtDict = {}
# Iterate through the schedule and generate parts of an insert query that will ultimately
# add the duties to the DB
dutyDayStr = ""
noDutyDayStr = ""
for d in sched:
# Check to see if there is at least one RA assigned for duty
# on this day.
if d.numberOnDuty() > 0:
# If there is at least one RA assigned for duty on this day,
# then iterate over all of the RAs assigned for duty on this
# day and add them to the dutyDayStr
for s in d.iterDutySlots():
# Retrieve the RA object that is assigned to this duty slot
r = s.getAssignment()
# Add the necessary information to the dutyDayStr
dutyDayStr += "({},{},{},{},{},{}),".format(resHallID, r.getId(), days[d.getDate()],
schedId, d.getPoints(), s.getFlag())
# Check to see if the RA has already been added to the dictionary
if r in avgPtDict.keys():
# If so, add the points to the dict
avgPtDict[r.getId()] += d.getPoints()
else:
# Otherwise, initialize the RA's entry with this day's points.
avgPtDict[r.getId()] = d.getPoints()
else:
# Otherwise, if there are no RAs assigned for duty on this day,
# then add the day to the noDutyDayStr
noDutyDayStr += "({},{},{},{}),".format(resHallID, days[d.getDate()], schedId, d.getPoints())
# Attempt to save the schedule to the DBgit s
try:
# If there were days added to the dutyDayStr
if dutyDayStr != "":
# Then insert all of the duties that were scheduled for the month into the DB
cur.execute("""
INSERT INTO duties (hall_id, ra_id, day_id, sched_id, point_val, flagged)
VALUES {};
""".format(dutyDayStr[:-1]))
# If there were days added to the noDutyDayStr
if noDutyDayStr != "":
# Then insert all of the blank duty values for days that were not scheduled
cur.execute("""
INSERT INTO duties (hall_id, day_id, sched_id, point_val) VALUES {};
""".format(noDutyDayStr[:-1]))
except psycopg2.IntegrityError:
# If we encounter an IntegrityError, then that means we attempted to insert a value
# into the DB that was already in there.
# Log the occurrence
logging.exception(
"IntegrityError encountered when attempting to save duties for Schedule: {}. Rolling back changes."
.format(schedId)
)
# Close the cursor
cur.close()
# Rollback the changes to the DB
dbConn.rollback()
# Notify the user of this issue.
return -2, "Unable to Generate Schedule. Please try again later."
# If autoExcAdj is set, then create adjust the excluded RAs' points
if autoExcAdj and len(eligibleRAStr) > 1:
logging.info("Adjusting Excluded RA Point Modifiers")
# Select all RAs in the given hall whose auth_level is below 3 (HD)
# that were not included in the eligibleRAs list
cur.execute("""
SELECT ra_id
FROM staff_membership
WHERE ra_id NOT IN %s
AND res_hall_id = %s""", (tuple(eligibleRAList), resHallID))
raAdjList = cur.fetchall()
# Calculate the average number of points earned for the month.
sum = 0
for ra in avgPtDict.keys():
sum += avgPtDict[ra]
# Calculate the average using floor division
avgPointGain = sum // len(avgPtDict.keys())
logging.info("Average Point Gain: {} for Schedule: {}".format(avgPointGain, schedId))
logging.debug("Number of excluded RAs: {}".format(len(raAdjList)))
# logging.debug(str(avgPtDict))
# Iterate through the excluded RAs and add point modifiers
# to them.
for ra in raAdjList:
addRAPointModifier(ra[0], resHallID, avgPointGain)
# Commit changes to the DB
dbConn.commit()
# Close the DB cursor
cur.close()
logging.info("Successfully Generated Schedule: {}".format(schedId))
# Notify the user of the successful schedule generation!
return 1, "Schedule generated successfully."
if __name__ == "__main__":
atexit.register(teardown)
startup()