예제 #1
0
def get_blocklists() -> Dict[str, List[str]]:
    """

    Get blocklist data from both TAs blocklisting students and
    students blocklisting TAs

    :returns: a dictionary of ta logins as the keys and lists of
              students blocklisted as the values
    :rtype: Dict[str, List[str]]

    """
    bl1 = os.path.join(BASE_PATH, 'ta/t-s-blocklist.json')
    bl2 = os.path.join(BASE_PATH, 'hta/s-t-blocklist.json')
    with locked_file(bl1) as f:
        ts_bl = json.load(f)

    with locked_file(bl2) as f:
        st_bl = json.load(f)

    for ta in st_bl:
        if ta not in ts_bl:
            ts_bl[ta] = []

        ts_bl[ta].extend(st_bl[ta])

    for ta in ts_bl:
        ts_bl[ta] = list(set(ts_bl[ta]))

    return ts_bl
예제 #2
0
def get_lab_data() -> Tuple[Dict[str, Set[str]], Set[str]]:
    """

    collect lab information for this course

    :returns: dictionary of login -> attended labs set and a set of
              all the labs for the course
    :rtype: Tuple[Dict[str, Set[str]], Set[str]]

    """
    lab_data: Dict[str, Set[str]]
    lab_path = pjoin(lab_base, 'attendance.json')
    with locked_file(lab_path) as f:
        attendance_data = json.load(f)

    all_labs = set(attendance_data.keys())

    lab_data = defaultdict(set)
    for lab in attendance_data:
        for student in attendance_data[lab]:
            lab_data[student].add(lab)

    with locked_file(lab_excp_path) as f:
        lines = f.read().strip().split('\n')

    for line in lines:
        login, lab = line.split(' ')
        lab_data[login].add(lab)

    return lab_data, all_labs
예제 #3
0
    def write_line(self, **kwargs) -> None:
        """

        update the log file for this handin

        :param \*\*kwargs:

                         - grader (str): login of the handin grader

                         - flag_reason (str): explanation why the handin is
                           flagged

                         - complete (bool): whether or not handin is complete


        """
        with locked_file(self.question.log_filepath) as f:
            data: Log = json.load(f)

        for handin in data:
            if handin['id'] == self.id:
                if 'grader' in kwargs:
                    handin['grader'] = kwargs['grader']

                if 'flag_reason' in kwargs:
                    handin['flag_reason'] = kwargs['flag_reason']

                if 'complete' in kwargs:
                    handin['complete'] = kwargs['complete']

                break

        with locked_file(self.question.log_filepath, 'w') as f:
            json.dump(data, f, indent=2, sort_keys=True)
예제 #4
0
    def download(self, base_path: str, login: str) -> None:
        fpath = os.path.join(base_path, self.fname)
        if self.gf is None:
            # no submission
            self.file_path = None
            self.downloaded = True
            return

        expect_ext = os.path.splitext(self.fname)[1]
        actual_ext = os.path.splitext(self.gf.name)[1]
        # student uploaded incorrect filetype (name irrelevant)
        if expect_ext != actual_ext:
            if expect_ext == '.zip':
                # this is a hack to make things not break
                # @fix
                fpath += actual_ext
                self.fname += actual_ext

            with locked_file('filename_error_template.html', 'r') as f:
                if not actual_ext:
                    actual_ext = 'no extension'
                self.warning = f.read().format(exp_ext=expect_ext,
                                               sub_ext=actual_ext)
        else:
            self.warning = None

        self.gf.download(fpath)

        self.file_path = fpath
        self.downloaded = True
예제 #5
0
    def get_snippet(self):
        lines_per_file = 5  # how many lines to print per snippet
        if not self.downloaded:
            e = (f'File {self.fname} must be downloaded before '
                 f'generating snippet')
            raise ValueError(e)

        if not self.completed:
            return (make_span('Not submitted.', 'red'), '')
        elif not self.make_snippet:
            first_cell = make_span('Submitted.', 'green')
            return first_cell, 'Snippet Unavailable'
        else:
            with locked_file(self.file_path) as dl_f:
                try:
                    lines = dl_f.read().strip().split('\n')
                except UnicodeDecodeError:
                    return (make_span('Submitted',
                                      'green'), 'File could not be read')

            if lines == [] or lines == ['']:
                first_cell = make_span(f'Empty file.', 'orange')
                return (first_cell, '')

            if len(lines) > lines_per_file:
                snippet = lines[:lines_per_file]
                snippet.append('...')
            elif len(lines) == lines_per_file:
                snippet = lines[:]
            else:
                snippet = lines[:len(lines)]

            snippet = '<br>'.join(line.strip() for line in snippet)
            first_cell = make_span(f'Submitted.', 'green')
            return (first_cell, snippet)
