def render_gs_anchor(pdf: PDF, variant, score=0): ''' Renders the gs anchor box for a given question/question part. -1: Empty box. Intended for template variants. 0: Completely incorrect answer. < 1: Partially correct answer. 1: Completely correct answer. ''' if score == -1: a_cfg = get_cfg('gsAnchor', 'blank') elif score == 0: a_cfg = get_cfg('gsAnchor', 'incorrect') elif score < 1: a_cfg = get_cfg('gsAnchor', 'partial') elif score == 1: a_cfg = get_cfg('gsAnchor', 'correct') else: return fill = a_cfg['fill'] text = f'{a_cfg["text"]}: {score}' if score > -1 else a_cfg['text'] pdf.set_font(cfg['font']['body']['font']) pdf.set_fill_color(fill['r'], fill['g'], fill['b']) pdf.cell(lineWidth, h=get_cfg('gsAnchor', 'height'), txt=text, fill=True) pdf.ln()
def render(self, pdf: PDF, as_template=False): """ render a question part onto the pdf. 1. renders the question ctx, if any 2. adds the gs grading anchor 3. renders the given answer, if any 4. pads until we've reached the max pages for the question. if as_template is true, will not render answer, and anchor will be empty. """ start = pdf.page_no() render_part_header( pdf, f'Question {self.question_number}.{self.part}: {self.key}') pdf.set_font(get_cfg('font', 'body', 'font', default='arial'), size=get_cfg('font', 'body', 'font', cast=int, default=10)) # self.render_ctx(pdf) draw_line(pdf) render_gs_anchor(pdf, self.key, -1 if as_template else self.score) draw_line(pdf) self.render_expected(pdf) draw_line(pdf) self.render_ans( pdf) if not as_template else self.render_template_ans(pdf) pad_until(pdf, start + self.max_pages - 1, f'padding for question {self.question_number}.{self.part}')
def render_file(self, pdf: PDF, filename, blank=False): ''' Renders a file to a pdf. Does not start a fresh page. Pads out the pages with blank pages if the file does not exist ''' path = self.files.get(filename, False) start = pdf.page_no() render_part_header(pdf, filename) if path: start = pdf.page_no() ext = os.path.splitext(path)[1][1:] font = get_cfg('font', 'code') if ext in get_cfg( 'files', 'code') else get_cfg('font', 'body') pdf.set_font(font['font'], size=font['size']) if blank: pdf.cell(lineWidth, txt="This is a sample student answer.") elif ext in get_cfg('files', 'md'): pdf.write_html(to_latin1(markdown2.markdown_path(path))) elif ext in get_cfg('files', 'pics'): pdf.image(path, w=lineWidth) else: for line in open(path, 'r'): pdf.multi_cell(lineWidth, lineHeight, txt=to_latin1(line)) self.pad_from(pdf, start, filename)
def render_front_page(self, pdf: PDF, template=False): ''' renders the title page for a student submission ''' pdf.set_font(get_cfg('font', 'title', 'font', default="arial"), size=get_cfg('font', 'title', 'size', default=9, cast=int), style="U") pdf.cell(0, 60, ln=1) id = self.uid if not template else " " * len(self.uid) name = self.name if not template else " " * len(self.name) pdf.cell(0, 20, f'Name: {name}', align='L') pdf.cell(0, 20, f'SID: {self.sid}', align='L') pdf.cell(0, 20, f'Email: {id}@berkeley.edu', align='L')
def render_header(pdf: PDF, txt, header_cfg=get_cfg('font', 'header')): ''' renders a question header with the given text. ''' pdf.set_font(header_cfg['font'], size=header_cfg['size']) pdf.cell(lineWidth, txt=txt) draw_line(pdf, pdf.get_string_width(txt), header_cfg['line'])
def __init__(self, question_number: int, part: int, key, score: int = 0, weight: int = 1, ctx='', true_ans='', ans=''): super().__init__(question_number, part, key, score, weight) self.ans = str(ans) self.ctx = ctx self.true_ans = str(true_ans) self.max_pages = get_cfg('maxPages', 'string', cast=int, default=1) self.score = -1
def __init__(self, question_number: int, part: int, key, score: int = 0, weight: int = 1, files=[], file_bundle=None): super().__init__(question_number, part, key, score, weight) self.files = files self.file_bundle = file_bundle self.max_pages = get_cfg("maxPages", "file", cast=int, default=1) * len(files) self.score = -1 # for manually graded coding questions
def draw_line(pdf: PDF, width=lineWidth, color=get_cfg('font', 'header', 'line', default={"r": 0, "b": 0, "g": 0})): ''' draws a line on the page. by default, the line is the page width. ''' pdf.ln(6) pdf.set_line_width(0.5) pdf.set_draw_color(color['r'], color['b'], color['g']) pdf.line(10, pdf.get_y(), 12 + width, pdf.get_y()) pdf.ln(6)
def merge(qmap_json, gs_csv, instance=1, method=get_cfg('questions', 'mergeMethod', default='partial')): ''' given a gradescope csv and a plgspl question map (per student), generates a "manual grading" csv for pl ''' if method == "partial": merge_partials(qmap_json, gs_csv, instance) elif method == "total": merge_total(qmap_json, gs_csv, instance) else: print("Unsupported merge method.")
def to_pdf(info_json, manual_csv, file_dir=None, roster=None): submissions = dict() config = qs.AssignmentConfig() if roster: with open(roster) as r: roster_json = json.load(r) else: roster_json = {} # load the raw assignment config file cfg = json.load(open(info_json)) out_file = cfg.get("title", "assignment").replace(" ", "_") zones = cfg['zones'] print(f'Parsing config for {out_file}...') for z in zones: for i, raw_q in enumerate(z['questions']): parts = raw_q['parts'] if 'parts' in raw_q else [] files = set(raw_q['files']) if 'files' in raw_q else set() if 'id' not in raw_q: vs = list(map(lambda q: q['id'], raw_q['alternatives'])) q = qs.QuestionInfo(vs[0], i + 1, variants=vs, number_choose=raw_q['numberChoose'], parts=parts, expected_files=files) else: q = qs.QuestionInfo(raw_q['id'], i + 1, parts=parts, expected_files=files, is_mc=bool(raw_q.get("is_mc", False))) config.add_question(q) print( f'Parsed config. Created {config.get_question_count()} questions and {config.get_variant_count()} variants.', end='\n\n') # iterate over the rows of the csv and parse the data print( f'Parsing submissions from {manual_csv} and provided file directory (if any)' ) manual = pd.read_csv(manual_csv) for i, m in manual.iterrows(): uid_full = m.get('uid', m.get('UID')) roster_map = roster_json.get(uid_full) if roster_map: name, student_id = roster_map else: name, student_id = "Unknown", "Unknown" uid = str(uid_full).split("@", 1)[0] qid = m['qid'] sid = m['submission_id'] submission = submissions.get(uid) if not submission: submission = qs.Submission(uid, name, student_id) submissions[uid] = submission q = config.get_question(qid) if not q: continue # look for any files related to this question submission fns = [] if file_dir: for fn in os.listdir(file_dir): # if it has the student id, and the qid_sid pair, count it as acceptable if fn.find(uid_full) > -1 and fn.find( f'{qs.escape_qid(qid)}_{sid}' ) > -1 and qs.parse_filename(fn, qid) in q.expected_files: fns.append(os.path.join(file_dir, fn)) q.add_file(os.path.join(file_dir, fn)) submission.add_student_question( qs.StudentQuestion(q, m['params'], m['true_answer'], m['submitted_answer'], m['partial_scores'], qs.StudentFileBundle(fns, qid), qid)) print(f'Created {len(submissions)} submission(s)..') pdf = PDF() def pdf_output(pdf, name, uid=""): # try: pdf.output(os.path.join(os.getcwd(), f'{out_file}_{name}-fa20mt1.pdf')) # except: # print("Couldn't make pdf for submission {} with uid {}".format(name, uid)) prev = 1 expected_pages = 0 template_submission = None missing_questions = [] for i, (_, v) in enumerate(submissions.items()): v: qs.Submission start_page = pdf.page_no() v.render_submission(pdf, config, template_submission=template_submission) if i == 0: sample_pdf = PDF() v.render_submission(sample_pdf, config, True) template_submission = v pdf_output(sample_pdf, "sample") expected_pages = sample_pdf.page_no() max_submissions = get_cfg('gs', 'pagesPerPDF') / expected_pages if max_submissions < 1: print( 'Cannot create submissions given the current max page constraint.' ) print('Please adjust your defaults.') exit(1) max_submissions = int(max_submissions) diff = pdf.page_no() - start_page if diff < expected_pages: missing_questions.append(v.uid) elif diff > expected_pages: print( f'Submission {i}, {v.uid} exceeds the sample template. Please make sure that the first submission is complete' ) exit(1) while pdf.page_no() - start_page < expected_pages: pdf.add_page() pdf.cell(0, 20, f'THIS IS A BLANK PAGE', ln=1, align='C') if i != 0 and i % max_submissions == 0: pdf_output(pdf, f'{i - max_submissions + 1}-{i + 1}') prev = i + 1 pdf = PDF() if prev < len(submissions) or len(submissions) == 1: pdf_output(pdf, f'{prev}-{len(submissions)}') if len(missing_questions) > 0: print( f'{len(missing_questions)} submissions are missing question submissions. Please make sure to manually pair them in gradescope!', missing_questions, sep="\n") json.dump({k: v.list_questions(config) for k, v in submissions.items()}, open(f'{out_file}_qmap.json', 'w'))
def render_part_header(pdf: PDF, txt): ''' renders a part header with the given text. ''' render_header(pdf, txt, header_cfg=get_cfg('font', 'subheader'))
def __init__(self, question_number: int, part: int, key, score: int = 0, weight: int = 1): self.question_number = question_number self.part = part self.key = key self.score = score self.max_pages = get_cfg('maxPages', 'default', cast=int, default=1)
def pad_from(self, pdf, start, filename): pad_until(pdf, start + get_cfg('maxPages', 'file', cast=int, default=1) - 1, f'padding for file {filename}')
from plgspl.types import PDF from enum import Enum from functools import reduce from typing import List, Dict import json import os import re import collections from plgspl.cfg import cfg, get_cfg import markdown2 from unidecode import unidecode lineWidth = get_cfg('page', 'lineWidth', default=180, cast=int) lineHeight = get_cfg('page', 'lineHeight', default=0, cast=int) def to_latin1(s: str) -> str: return unidecode(s) def draw_line(pdf: PDF, width=lineWidth, color=get_cfg('font', 'header', 'line', default={"r": 0, "b": 0, "g": 0})): ''' draws a line on the page. by default, the line is the page width. ''' pdf.ln(6) pdf.set_line_width(0.5) pdf.set_draw_color(color['r'], color['b'], color['g']) pdf.line(10, pdf.get_y(), 12 + width, pdf.get_y()) pdf.ln(6)