def render_full(self, pdf: PDF): empty_bubble = u"\u25CB" full_bubble = u"\u25CF" answers = ['a', 'b', 'c', 'd'] answer_bubbles = [full_bubble if self.ans == a else empty_bubble for a in answers] line = " ".join([answers[i] + " " + answer_bubbles[i] for i in range(len(answers))]) pdf.multi_cell(lineWidth, lineHeight, txt=f"{self.part}) {self.ans}")
def render_ans_helper(self, pdf: PDF, template=False): if self.file_bundle: if len(self.files) > 0: self.file_bundle.render_file(pdf, self.files[0], template) for f in self.files[1:]: pdf.add_page() self.file_bundle.render_file(pdf, f, template)
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_expected(self, pdf: PDF): ''' renders the expected anwer to the pdf. renders the string "No context provided." if not overloaded ''' pdf.cell(lineWidth, txt="No expected answer provided.")
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 render_ctx(self, pdf: PDF): if isinstance(self.ctx, str): if self.ctx == "": super().render_ctx(pdf) else: pdf.multi_cell(lineWidth, lineHeight, txt=self.ctx) else: pdf.multi_cell(lineWidth, lineHeight, txt=json.dumps(self.ctx))
def pad_until(pdf: PDF, page_number, info=''): ''' pads the pdf until the target page number. ''' if pdf.page_no() > page_number: print('Warning: A question exceeds expected length. Please re-adjust your configuration.', info) print('Dumping current pdf as incomplete_assignment.pdf') pdf.output(os.path.join(os.getcwd(), 'incomplete_assignment.pdf')) exit(1) while pdf.page_no() < page_number: pad(pdf)
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_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(self, pdf: PDF, template=False): ''' renders the question to the page. by default, does not start a new page for the first question. ''' self.question.render(pdf) #if get_cfg('questions', 'dumpParams', default=False): # pdf.multi_cell(lineWidth, lineHeight, txt=json.dumps(self.params)) # draw_line(pdf) for i, p in enumerate(self.parts): if self.question.is_mc: p.render_full(pdf) draw_line(pdf) else: if i != 0: pdf.add_page() p.render(pdf, template)
def render_submission(self, pdf: PDF, qMap: AssignmentConfig, is_template=False, template_submission=None): ''' renders all of the student's answers/questions to the given question, in the order described by the given question map. ''' pdf.add_page() self.render_front_page(pdf, is_template) for q in qMap.get_question_list(): count = q.number_choose for qv in q.variants: if count == 0: break sq = self.get_student_question(qv) if sq != None: count -= 1 pdf.add_page() sq.render(pdf, is_template) if count != 0 and template_submission: for qv in q.variants: if count == 0: break sq = self.get_student_question(qv) if sq != None: continue sq = template_submission.get_student_question(qv) if sq != None: count -= 1 pdf.add_page() sq.render(pdf, True)
def render(self, pdf: PDF, as_template=False): 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) """ 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_ctx(self, pdf: PDF): ''' renders any question context to the pdf. renders the string "No context provided." if not overloaded ''' pdf.cell(lineWidth, txt="No context provided.")
def render_ans(self, pdf: PDF): pdf.cell(lineWidth, lineHeight, txt=f'{self.key}: {self.val}') pdf.ln() pdf.cell(lineWidth, lineHeight, txt=f'Variables: {self.vars}')
def render_full(self, pdf: PDF): pdf.multi_cell(lineWidth, lineHeight, txt=f"{self.part}) {', '.join(list(self.ans))}")
def render_ans(self, pdf: PDF): ''' renders the answer content to the pdf. renders the string "no answer provided" if not overloaded ''' pdf.cell(lineWidth, txt="No answer provided.")
def render_template_ans(self, pdf: PDF): ''' renders the templated answer content to the pdf. renders a blank string if not overloaded ''' pdf.cell(lineWidth, txt="")
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 render_ans(self, pdf: PDF): pdf.multi_cell(lineWidth, lineHeight, txt="Your answer: " + to_latin1(f'"{self.ans}"'))
def render_full(self, pdf: PDF): pdf.multi_cell(lineWidth, lineHeight, txt=f"{self.part}) '{to_latin1(self.ans)}'")
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_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)