Source code for courses

from collections import defaultdict
from itertools import repeat
from typing import Any, Dict, Iterable, List, Optional, Set, Union

import pandas as pd

from backend.events import AcademicalEvent

View = Union[List[str], Set[str], Dict[int, str]]


[docs]def generate_empty_dataframe(): index = ["code", "type", "id"] columns = ["week", "event"] activities = pd.DataFrame(columns=index + columns) activities.set_index(keys=index, inplace=True) return activities
[docs]class Course: """ A course aims to represent one or more courses. It contains its events and is represented with a name and a code. :param code: the code of the course :type code: str :param name: the full name of the course :type name: str :param weight: the weight attributed to the course :type weight: float :param activities: a structure of all the events indexed by code, type and id :type activities: Optional[pd.Dataframe] :Example: >>> course = Course('LMECA2732', 'Robotics') """ def __init__( self, code: str, name: str, weight: float = 1, activities: Optional[pd.DataFrame] = None, ): # A Course is defined by its code and its name self.code = code self.name = name self.weight = weight if activities is not None: self.activities = activities else: self.activities = generate_empty_dataframe() def __eq__(self, other: Union["Course", str]) -> bool: if isinstance(other, Course): return self.code == other.code elif isinstance(other, str): return self.code == other else: raise TypeError def __ne__(self, other: Union["Course", str]) -> bool: return not self.__eq__(other) def __str__(self) -> str: return self.code + ": " + self.name def __repr__(self) -> str: return str(self)
[docs] def add_activity(self, events: List[AcademicalEvent]): """ Adds an activity to the current course's activities. An activity is a set of events with the same id. :param events: list of academical events coming from the same activity :type events: List[AcademicalEvent] """ if len(events) == 0: return data = [[event.get_week(), event] for event in events] event_type = type(events[0]) id = events[0].id tuples = list(repeat((self.code, event_type, id), len(data))) index = pd.MultiIndex.from_tuples(tuples, names=self.activities.index.name) df = pd.DataFrame(data=data, columns=self.activities.columns, index=index) self.activities = pd.concat([self.activities, df])
[docs] def set_weights( self, percentage: float = 50, event_type: Optional[AcademicalEvent] = None ): """ Modifies this course's events weight. :param percentage: the "priority" required for this course in (0-100)%, default is 50% :type percentage: float :param event_type: if present, modify the weight of a certain type of event only :type event_type: Optional[AcademicalEvent] """ def f(event): event.set_weight(percentage / 10) if event_type is None: self.activities["event"].apply(f) else: level = self.activities.index.names.index(event_type) valid = self.activities.index.get_level_values(level) == event_type self.activities["event"][valid].apply(f)
[docs] def get_summary(self) -> Dict[str, Set[str]]: """ Returns the summary of all activities in the course. :return: dict of activity codes, ordered by activity type (CM, TP, etc.) :rtype: Dict[str, Set[str]] """ # TODO: Fix summary for external calendar summary = defaultdict(set) ids = self.activities.index.get_level_values("id").sort_values().unique() for id in ids: event_type, code = id.split(": ", maxsplit=1) summary[event_type].add(code) return summary
[docs] def get_activities( self, view: Optional[View] = None, reverse: bool = False ) -> pd.DataFrame: """ Returns a table of all activities that optionally match correct ids. :param view: if present, list of ids or dict {week_number : ids} :type view: Optional[View] :param reverse: if True, the activities in View will be removed :type reverse: bool :return: table containing all the activities and their events :rtype: pd.DataFrame """ if view is None: return self.activities elif isinstance(view, list) or isinstance(view, set): valid = self.activities.index.get_level_values("id").isin(view) if reverse: valid = ~valid return self.activities[valid] elif isinstance(view, dict): activities = [generate_empty_dataframe()] grp_weeks = self.activities.groupby("week") # weeks that are both in ids dict and in activities valid_weeks = set(view.keys()).intersection(grp_weeks.groups.keys()) for week in valid_weeks: week_data = grp_weeks.get_group(week) valid = week_data.index.get_level_values("id").isin(view[week]) if reverse: valid = ~valid activities.append(week_data[valid]) return pd.concat(activities) else: return None
[docs] def get_events(self, **kwargs) -> Iterable[AcademicalEvent]: """ Returns a list of events that optionally matches correct ids. :param kwargs: parameters that will be passed to :func:`Course.get_activities` :type kwargs: Any :return: list of events :rtype: Iterable[AcademicalEvent] """ return self.get_activities(**kwargs)["event"].values
[docs]def merge_courses( courses: Iterable[Course], code: str = "0000", name: str = "merged", weight: float = 1, views: Optional[Dict[str, View]] = None, **kwargs: Any, ) -> Course: """ Merges multiple courses into one. :param courses: multiple courses :type courses: Iterable[Courses] :param code: the new code :type code: str :param name: the new name :type name: str :param weight: the new weight :type weight: float :param views: map of views that will be passed to :func:`Course.get_activities` :type views: Optional[Dict[str, View]] :param kwargs: additional parameters that will be passed to :func:`Course.get_activities` :type kwargs: Any :return: the new course :rtype: Course """ if views: activities = pd.concat( course.get_activities(view=views[course.code], **kwargs) for course in courses ) else: activities = pd.concat(course.get_activities(**kwargs) for course in courses) return Course(code=code, name=name, weight=weight, activities=activities)