diff --git a/README.md b/README.md index f80a1c5..4268376 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ student assignments. You can install `assignment-tool` by running - pip install git+https://github.com/KohlbacherLab/assignment-tool.git@1.1.1 + pip install git+https://github.com/KohlbacherLab/assignment-tool.git@1.2.0 ## Usage @@ -151,6 +151,10 @@ on the feedback PDF file in numerical order. If multiple comments are added for the same subtask, the order in which they are specified in the Excel sheet is maintained on the feedback PDF file. +Optionally, *global* comments can be specified by leaving the *Task* and +*Subtask* fields empty. Such comments can then be rendered separately from the +per-task comments. + ### The *Summary* Sheet

diff --git a/examples/ExampleSheet.xlsx b/examples/ExampleSheet.xlsx index 2a32d54..2c773ad 100644 Binary files a/examples/ExampleSheet.xlsx and b/examples/ExampleSheet.xlsx differ diff --git a/examples/template.tex b/examples/template.tex index 135fec1..f997867 100644 --- a/examples/template.tex +++ b/examples/template.tex @@ -8,24 +8,45 @@ \setlength\parindent{0mm} +% Command is executed before global comments and only if global comments are present +\newcommand\beforeGlobalComments{ + \subsection*{General Remarks} + \begin{itemize}\setlength\itemsep{0mm} +} + +% Command is executed after global comments and only if global comments are present +\newcommand\afterGlobalComments{ + \end{itemize} +} + +% Command is executed for every global comment. Arguments: Comment. +\newcommand\globalComment[1]{ +\item #1 +} + +% Command is executed before per-task comments and only if per-task comments are present \newcommand\beforeComments{ \begin{itemize}\setlength\itemsep{0mm} } +% Command is executed after per-task comments and only if per-task comments are present \newcommand\afterComments{ \end{itemize} } +% Command is executed for every per-task comment. Arguments: Comment. \newcommand\comment[1]{ \item #1 } +% Command is executed for every scored task, passing the task number as an argument \newcommand\newtask[1]{ \subsection*{Exercise #1} } +% Command is executed for every scored subtask. Arguments: sheet number, task number, subtask number, score for the subtask, maximum attainable score for the subtask \newcommand\scoreTask[5]{% -#2.#3\dotfill #4 von #5 +#2.#3\dotfill #4 of #5 } @@ -33,16 +54,18 @@ \subsection*{Exercise #1} \hrule \smallskip -{\large Bewertung Übungsblatt §§sheetnr§§\hfill Gesamt: §§total§§ von §§maxtotal§§}\smallskip +{\large Scores for Exercise Sheet §§sheetnr§§\hfill Total: §§total§§ of §§maxtotal§§}\smallskip {\large §§fullname§§}\smallskip -{Tutor: §§tutorname§§} +{Tutor: §§tutorname§§\hfill PHY9911: Practical String Theory} \smallskip \hrule \vspace{0.2cm} -§§body§§ +§§global§§ + +§§tasks§§ \end{document} diff --git a/img/header.png b/img/header.png index e906dc8..51c7e85 100644 Binary files a/img/header.png and b/img/header.png differ diff --git a/setup.py b/setup.py index a573467..6e7f4bc 100644 --- a/setup.py +++ b/setup.py @@ -21,13 +21,13 @@ from setuptools import setup, find_packages requires = [ - 'pandas', + 'pandas>=1.0.0', 'xlrd', ] setup( name = 'assignmenttool', - version = '1.1.1', + version = '1.2.0', package_dir={'':'src'}, packages=find_packages('./src'), author = 'Leon Kuchenbecker', diff --git a/src/assignmenttool/__init__.py b/src/assignmenttool/__init__.py index c3dcb3d..b776be1 100755 --- a/src/assignmenttool/__init__.py +++ b/src/assignmenttool/__init__.py @@ -86,9 +86,23 @@ def mail_feedback(config, participants, pdfs): #################################################################################################### +def read_scores(infile): + """Reads the scores from the Excel sheet while being as relaxed about the + data type of the task / subtask columns as possible""" + scores = pd.read_excel(infile, sheet_name = 'Grading', dtype={ + 'Username' : str, + 'Sheet' : 'Int64', + 'Task' : 'Int64', + 'Subtask' : 'Int64', + 'Type' : str, + 'Value' : object, + }) + + return scores + def process(config): # Read Scores and comments - scores = pd.read_excel(config.infile, sheet_name = 'Grading') + scores = read_scores(config.infile) # Read participants participants = pd.read_excel(config.infile, sheet_name = 'Participants').set_index('Username') @@ -114,20 +128,38 @@ def process(config): # Build dictionaries d = defaultdict(lambda : defaultdict( lambda : { 'score' : None, 'comments' : []} ) ) + sheet_comments = defaultdict(lambda : []) + for _,row in scores.iterrows(): - task = (row.Sheet, row.Task, row.Subtask) - record = d[row.Username][task] + if pd.isna(row.Sheet): + raise AToolError('Failed to parse provided Excel file, "Grading" sheet contains empty value for "Sheet" column.') if row.Type.upper() == 'SCORE': + # Check if the row checks out + if pd.isna(row.Task) or pd.isna(row.Subtask) or pd.isna(row.Value): + raise AToolError(f'Failed to parse provided Excel file, "Grading" sheet contains empty value for "Task", "Subtask" or "Value" column for sheet {row.Sheet}.') + task = (row.Sheet, row.Task, row.Subtask) + record = d[row.Username][task] + # Check if a score occurs redundantly for the same task if record['score'] is not None: raise AToolError(f'Duplicate score for identical task found (User: {row.Username}, Sheet: {row.Sheet}, Task: {row.Task}, Subtask: {row.Subtask}') + # Obtain maximum attainable score try: record['max_score'] = max_scores[task] except KeyError: raise AToolError(f'Could not find maximum score for task {task}') record['score'] = row.Value elif row.Type.upper() == 'COMMENT': - record['comments'].append(row.Value) + # Check if this is a comment that applies to the entire sheet + if pd.isna(row.Task) and pd.isna(row.Subtask): + sheet_comments[row.Username].append(row.Value) + elif pd.isna(row.Task) or pd.isna(row.Subtask): + raise AToolError(f'Failed to parse provided Excel file, "Grading" sheet contains empty value for "Task" or "Subtask" but not for both.') + # ... or if it's a comment that applies to a specific task + else: + task = (row.Sheet, row.Task, row.Subtask) + record = d[row.Username][task] + record['comments'].append(row.Value) else: raise AToolError(f'Invalid value type "{row.Type}".') @@ -166,8 +198,19 @@ def process(config): body.append(f'\\comment{{{comment}}}') body.append(r'\afterComments') + # Global comments + global_ = [] + if user in sheet_comments: + global_.append('\\beforeGlobalComments') + global_comments = sheet_comments[user] + for comment in global_comments: + global_.append(f'\\globalComment{{{comment}}}') + global_.append('\\afterGlobalComments') + # Write out and compile tex + tex = tex.replace('§§global§§', '\n'.join(global_)) tex = tex.replace('§§body§§', '\n'.join(body)) + tex = tex.replace('§§tasks§§', '\n'.join(body)) pdf = compileLaTeX(tex, config.pdflatex) # Move output file in place