components/helper.py
'''
helper.py
Contains functions that process and/or manipulate data that are OUTSIDE the database
'''
## Prevent database.py from being imported twice
from sys import modules
try:
from components import database
except ImportError:
database = modules['components.database']
import datetime
import web
INDEX_FIRST_ELEM = 0
INDEX_SECOND_ELEM = 1
LENGTH_EMPTY = 0
MONTH_AUGUST = 8
# Currently, the system only has data for AY 16/17 and AY 17/18
NUMBER_OF_AY_IN_SYSTEM = 2
######################################################################################
# Functions that are related to AY and semester information
######################################################################################
def get_next_ay(ay):
'''
Return the AY that comes after the given AY
'''
ay = ay.split(' ')[1].split('/')
return 'AY ' + str(int(ay[0])+1) + '/' + str(int(ay[1])+1)
def get_number_of_ay_in_system():
'''
Return the number of AY in the system
'''
return NUMBER_OF_AY_IN_SYSTEM
def get_all_ay_sems(ay_count=NUMBER_OF_AY_IN_SYSTEM):
'''
Returns all the AY-Sems in the system
'''
current_ay = database.get_current_ay()
ay_sems_in_system = [current_ay+" Sem 1", current_ay+" Sem 2"]
target_ay = current_ay
for i in range(ay_count-1):
target_ay = get_next_ay(target_ay)
ay_sems_in_system.append(target_ay+" Sem 1")
ay_sems_in_system.append(target_ay+" Sem 2")
return ay_sems_in_system
def get_all_future_ay_sems(ay_count=NUMBER_OF_AY_IN_SYSTEM):
'''
Returns all the FUTURE AY-Sems in the system
'''
future_ay_sems_in_system = []
target_ay = database.get_current_ay()
for i in range(ay_count-1):
target_ay = get_next_ay(target_ay)
future_ay_sems_in_system.append(target_ay+" Sem 1")
future_ay_sems_in_system.append(target_ay+" Sem 2")
return future_ay_sems_in_system
def is_aysem_in_list(given_aysem, given_list):
'''
Returns true if given_aysem is found inside given_list.
Example:
given_list is a list of [('AY 16/17 Sem 1',), ('AY 16/17 Sem 2',)] structure.
'''
for aysem_tuple in given_list:
retrieved_aysem = aysem_tuple[0]
if given_aysem == retrieved_aysem:
return True
return False
def is_aysem_in_system(ay_sem):
'''
Returns the AY-Sem if given AY-Sem is found inside the system, otherwise return False
'''
ay_sems_in_system = get_all_ay_sems()
valid_aysem = False
for i in range(len(ay_sems_in_system)):
if ay_sem.upper() == ay_sems_in_system[i].upper():
valid_aysem = ay_sems_in_system[i]
break
return valid_aysem
def is_aysem_in_system_and_is_future(ay_sem):
'''
Returns the aysem if given aysem is found inside the system AND is a future AY-Sem,
otherwise return False
'''
future_ay_sems_in_system = get_all_future_ay_sems()
valid_future_aysem = False
for i in range(len(future_ay_sems_in_system)):
if ay_sem.upper() == future_ay_sems_in_system[i].upper():
valid_future_aysem = future_ay_sems_in_system[i]
break
return valid_future_aysem
def get_current_ay_sem():
'''
Retrieves the current AY-Semester as a string.
e.g. "AY 16/17 Sem 2"
'''
current_date = get_current_date()
current_year = current_date.year
current_month = current_date.month
return get_ay_sem(current_year, current_month)
def get_ay_sem(year, month):
'''
Retrieves AY-Semester as a string given the month and year values.
'''
if month < MONTH_AUGUST:
year_where_ay_starts = year - 1
acad_year = get_formatted_academic_year(year_where_ay_starts)
semester = 2
else:
acad_year = get_formatted_academic_year(year)
semester = 1
return "AY " + acad_year + " Sem " + str(semester)
def get_current_date():
'''
Returns today's date as a Date Object.
'''
return datetime.date.today()
def get_formatted_academic_year(year_where_ay_starts):
'''
Given the year which the Academic Year started in, formats and returns
the Academic Year (AY) as a string.
E.g. if AY started in 2015, this function returns "15/16"
'''
truncated_year = str(year_where_ay_starts)[-2:] # Retrieves last 2 digits
year_where_ay_ends = int(truncated_year) + 1
ay_formatted = truncated_year + "/" + str(year_where_ay_ends)
return ay_formatted
######################################################################################
# Functions that are related to mounting and/or quota information
######################################################################################
def get_quota_in_aysem(ay_sem, aysem_quota_merged_list):
'''
This is a helper function.
Retrieves the correct quota from ay_sem listed inside
aysem_quota_merged_list parameter.
'''
for aysem_quota_pair in aysem_quota_merged_list:
aysem_in_pair = aysem_quota_pair[0]
if ay_sem == aysem_in_pair:
quota_in_pair = aysem_quota_pair[1]
return quota_in_pair
return None # quota not found in list
######################################################################################
# Functions that are related to general student enrollment information
######################################################################################
def append_missing_year_of_study(initial_table):
'''
Helper function to append missing years of study to the
given initial table.
initial_table given in lists of (year, number of students)
pair.
e.g. If year 5 is missing from table, appends (5,0) to table
and returns the table
'''
MAX_POSSIBLE_YEAR = 6
for index in range(0, MAX_POSSIBLE_YEAR):
year = index + 1
year_exists_in_table = False
for year_count_pair in initial_table:
req_year = year_count_pair[0]
if req_year == year:
year_exists_in_table = True
break
if not year_exists_in_table:
initial_table.append((year, 0))
return initial_table
######################################################################################
# Functions that are related to prerequisite or preclusion information
######################################################################################
def get_prerequisite_as_string(module_code):
'''
Returns a string of pre-requisites of specified module_code
'''
prereq_list = get_prerequisite_sorted_list(module_code)
prereq_string = convert_list_of_prereqs_to_string(prereq_list)
return prereq_string
def get_prerequisite_units(module_code):
'''
Retrieves all the prerequisite of given module_code as a list
of lists. The list will be a list of units having the "and" relationship.
Each unit is a list of modules having the "or" relationship.
Example: ("CS1010" or "CS1231") and "MA1100" will return
[["CS1010", "CS1231"], ["MA1100"]]
'''
prereq_list = get_prerequisite_sorted_list(module_code)
if len(prereq_list) == LENGTH_EMPTY:
return list()
else:
final_list = list()
first_unit = prereq_list[INDEX_FIRST_ELEM]
previous_index = first_unit[INDEX_FIRST_ELEM]
module_to_add = first_unit[INDEX_SECOND_ELEM]
current_list_of_module = [module_to_add]
for index in range(INDEX_SECOND_ELEM, len(prereq_list)):
current_index = prereq_list[index][INDEX_FIRST_ELEM]
current_module = prereq_list[index][INDEX_SECOND_ELEM]
if current_index == previous_index:
current_list_of_module.append(current_module)
else:
final_list.append(current_list_of_module)
previous_index = current_index
current_list_of_module = [current_module]
final_list.append(current_list_of_module)
return final_list
def get_prerequisite_sorted_list(module_code):
'''
Get the prerequisites of the given module_code as a list of pairs,
where each pair is (index, prereq module) from the database, the list is
sorted in ascending order for the index values.
'''
prerequisites = database.get_prerequisite(module_code)
prereq_list = convert_to_list(prerequisites)
# sort list of lists based on index (which is the first elem of each row)
prereq_list.sort(key=lambda row: row[INDEX_FIRST_ELEM])
return prereq_list
def get_preclusion_list(module_code):
'''
Get the preclusions of the given module_code as a list of modules.
'''
preclusions = database.get_preclusion(module_code)
preclude_list = convert_to_list(preclusions)
processed_list = [preclude[INDEX_FIRST_ELEM] for preclude in preclude_list]
return processed_list
def get_preclusion_as_string(module_code):
'''
Returns a string of preclusions of specified module_code
'''
processed_list = get_preclusion_list(module_code)
preclude_string = ", ".join(processed_list)
return preclude_string
def get_preclusion_units(module_code):
'''
Retrieves all the preclusions of given module_code as a list
of modules.
'''
preclusion_list = get_preclusion_list(module_code)
return preclusion_list
def convert_list_of_prereqs_to_string(prereq_list):
'''
Converts given list of lists (prereq_list) into string form of prereqs.
Pre-condition: Given list must have the rows' first index sorted.
Example: [['0', 'CS1010'], ['0', 'CS1231'], ['1', 'CS2105']] will yield
the string (CS1010 or CS1231) and CS2105.
Same index elements have an "OR" relationship, different index elements
have an "AND" relationship.
'''
number_of_prereq = len(prereq_list)
if number_of_prereq == LENGTH_EMPTY:
return ""
required_string = ""
temp_list_for_or = list()
previous_index = None
for prereq_with_index in prereq_list:
current_index = prereq_with_index[INDEX_FIRST_ELEM]
current_prereq = prereq_with_index[INDEX_SECOND_ELEM]
if previous_index is None:
previous_index = current_index
temp_list_for_or.append(current_prereq)
else:
if previous_index == current_index:
temp_list_for_or.append(current_prereq)
else:
prereq_of_or_string = convert_list_prereq_to_or_string(temp_list_for_or)
required_string = process_and_relation_prereq(required_string,
prereq_of_or_string)
previous_index = current_index
temp_list_for_or = [current_prereq]
if len(required_string) == LENGTH_EMPTY:
# there is no 'and' relation
prereq_of_or_string = convert_list_prereq_to_or_string(temp_list_for_or,
False)
required_string = prereq_of_or_string
else:
# there is 'and' relation to process
prereq_of_or_string = convert_list_prereq_to_or_string(temp_list_for_or)
required_string = process_and_relation_prereq(required_string,
prereq_of_or_string)
return required_string
def convert_list_prereq_to_or_string(temp_list, to_add_brackets=True):
'''
Converts all elements in temp_list to a string separated by "or"
'''
number_of_prereq = len(temp_list)
if number_of_prereq == 1:
return temp_list[INDEX_FIRST_ELEM]
else:
# more than 1 prereq
required_string = " or ".join(temp_list)
if to_add_brackets:
required_string_with_brackets = "(" + required_string + ")"
return required_string_with_brackets
else:
return required_string
def process_and_relation_prereq(existing_string, prereq_of_or_string):
'''
Appends the prereq_of_or_string into existing_string by building
"and" relations between existing prereqs in existing_string
with those in prereq_of_or_string
'''
existing_string_length = len(existing_string)
if existing_string_length == LENGTH_EMPTY:
return prereq_of_or_string
else:
and_string = " and "
required_string = existing_string + and_string + prereq_of_or_string
return required_string
######################################################################################
# Function that validates input that are used to query or modify the database
######################################################################################
def validate_input(input_data, input_types, is_future=False,
aysem_specific=True, attr_required=True, show_404=True):
'''
Validates that the GET input data (in the URL) is valid.
input_types indicate the list of data types to validate.
e.g. if GET input contains 'code' and 'aysem', then input_types = ["code", "aysem"]
An input is considered valid if:
1) The value was specified and
2) The value exists in the system
If any input is invalid, return 404 page (by default).
Depending on the circumstances, optional arguments may be passed into the function:
is_future:
Set to True if want to ensure that AY-Sem is in a future AY
(if AY-Sem is not required at all, then ignore this argument)
aysem_specific:
Set to True if the AY-Sem attribute is mandatory, False if it is optional
(if AY-Sem is not required at all, then ignore this argument)
attr_required:
Set to True if at least one attribute is required
Set to False if it is acceptable to have no input data
show_404:
Set to False if don't want to return 404 page (function will return False instead)
'''
from app import RENDER
if attr_required is False and len(input_data) == 0:
return input_data
for input_type in input_types:
if input_type == "code":
try:
module_code = input_data.code
except AttributeError:
if show_404:
error = RENDER.notfound('Module code is not specified')
raise web.notfound(error)
else:
return False
module_exist = database.is_existing_module(module_code.upper())
if not module_exist:
if show_404:
error = RENDER.notfound('Module code "' + module_code +\
'" does not exist in our system')
raise web.notfound(error)
else:
return False
else:
input_data.code = module_code.upper()
elif input_type == "aysem":
try:
ay_sem = input_data.aysem
if ay_sem == "":
raise AttributeError
except AttributeError:
if aysem_specific:
if show_404:
error = RENDER.notfound('AY-Semester is not specified')
raise web.notfound(error)
else:
return False
else:
continue
if is_future:
valid_aysem = is_aysem_in_system_and_is_future(ay_sem)
else:
valid_aysem = is_aysem_in_system(ay_sem)
if not valid_aysem:
if is_future:
error = RENDER.notfound('AY-Semester "' + ay_sem +\
'" does not exist in our system,' +\
' or is not in a future AY')
else:
error = RENDER.notfound('AY-Semester "' + ay_sem +\
'" does not exist in our system')
if show_404:
raise web.notfound(error)
else:
return False
else:
input_data.aysem = valid_aysem
elif input_type == "modify_type" or input_type == "restore_type":
try:
if input_type == "modify_type":
info_type = input_data.modifyType
else:
info_type = input_data.restoreType
except AttributeError:
if input_type == "modify_type":
error = RENDER.notfound('Modify type is not specified')
else:
error = RENDER.notfound('Restore type is not specified')
raise web.notfound(error)
valid_info_type = info_type.upper() == "QUOTA" or \
info_type.upper() == "MOUNTING" or \
info_type.upper() == "MODULEDETAILS"
if not valid_info_type:
if input_type == "modify_type":
error = RENDER.notfound('Modify type "' + info_type +\
'" is not recognised by the system')
else:
error = RENDER.notfound('Restore type "' + info_type +\
'" is not recognised by the system')
raise web.notfound(error)
elif input_type == "moduleAandB":
try:
moduleA_code = input_data.moduleA
is_moduleA_specified = True
except AttributeError:
is_moduleA_specified = False
try:
moduleB_code = input_data.moduleB
is_moduleB_specified = True
except AttributeError:
is_moduleB_specified = False
if (is_moduleA_specified and not is_moduleB_specified) or \
(not is_moduleA_specified and is_moduleB_specified):
error = RENDER.notfound("1 out of 2 module codes is not specified")
raise web.notfound(error)
elif not is_moduleA_specified and not is_moduleB_specified:
continue
module_exist = database.is_existing_module(moduleA_code.upper())
if not module_exist:
error = RENDER.notfound('Module code "' + moduleA_code +\
'" does not exist in our system')
raise web.notfound(error)
else:
input_data.moduleA = moduleA_code.upper()
module_exist = database.is_existing_module(moduleB_code.upper())
if not module_exist:
error = RENDER.notfound('Module code "' + moduleB_code +\
'" does not exist in our system')
raise web.notfound(error)
else:
input_data.moduleB = moduleB_code.upper()
return input_data
######################################################################################
# General helper functions that are used by other functions
######################################################################################
def convert_to_list(table):
'''
Converts a list of tuples to a list of lists.
'''
converted_table = list()
for row in table:
conveted_list = list(row)
converted_table.append(conveted_list)
return converted_table
def replace_null_with_dash(table):
'''
Changes all the NULL values found in the table to '-'
This function supports a 2D table.
'''
table = convert_to_list(table)
for row in table:
row_length = len(row)
for col in range(row_length):
if row[col] is None:
row[col] = '-'
return table
def convert_2D_to_1D_list(table):
'''
Converts a list of lists into a list, where
all elements in the 2D table is converted into
a 1D list.
'''
new_list = list()
for row in table:
for item in row:
new_list.append(item)
return new_list
def replace_empty_quota_with_symbols(mounting_plan):
'''
Replace all quota values with '-' (if not mounted)
or '?' (if mounted)
'''
SEM1_MOUNTING_INDEX = 2
SEM2_MOUNTING_INDEX = 3
SEM1_QUOTA_INDEX = 4
SEM2_QUOTA_INDEX = 5
mounting_plan = convert_to_list(mounting_plan)
for i in range(len(mounting_plan)):
subplan = mounting_plan[i]
sem1_quota = subplan[SEM1_QUOTA_INDEX]
if sem1_quota is None or sem1_quota == -1:
sem1_mounting = subplan[SEM1_MOUNTING_INDEX]
if sem1_mounting == 1:
mounting_plan[i][SEM1_QUOTA_INDEX] = '?'
else:
mounting_plan[i][SEM1_QUOTA_INDEX] = '-'
sem2_quota = subplan[SEM2_QUOTA_INDEX]
if sem2_quota is None or sem2_quota == -1:
sem2_mounting = subplan[SEM2_MOUNTING_INDEX]
if sem2_mounting == 1:
mounting_plan[i][SEM2_QUOTA_INDEX] = '?'
else:
mounting_plan[i][SEM2_QUOTA_INDEX] = '-'
return mounting_plan