예제 #6
0
    def load(self):
        """

        Loads the assignment (checks all paths are proper and
        loads all assignment questions)
        :raises AssertionError: invalid assignment

        """
        # checking that assignment has correct paths
        n = self.full_name
        assert pexists(self.log_path), \
            f'started assignment "{n}" with no log directory'
        assert pexists(self.rubric_path), \
            f'started assignment "{n}" with no rubric directory'
        assert pexists(self.grade_path), \
            f'started assignment "{n}" with no grade directory'
        assert pexists(self.blocklist_path), \
            f'started assignment "{n}" with no blocklist file'
        assert pexists(self.files_path), \
            f'started assignment "{n}" with no student code directory'
        
        if not self.anonymous:
            with locked_file(self.anon_path) as f:
                data: Dict[str, int] = json.load(f)

            self._login_to_id_map: Dict[str, int] = data
            self._id_to_login_map: Dict[int, str] = {data[k]: k for k in data}

        self._load_questions()
        self.loaded = True
        return self
예제 #7
0
def confirmed_responses(filename=CONFIG.handin.log_path):
    with locked_file(filename, 'r') as f:
        lines = f.read().strip().split('\n')
        if not lines or lines == ['']:
            return []
        else:
            return set(map(int, lines))
예제 #8
0
    def __init__(self, uname: str) -> None:
        """

        make a new User

        :param uname: CS login of user
        :type uname: str

        """
        with locked_file(ta_path) as t, locked_file(hta_path) as h:
            tas = t.read().strip().split('\n')
            htas = h.read().strip().split('\n')

        self.uname = uname
        self.ta = uname in tas
        self.hta = uname in htas
예제 #9
0
    def login_to_id(self, login: str) -> int:
        """

        Get anonymous ID of student by login

        :param login: Student CS login
        :type login: str
        :returns: Anonymous ID for student by login
        :rtype: int
        :raises: ValueError: Student has no anonymous ID for this assignment

        """
        if self.anonymous:
            raise ValueError('Cannot get login on anonymous assignment')

        #  first try using cached info
        if login in self._login_to_id_map:
            return self._login_to_id_map[login]

        #  next try reloading anonymization information
        with locked_file(self.anon_path) as f:
            data: Dict[str, int] = json.load(f)

        try:
            self._login_to_id_map[login] = data[login]
            self._id_to_login_map[data[login]] = login
            return data[login]
        except KeyError:
            #  then fail
            raise ValueError(f'login {login} does not exist in map for {self}')
예제 #10
0
    def id_to_login(self, ident: int) -> str:
        """

        Get anonymous ID of student by login

        :param ident: Student anonymous ID for this assignment
        :type ident: int
        :returns: Login of student with ident id
        :rtype: str
        :raises: ValueError: No student with anon ID for this assignment

        """
        if self.anonymous:
            raise ValueError('Cannot get login on anonymous assignment')

        #  first try using cached info
        if ident in self._id_to_login_map:
            return self._id_to_login_map[ident]

        #  next try reloading anonymization information
        with locked_file(self.anon_path) as f:
            data: Dict[str, int] = json.load(f)

        for login in data:
            if data[login] == ident:
                self._id_to_login_map[ident] = login
                self._login_to_id_map[login] = ident
                return login

        #  then fail
        raise ValueError(f'id {ident} does not exist in map for {self}')
예제 #11
0
def get_used_late_days(login: str) -> List[str]:
    with locked_file(late_days_path) as f:
        data = json.load(f)

    if login in data:
        return data[login]
    else:
        return []
