mirror of
https://github.com/balkian/MoodleBulkGrader.git
synced 2024-12-07 08:32:29 +00:00
200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
'''
|
|
@author Fernando Sánchez (jf.sanchez, balkian) UPM
|
|
|
|
See README.md
|
|
|
|
'''
|
|
import sys
|
|
import os
|
|
import pathlib
|
|
import argparse
|
|
import subprocess
|
|
import mimetypes
|
|
|
|
from collections import defaultdict
|
|
|
|
|
|
from glob import glob
|
|
|
|
from PIL import Image
|
|
|
|
from shutil import copy, copyfile
|
|
from PyPDF2 import PdfFileMerger, PdfFileReader
|
|
|
|
SUBMISSIONS_PATH = os.environ.get('SUBMISSIONS_PATH', 'submissions')
|
|
REVIEWS_PATH = os.environ.get('REVIEWS_PATH', 'review')
|
|
SECTIONS = '2.1 2.2 2.3.a 2.3.b 2.3.c'
|
|
SECTIONS = set(os.environ.get('SECTIONS', SECTIONS).split(' '))
|
|
|
|
PDFVIEWER = os.environ.get('PDFVIEWER', 'evince')
|
|
LABELS = ['NOTA', 'GRADE']
|
|
|
|
def copy_all():
|
|
'''Copia
|
|
'''
|
|
for submission in os.listdir(submissions):
|
|
tokens = submission.split('_')
|
|
nombre = tokens[0]
|
|
dst = reviews / nombre
|
|
os.makedirs(dst, exist_ok=True)
|
|
if not os.path.exists(dst / submission):
|
|
copy(submissions / submission, dst)
|
|
|
|
|
|
def create_pdfs():
|
|
students = os.listdir(reviews)
|
|
missing = []
|
|
|
|
for student in students:
|
|
output = reviews / (student+'.pdf')
|
|
if os.path.exists(output):
|
|
continue
|
|
|
|
folder = reviews / student
|
|
if not folder.is_dir():
|
|
continue
|
|
print(folder)
|
|
files = os.listdir(folder)
|
|
if len(files) == 1 and files[0].endswith('pdf'):
|
|
copyfile(folder / files[0], output)
|
|
elif len(files) > 1 and all(file.endswith('pdf') for file in files):
|
|
merger = PdfFileMerger()
|
|
for pdf in files:
|
|
merger.append(str(folder / pdf))
|
|
merger.write(str(output))
|
|
merger.close()
|
|
elif all(file.endswith('jpg') or file.endswith('jpeg') for file in files):
|
|
try:
|
|
imgs = []
|
|
for file in files:
|
|
imgs.append(Image.open(folder / file).convert('RGB'))
|
|
imgs[0].save(output, save_all=True, append_images=imgs[1:])
|
|
except Exception as ex:
|
|
if os.path.exists(output):
|
|
os.remove(output)
|
|
print('Error al convertir', ex)
|
|
missing.append(student)
|
|
else:
|
|
for file in files:
|
|
print(file)
|
|
print(mimetypes.guess_type(folder / file))
|
|
missing.append(student)
|
|
|
|
|
|
print(f'Missing {len(missing)}/{len(students)}')
|
|
|
|
|
|
def get_annotations(src, grading_labels=LABELS):
|
|
input1 = PdfFileReader(open(src, "rb"))
|
|
nPages = input1.getNumPages()
|
|
|
|
annotations = []
|
|
notas = {}
|
|
|
|
for i in range(nPages) :
|
|
# get the data from this PDF page (first line of text, plus annotations)
|
|
page = input1.getPage(i)
|
|
page_annotations = []
|
|
|
|
try :
|
|
for annot in page['/Annots']:
|
|
# Other subtypes, such as /Link, cause errors
|
|
subtype = annot.getObject()['/Subtype']
|
|
if subtype == "/Text":
|
|
text = annot.getObject()['/Contents']
|
|
print('LABELS', grading_labels)
|
|
if any(text.startswith(label) for label in grading_labels):
|
|
lines = text.splitlines()[1:]
|
|
for line in lines:
|
|
tokens = list(x.strip() for x in line.split(' '))
|
|
if tokens[0] in notas:
|
|
raise Exception(f'Sobreescribiendo nota {tokens[0]} para {src}. Página {i+1}')
|
|
notas[tokens[0]] = float(tokens[1])
|
|
else:
|
|
page_annotations.append(text)
|
|
except KeyError as ex:
|
|
pass
|
|
if not page_annotations:
|
|
continue
|
|
annotations.append(f'Página {i+1}:\n' + '\n'.join(page_annotations))
|
|
return '\n'.join(annotations), notas
|
|
|
|
|
|
def process_one(review, sections=SECTIONS, grading_labels=LABELS):
|
|
sections = set(sections)
|
|
text, notas = get_annotations(review, grading_labels)
|
|
|
|
graded = set(notas.keys())
|
|
missing = sections - graded
|
|
invalid = graded - sections
|
|
valid = sections & graded
|
|
return valid, invalid, missing
|
|
|
|
|
|
def grading_status(valid, invalid, full, total):
|
|
print('Valid graded sections:')
|
|
for (k, v) in valid.items():
|
|
print(f'\t{k}:\t{len(v):>5}/{total}')
|
|
print('Invalid graded sections:\t')
|
|
for (k, v) in invalid.items():
|
|
print(f'\t{k}:\t{len(v):>5}/{total}')
|
|
|
|
print(f'Fully graded: {full}')
|
|
|
|
|
|
def calculate(grade=True, student=None, sections=SECTIONS, viewer=PDFVIEWER, grading_labels=LABELS):
|
|
print('Grading')
|
|
valid = {k: [] for k in sections}
|
|
invalid = defaultdict(list)
|
|
if student:
|
|
files = [reviews / (student + '.pdf')]
|
|
else:
|
|
files = os.listdir(reviews)
|
|
files = list(reviews / file for file in files if os.path.isfile(reviews / file))
|
|
total = len(files)
|
|
full = 0
|
|
for ix, review in enumerate(files):
|
|
print(f'Processing {review}')
|
|
v, i, m = process_one(review, sections=sections, grading_labels=grading_labels)
|
|
if grade and (m or i):
|
|
subprocess.call([viewer, review])
|
|
v, i, m = process_one(review, sections=sections, grading_labels=grading_labels)
|
|
for k in v:
|
|
valid[k].append(review)
|
|
for k in i:
|
|
invalid[k].append(review)
|
|
if not m:
|
|
full += 1
|
|
if grade:
|
|
grading_status(valid, invalid, full, ix)
|
|
grading_status(valid, invalid, full, total)
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(prog='MOODLEBulkGrader')
|
|
parser.add_argument('--copy', action='store_true',
|
|
help='Copy assignments and sort them into folders')
|
|
parser.add_argument('--merge', action='store_true',
|
|
help='Merge individual files into a single PDF (might require some manual intervention')
|
|
parser.add_argument('--no-grade', action='store_true',
|
|
help='Do not start auto-grade')
|
|
parser.add_argument('--student', action='store',
|
|
default=None, help='Only grade a single student')
|
|
parser.add_argument('--sections', default=','.join(SECTIONS), help='Sections to grade (comma-separated)')
|
|
parser.add_argument('--labels', default=','.join(LABELS), help='Use any of these labels (comma-separated) in the first line of a comment to add grades for each section, one per line.')
|
|
parser.add_argument('--viewer', default=PDFVIEWER, help='PDF viewer program to add text annotations')
|
|
parser.add_argument('--submissions-path', default=SUBMISSIONS_PATH, help='Folder with original submissions')
|
|
parser.add_argument('--reviews-path', default=REVIEWS_PATH, help='Folder with one PDF per student.')
|
|
args = parser.parse_args()
|
|
reviews = pathlib.Path(args.reviews_path)
|
|
submissions = pathlib.Path(args.submissions_path)
|
|
if args.copy:
|
|
copy_all()
|
|
if args.merge:
|
|
create_pdfs()
|
|
calculate(grade=not args.no_grade,
|
|
student=args.student,
|
|
sections=args.sections.split(','),
|
|
viewer=args.viewer,
|
|
grading_labels=args.labels.split(','),
|
|
)
|