Skip to content

Commit b92fc73

Browse files
committed
Feature: Global comments that do not apply to a specific task
1 parent fa8aef7 commit b92fc73

File tree

6 files changed

+81
-11
lines changed

6 files changed

+81
-11
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ student assignments.
1010

1111
You can install `assignment-tool` by running
1212

13-
pip install git+https://github.com/KohlbacherLab/assignment-tool.git@1.1.1
13+
pip install git+https://github.com/KohlbacherLab/assignment-tool.git@1.2.0
1414

1515
## Usage
1616

@@ -151,6 +151,10 @@ on the feedback PDF file in numerical order. If multiple comments are added for
151151
the same subtask, the order in which they are specified in the Excel sheet is
152152
maintained on the feedback PDF file.
153153

154+
Optionally, *global* comments can be specified by leaving the *Task* and
155+
*Subtask* fields empty. Such comments can then be rendered separately from the
156+
per-task comments.
157+
154158
### The *Summary* Sheet
155159

156160
<p align="center">

examples/ExampleSheet.xlsx

152 Bytes
Binary file not shown.

examples/template.tex

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,64 @@
88

99
\setlength\parindent{0mm}
1010

11+
% Command is executed before global comments and only if global comments are present
12+
\newcommand\beforeGlobalComments{
13+
\subsection*{General Remarks}
14+
\begin{itemize}\setlength\itemsep{0mm}
15+
}
16+
17+
% Command is executed after global comments and only if global comments are present
18+
\newcommand\afterGlobalComments{
19+
\end{itemize}
20+
}
21+
22+
% Command is executed for every global comment. Arguments: Comment.
23+
\newcommand\globalComment[1]{
24+
\item #1
25+
}
26+
27+
% Command is executed before per-task comments and only if per-task comments are present
1128
\newcommand\beforeComments{
1229
\begin{itemize}\setlength\itemsep{0mm}
1330
}
1431

32+
% Command is executed after per-task comments and only if per-task comments are present
1533
\newcommand\afterComments{
1634
\end{itemize}
1735
}
1836

37+
% Command is executed for every per-task comment. Arguments: Comment.
1938
\newcommand\comment[1]{
2039
\item #1
2140
}
2241