예제 #12
0
def send_summaries(resummarize: Optional[bool] = None) -> None:
    """

    send course grade summaries

    :param resummarize: whether or not to regenerate grade summaries, or None
                        to have program prompt whether or not to regenerate
    :type resummarize: Optional[bool], optional

    """
    if resummarize is None:
        print('Regenerate summaries? [y/n]')
        resp = input('> ').lower()
        if resp == 'y':
            resummarize = True
        elif resp == 'n':
            resummarize = False
        else:
            send_summaries()

    if resummarize:
        generate_grade_summaries(write=True)

    yag = yagmail.SMTP(CONFIG.email_from)
    full_students = load_students()
    login_to_email = {line[0]: line[1] for line in full_students}
    students = list(map(lambda line: line[0], full_students))
    with locked_file(ta_group_path) as f:
        tas = list(map(str.strip, f.read().strip().split('\n')))

    for sum_file in os.listdir(sum_path):
        path = pjoin(sum_path, sum_file)
        login = os.path.splitext(sum_file)[0]
        print(f'{login!r}')
        if login not in students or login in tas:
            print(f'skipping login {login}')
            continue

        with locked_file(path) as f:
            contents = f'<pre>{f.read()}</pre>'

        em = login_to_email[login]
        to = CONFIG.test_mode_emails_to if CONFIG.test_mode else em

        print(f'Sending {login} summary to {to}')
        yag.send(to=to, subject='Aggregated grade report', contents=[contents])
예제 #13
0
    def __set_partner_data(self):
        ''' sets project partner data for this student
        (puts into project_name.json file) '''
        gdata = self.asgn['group_data']
        if gdata is None or gdata['partner_col'] is None:
            raise ValueError('Cannot call set_partner_data on assignment '
                             'with no partner column')

        rndx = col_str_to_num(gdata['partner_col'])
        partner_data = set(self.row[rndx - 1].split(', '))
        partner_data.add(self.login)

        proj_path = os.path.join(proj_base, f'{gdata["multi_part_name"]}.json')
        if not os.path.exists(proj_path):
            with locked_file(proj_path, 'w') as f:
                json.dump([], f, indent=2, sort_keys=True)

        with locked_file(proj_path) as f:
            groups = json.load(f)

        # checking this is a valid group
        in_file = False
        for group in groups:
            if set(group) == partner_data:
                # this group has already submitted something
                in_file = True
                # but keep checking to make sure no students in this group are
                # in another group
                continue

            for student in partner_data:
                if student in group:
                    e = f'Student {student} signed up for multiple groups.'
                    e += ' Needs fixing (in /ta/grading/data/projects)'
                    print(e)

        if len(partner_data) != 2:
            print(f'Group {partner_data} with != 2 members')

        if not in_file:
            groups.append(list(partner_data))
            with locked_file(proj_path, 'w') as f:
                json.dump(groups, f, indent=2, sort_keys=True)
예제 #14
0
def generate_gradebook(path: Optional[str] = None) -> None:
    """

    generates gradebook for the course

    :param path: path to put gradebook in; puts in /hta/gradebook.tsv if None.
                defaults to None
    :type path: Optional[str], optional

    """
    if path is None:
        path = pjoin(BASE_PATH, 'hta', 'gradebook.tsv')

    if os.path.splitext(path)[1] != '.tsv':
        print('Warning: Output format is tsv, but writing to non-tsv file')

    data = get_full_grade_dict()
    categories = set()
    gradebook: Dict[str, dict] = defaultdict(lambda: {})
    for student in data.keys():
        s_data = data[student]
        for asgn in s_data.keys():
            grade = s_data[asgn]
            if isinstance(grade, (int, str, float)):
                gradebook[asgn][student] = grade
            else:
                cats = grade.keys()
                for cat in cats:
                    descr = f'{asgn} {cat}'
                    categories.add(descr)
                    gradebook[descr][student] = grade[cat]

    students = sorted(data.keys())
    sorted_cats = sorted(categories)
    book = [['Student', *sorted_cats]]

    for student in students:
        summary = [student]
        for descr in sorted_cats:
            if student in gradebook[descr]:
                book_grade = gradebook[descr][student]
            else:
                book_grade = '(No data)'

            summary.append(book_grade)

        book.append(summary)

    S = ''
    for line in book:
        line_str = '\t'.join(line)
        S += f'{line_str}\n'

    with locked_file(path, 'w') as f:
        f.write(S)
예제 #15
0
    def write_grade(self, rubric: Rubric) -> None:
        """

        write the grade rubric

        :param rubric: new rubric to write into grade file
        :type rubric: Rubric

        """
        loaded_rubric_check(rubric)
        with locked_file(self.grade_path, 'w') as f:
            json.dump(rubric, f, indent=2, sort_keys=True)
