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
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
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)
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
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)
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
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))
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
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}')
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}')
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 []
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])
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)
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)
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)
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)
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
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)
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]
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
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
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])
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
def _add_to_log(self) -> None: with locked_file(log_path, 'a') as f: f.write(f'{self.ident}\n')
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)
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
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:
# 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
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
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