42+
% Command is executed for every scored task, passing the task number as an argument
2343
\newcommand\newtask[1]{
2444
\subsection*{Exercise #1}
2545
}
2646

47+
% Command is executed for every scored subtask. Arguments: sheet number, task number, subtask number, score for the subtask, maximum attainable score for the subtask
2748
\newcommand\scoreTask[5]{% <sheet> <task> <subtask> <score> <total>
28-
#2.#3\dotfill #4 von #5
49+
#2.#3\dotfill #4 of #5
2950

3051
}
3152

3253
\begin{document}
3354
\hrule
3455
\smallskip
3556

36-
{\large Bewertung Übungsblatt §§sheetnr§§\hfill Gesamt: §§total§§ von §§maxtotal§§}\smallskip
57+
{\large Scores for Exercise Sheet §§sheetnr§§\hfill Total: §§total§§ of §§maxtotal§§}\smallskip
3758

3859
{\large §§fullname§§}\smallskip
3960

40-
{Tutor: §§tutorname§§}
61+
{Tutor: §§tutorname§§\hfill PHY9911: Practical String Theory}
4162

4263
\smallskip
4364
\hrule
4465
\vspace{0.2cm}
4566

46-
§§body§§
67+
§§global§§
68+
69+
§§tasks§§
4770

4871
\end{document}

img/header.png

-123 KB
Loading

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
from setuptools import setup, find_packages
2222

2323
requires = [
24-
'pandas',
24+
'pandas>=1.0.0',
2525
'xlrd',
2626
]
2727

2828
setup(
2929
name = 'assignmenttool',
30-
version = '1.1.1',
30+
version = '1.2.0',
3131
package_dir={'':'src'},
3232
packages=find_packages('./src'),
3333
author = 'Leon Kuchenbecker',

src/assignmenttool/__init__.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,23 @@ def mail_feedback(config, participants, pdfs):
8686

8787
####################################################################################################
8888

89+
def read_scores(infile):
90+
"""Reads the scores from the Excel sheet while being as relaxed about the
91+
data type of the task / subtask columns as possible"""
92+
scores = pd.read_excel(infile, sheet_name = 'Grading', dtype={
93+
'Username' : str,
94+
'Sheet' : 'Int64',
95+
'Task' : 'Int64',
96+
'Subtask' : 'Int64',
97+
'Type' : str,
98+
'Value' : object,
99+
})
100+
101+
return scores
102+
89103
def process(config):
90104
# Read Scores and comments
91-
scores = pd.read_excel(config.infile, sheet_name = 'Grading')
105+
scores = read_scores(config.infile)
92106

93107
# Read participants
94108
participants = pd.read_excel(config.infile, sheet_name = 'Participants').set_index('Username')
@@ -114,20 +128,38 @@ def process(config):
114128

115129
# Build dictionaries
116130
d = defaultdict(lambda : defaultdict( lambda : { 'score' : None, 'comments' : []} ) )
131+
sheet_comments = defaultdict(lambda : [])
132+
117133

118134
for _,row in scores.iterrows():
119-
task = (row.Sheet, row.Task, row.Subtask)
120-
record = d[row.Username][task]
135+
if pd.isna(row.Sheet):
136+
raise AToolError('Failed to parse provided Excel file, "Grading" sheet contains empty value for "Sheet" column.')
121137
if row.Type.upper() == 'SCORE':
138+
# Check if the row checks out
139+
if pd.isna(row.Task) or pd.isna(row.Subtask) or pd.isna(row.Value):
140+
raise AToolError(f'Failed to parse provided Excel file, "Grading" sheet contains empty value for "Task", "Subtask" or "Value" column for sheet {row.Sheet}.')
141+
task = (row.Sheet, row.Task, row.Subtask)
142+
record = d[row.Username][task]
143+
# Check if a score occurs redundantly for the same task
122144
if record['score'] is not None:
123145
raise AToolError(f'Duplicate score for identical task found (User: {row.Username}, Sheet: {row.Sheet}, Task: {row.Task}, Subtask: {row.Subtask}')
146+
# Obtain maximum attainable score
124147
try:
125148
record['max_score'] = max_scores[task]
126149
except KeyError:
127150
raise AToolError(f'Could not find maximum score for task {task}')
128151
record['score'] = row.Value
129152
elif row.Type.upper() == 'COMMENT':
130-
record['comments'].append(row.Value)
153+
# Check if this is a comment that applies to the entire sheet
154+
if pd.isna(row.Task) and pd.isna(row.Subtask):
155+
sheet_comments[row.Username].append(row.Value)
156+
elif pd.isna(row.Task) or pd.isna(row.Subtask):
157+
raise AToolError(f'Failed to parse provided Excel file, "Grading" sheet contains empty value for "Task" or "Subtask" but not for both.')
158+
# ... or if it's a comment that applies to a specific task
159+
else:
160+
task = (row.Sheet, row.Task, row.Subtask)
161+
record = d[row.Username][task]
162+
record['comments'].append(row.Value)
131163
else:
132164
raise AToolError(f'Invalid value type "{row.Type}".')
133165

@@ -166,8 +198,19 @@ def process(config):
166198
body.append(f'\\comment{{{comment}}}')
167199
body.append(r'\afterComments')
168200

201+
# Global comments
202+
global_ = []
203+
if user in sheet_comments:
204+
global_.append('\\beforeGlobalComments')
205+
global_comments = sheet_comments[user]
206+
for comment in global_comments:
207+
global_.append(f'\\globalComment{{{comment}}}')
208+
global_.append('\\afterGlobalComments')
209+
169210
# Write out and compile tex
211+
tex = tex.replace('§§global§§', '\n'.join(global_))
170212
tex = tex.replace('§§body§§', '\n'.join(body))
213+
tex = tex.replace('§§tasks§§', '\n'.join(body))
171214
pdf = compileLaTeX(tex, config.pdflatex)
172215

173216
# Move output file in place

0 commit comments

Comments
 (0)