예제 #16
0
    def copy_rubric(self) -> Rubric:
        """

        return the JSON rubric of this question following the spec from
        custom_types.py

        :returns: base rubric for this question
        :rtype: Rubric

        """
        with locked_file(self.rubric_filepath) as f:
            return json.load(f)
예제 #17
0
    def get_rubric(self) -> Rubric:
        """

        get the rubric for this handin only; must be extracted

        :returns: rubric of this handin
        :rtype: Rubric

        """
        with locked_file(self.grade_path) as f:
            d = json.load(f)
            return d
예제 #18
0
    def rewrite_rubric(self, rubric: Rubric) -> None:
        """

        given a new rubric, check the rubric and write it into the questions
        rubric file.

        :param rubric: The updated base rubric for this question.
        :type rubric: Rubric

        """
        loaded_rubric_check(rubric)
        with locked_file(self.rubric_filepath, 'w') as f:
            json.dump(rubric, f, indent=2, sort_keys=True)
예제 #19
0
    def blocklisted_by(self, ta: str) -> bool:
        """

        determines if a TA can grade this handin

        :param ta: login of the TA to check
        :type ta: str
        :returns: whether the handin's student is blocklisted by TA
        :rtype: bool

        """
        bl_path = self.question.assignment.blocklist_path
        with locked_file(bl_path) as f:
            data = json.load(f)

        return ta in data and self.id in data[ta]
예제 #20
0
def load_students():
    path = os.path.join(BASE_PATH, 'ta/groups/students.csv')
    students = []
    with locked_file(path, 'r') as f:
        lines = map(str.strip, f.read().strip().split('\n'))
        for line in lines:
            row = line.split(',')
            if len(row) != 3:
                e = 'row %s in students.txt invalid, lines=%s'
                raise IndexError(e % (row, lines))
            username = row[0]
            email = row[1]
            students.append((email, username))

    students.append(('*****@*****.**', 'csci0111', 'HTA Account'))

    return students
예제 #21
0
def drive_api() -> Resource:
    with locked_file(ref_tok_path) as f:
        ref_tok = f.read().strip()

    credentials = Credentials(
        None,
        refresh_token=ref_tok,
        token_uri="https://accounts.google.com/o/oauth2/token",
        client_id=client_id,
        client_secret=client_secret
    )
    try:
        drive = build('drive', 'v3', credentials=credentials)
    except httplib2.ServerNotFoundError:
        print('httplib2 exception in drive build')
        sys.exit(1)
    except Exception as e:
        print(f'{e} exception in drive build')
        sys.exit(1)

    return drive
예제 #22
0
    def load_handins(self) -> None:
        """

        set self.handins based on the questions log file

        """
        with locked_file(self.log_filepath) as f:
            data = json.load(f)

        handins = []
        for raw_handin in data:
            handins.append(Handin(self, raw_handin.pop('id'), **raw_handin))

        self.handins: List['Handin'] = handins
        self.handin_count: int = len(self.handins)

        self.grading_started = self.assignment.started
        self.has_incomplete = any(map(lambda h: not h.complete, self.handins))
        self.has_flagged = any(map(lambda h: h.flagged, self.handins))

        # how many handins for this question have been completed
        self.completed_count = len([x for x in self.handins if x.complete])
        self.flagged_count = len([x for x in self.handins if x.flagged])
예제 #23
0
def get_student_grade_dict(login: str) -> Dict[str, Grade]:
    """

    given a login, collect grade information for that student

    :param login: CS login of student for whom to collect grade info
    :type login: str
    :returns: a dictionary of assignment mini_name -> grade for that assignment
    :rtype: Dict[str, Grade]
    :raises ValueError: no grades for this student

    """
    full_grade: Dict[str, Grade] = {}
    sdir = pjoin(grade_dir, login)
    if not os.path.exists(sdir):
        raise ValueError(f'Trying to get nonexistent grade_dict for {login}')

    for asgn in completed_asgn_names:
        d = asgn.replace(' ', '').lower()
        individual_path = pjoin(sdir, d)
        if not os.path.isdir(individual_path):
            full_grade[asgn] = {'Grade': 'No handin'}
            continue

        grade_path = pjoin(individual_path, 'grade.json')

        if not os.path.exists(grade_path):
            print(f'Nonexistent grade in {individual_path}, continuing...')
            continue

        with locked_file(grade_path) as f:
            grade: Grade = json.load(f)

        full_grade[asgn] = grade

    return full_grade
예제 #24
0
 def _add_to_log(self) -> None:
     with locked_file(log_path, 'a') as f:
         f.write(f'{self.ident}\n')
예제 #25
0
from hta_classes import HTA_Assignment
from handin_helpers import email_to_login

regrade_log = os.path.join(BASE_PATH, 'hta/grading/regrading/regrade_log.json')

# quiet mode just overwrites print function, it's terrible but it works
# and logging is annoying
if len(sys.argv) > 1 and (sys.argv[1] == '--quiet' or sys.argv[1] == '-q'):
    def print(*args, **kwargs):
        pass

os.umask(0o007)  # set file permissions
yag = yagmail.SMTP(CONFIG.email_from)

settings_path = pjoin(BASE_PATH, 'hta/grading/regrading/settings.json')
with locked_file(settings_path) as f:
    settings = json.load(f)

ssid = settings['request-ssid']
instruction_link = settings['regrade-instructions']


class FormError(Exception):
    pass


def handle(row: List[str]) -> None:
    # figure out who the student is
    student_email = row[1]
    try:
        login = email_to_login(student_email)
예제 #26
0
    late_with_ext = auto()  # in any non-on-time period, but with an extension
    late_exp_ext = auto()  # has extension but submitted after it expired
    using_late_day = auto()  # used a late day on this handin


on_time_stats = [
    HState.on_time, HState.first_deadline_buffer, HState.late_with_ext,
    HState.using_late_day
]
kinda_late_stats = [
    HState.kinda_late, HState.kinda_late_buffer, HState.kinda_late_exp_ext
]
late_stats = [HState.late, HState.late_exp_ext]

data_file = os.path.join(BASE_PATH, 'ta/assignments.json')
with locked_file(data_file) as f:
    data = json.load(f)

check_assignments(data)

log_path = HCONFIG.get_sub_log(CONFIG.test_mode)
add_sub_path = os.path.join(BASE_PATH, 'hta/grading/add-student-submission')
proj_base = os.path.join(BASE_PATH, 'ta/grading/data/projects')


class Question:
    def __init__(self, question: dict, row: List[str]) -> None:
        self.downloaded: bool = False
        self.q_data: dict = question
        col = self.q_data['col']
        ind = col_str_to_num(col) - 1
예제 #27
0
from helpers import locked_file, CONFIG, BASE_PATH
from googleapi import sheets_api
from hta_classes import HTA_Assignment
from handin_helpers import email_to_login

# quiet mode just overwrites print function, it's terrible but it works
# and logging is annoying
if len(sys.argv) > 1 and (sys.argv[1] == '--quiet' or sys.argv[1] == '-q'):
    def print(*args, **kwargs):
        pass

os.umask(0o007)  # set file permissions
yag = yagmail.SMTP(CONFIG.email_from)

data_file = pjoin(BASE_PATH, 'ta/assignments.json')
with locked_file(data_file) as f:
    data = json.load(f)

settings_path = pjoin(BASE_PATH, 'hta/grading/regrading/settings.json')
with locked_file(settings_path) as f:
    settings = json.load(f)

ssid = settings['request-ssid']
instruction_link = settings['regrade-instructions']


class FormError(Exception):
    pass


def handle(row: List[str]) -> None:
예제 #28
0
# WARNING:
# summary script currently assumes:
# drill 18 is not complete yet
# only gets drills 1-18
# canvas data in hta folder called canvas-grades-11-17.csv
# drill 14 was done through grading app

BASE_PATH = CONFIG.base_path
grade_dir = pjoin(BASE_PATH, 'hta', 'grades')
sum_path = pjoin(BASE_PATH, 'hta', 'summaries')
lab_base = pjoin(BASE_PATH, 'ta', 'grading', 'data', 'labs')
data_path = pjoin(BASE_PATH, 'ta', 'assignments.json')
drill_path = pjoin(BASE_PATH, 'hta/canvas-grades-11-17.csv')
lab_excp_path = pjoin(BASE_PATH, 'ta/grading/data/labs/exceptions.txt')
ta_group_path = pjoin(BASE_PATH, 'hta/groups/tas.txt')
with locked_file(data_path) as f:
    asgn_data: Dict[str, AssignmentData] = json.load(f)['assignments']

completed_asgn_names: List[str] = []
for asgn in asgn_data:
    if asgn_data[asgn]['grading_completed']:
        completed_asgn_names.append(asgn)


def get_full_grade_dict(
        students: Optional[List[str]] = None) -> Dict[str, Dict[str, Grade]]:
    """

    get a dictionary of student -> dictionary of grades

    :param students: students for whom to collect grade info. if None, data
예제 #29
0
def generate_grade_summaries(write: bool = False) -> Dict[str, str]:
    """

    generate grade summaries for the course

    :param write: whether or not to write the summaries into files in
                  /hta/summaries, defaults to False
    :type write: bool, optional
    :returns: dictionary of login -> grade summary strings
    :rtype: Dict[str, str]
    """
    print('Gathering grading app grade info...')
    data = get_full_grade_dict()
    print('Gathering lab info...')
    lab_data, all_labs = get_lab_data()
    print('Gathering drill info...')
    drill_data = get_drill_data()
    print('Generating summaries...')
    summs = {}
    for student in data:
        S = f'Aggregated grade summary for {student}\n\n'
        student_data = data[student]
        for (k, v) in sorted(student_data.items()):
            if k == 'Drill 14':
                if v == {'Grade': 'Complete'}:
                    drill_data[student][0].add('drill14')
                else:
                    drill_data[student][1].add('drill14')

                continue
            S += f'{k}\n'
            mlen = max(map(len, v.keys())) + 1
            if isinstance(v, (int, str, float)):
                S += f'{" " * 4}Grade: {v}'
            else:
                cats = v.keys()
                for cat in v:
                    grade = v[cat]
                    spaces = (mlen - len(cat))
                    S += f"{' ' * 4}{cat}{' ' * spaces}: {grade}\n"

            S += '\n'

        slabs = lab_data[student]
        unattended = all_labs.difference(slabs)
        nlabs = len(all_labs)
        lab_sum = f'{len(slabs)}/{nlabs} labs attended\n'
        if unattended:
            unattended_list = sorted(list(unattended),
                                     key=lambda s: int(s.replace('lab', '')))
            unattended_str = ', '.join(unattended_list)
            lab_sum += f'Labs not attended: {unattended_str}\n'

        S += lab_sum
        S += '\n'

        complete, incomplete = drill_data[student]
        tot = len(complete) + len(incomplete)
        drill_sum = f'{len(complete)}/{tot} drills completed\n'
        if incomplete:
            sorted_drills = sorted(list(incomplete),
                                   key=lambda s: int(s.replace('drill', '')))
            incomplete_str = ', '.join(sorted_drills)
            drill_sum += f'Drills not completed: {incomplete_str}\n'

        S += drill_sum

        summs[student] = S
        if write:
            fp = pjoin(sum_path, f'{student}.txt')
            with locked_file(fp, 'w') as f:
                f.write(S)

    return summs
예제 #30
0
proj_base_path = pjoin(DATA_PATH, 'projects')
asgn_data_path = pjoin(BASE_PATH, 'ta/assignments.json')
ta_path = pjoin(BASE_PATH, 'ta/groups/tas.txt')
hta_path = pjoin(BASE_PATH, 'ta/groups/htas.txt')
student_path = pjoin(BASE_PATH, 'ta/groups/students.txt')
log_base_path = pjoin(DATA_PATH, 'logs')
test_base_path = pjoin(DATA_PATH, 'tests')
rubric_base_path = pjoin(DATA_PATH, 'rubrics')
grade_base_path = pjoin(DATA_PATH, 'grades')
s_files_base_path = pjoin(DATA_PATH, 'sfiles')
anon_base_path = pjoin(DATA_PATH, 'anonymization')
blocklist_path = pjoin(DATA_PATH, 'blocklists')
rubric_schema_path = pjoin(BASE_PATH, 'ta/grading/rubric_schema.json')
assert pexists(asgn_data_path), f'No data file "{asgn_data_path}"'

with locked_file(asgn_data_path) as f:
    asgn_data: AssignmentJson = json.load(f)


# function that checks if an assignment has been started
# func is the function the wrapper will be wrapped around
def is_started(func: Callable) -> Callable:
    """

    decorator that checks if assignment has been started before
    calling the appropriate method

    :param func: method for Assignments (should take in assignment as first
                 argument)
    :return: decorated method