mcqpy-core 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/PKG-INFO +1 -1
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/pyproject.toml +1 -1
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/__init__.py +10 -1
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/overall_analysis.py +4 -1
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/grader.py +12 -8
- mcqpy_core-0.2.2/src/mcqpy_core/grading/types.py → mcqpy_core-0.2.3/src/mcqpy_core/grading/helpers.py +1 -35
- mcqpy_core-0.2.3/src/mcqpy_core/grading/types.py +56 -0
- mcqpy_core-0.2.2/src/mcqpy_core/cli/build.py +0 -89
- mcqpy_core-0.2.2/src/mcqpy_core/cli/check_latex.py +0 -31
- mcqpy_core-0.2.2/src/mcqpy_core/cli/grade.py +0 -100
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/README.md +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/_selection.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/config.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/main.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/token.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/web.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/init.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/main.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/check_tags.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/init.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/main.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/render.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/validate.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/autofill.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/check_filter.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/main.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/front_config.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/question_analysis.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/utils.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/rubric.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/header_config.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/manifest.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/base_filter.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/date.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/difficulty.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/factory.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/manifest.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/slug.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/stratified.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/tag.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/question.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/question_bank.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/utils.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/web/__init__.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/web/bundle.py +0 -0
- {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/web/token.py +0 -0
|
@@ -7,9 +7,14 @@ from .types import (
|
|
|
7
7
|
GradedSet,
|
|
8
8
|
ParsedQuestion,
|
|
9
9
|
ParsedSet,
|
|
10
|
-
|
|
10
|
+
GradeResult,
|
|
11
|
+
GradeState,
|
|
12
|
+
ParseResult,
|
|
13
|
+
ParseState,
|
|
11
14
|
)
|
|
12
15
|
|
|
16
|
+
from .helpers import get_grade_dataframe
|
|
17
|
+
|
|
13
18
|
__all__ = [
|
|
14
19
|
"MCQGrader",
|
|
15
20
|
"Rubric",
|
|
@@ -18,6 +23,10 @@ __all__ = [
|
|
|
18
23
|
"ParsedSet",
|
|
19
24
|
"GradedQuestion",
|
|
20
25
|
"GradedSet",
|
|
26
|
+
"GradeResult",
|
|
27
|
+
"GradeState",
|
|
28
|
+
"ParseResult",
|
|
29
|
+
"ParseState",
|
|
21
30
|
"get_grade_dataframe",
|
|
22
31
|
"grade_parsed_set",
|
|
23
32
|
]
|
|
@@ -9,7 +9,10 @@ from scipy.stats import pearsonr
|
|
|
9
9
|
from mcqpy_core.grading.analysis.utils import AnalysisFigure
|
|
10
10
|
|
|
11
11
|
def get_points_array(graded_sets: list[GradedSet]):
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
n_questions = max(len(graded_set.graded_questions) for graded_set in graded_sets)
|
|
14
|
+
|
|
15
|
+
point_arrays = np.zeros((len(graded_sets), n_questions))
|
|
13
16
|
for i, graded_set in enumerate(graded_sets):
|
|
14
17
|
for j, question in enumerate(graded_set.graded_questions):
|
|
15
18
|
point_arrays[i, j] = question.point_value
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
3
|
from mcqpy_core.manifest import Manifest
|
|
4
|
-
from mcqpy_core.grading.types import GradedQuestion, GradedSet, ParsedSet
|
|
4
|
+
from mcqpy_core.grading.types import GradedQuestion, GradedSet, ParsedSet, GradeResult, GradeState
|
|
5
5
|
from mcqpy_core.grading.rubric import Rubric
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def grade_parsed_set(manifest: Manifest, rubric: Rubric, parsed_set: ParsedSet) ->
|
|
8
|
+
def grade_parsed_set(manifest: Manifest, rubric: Rubric, parsed_set: ParsedSet) -> GradeResult:
|
|
9
9
|
graded_set = GradedSet(student_info=parsed_set.student_info, graded_questions=[])
|
|
10
10
|
|
|
11
|
+
other_info = {}
|
|
11
12
|
for parsed_question in parsed_set.questions:
|
|
12
13
|
manifest_item = manifest.get_item_by_qid(parsed_question.qid)
|
|
13
14
|
|
|
@@ -19,17 +20,20 @@ def grade_parsed_set(manifest: Manifest, rubric: Rubric, parsed_set: ParsedSet)
|
|
|
19
20
|
max_point_value=manifest_item.point_value,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
|
-
if sum(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
23
|
+
if sum(parsed_question.onehot) == 0:
|
|
24
|
+
message = f"No answers provided for question {parsed_question.qid} ({parsed_question.slug})."
|
|
25
|
+
other_info[parsed_question.qid] = message
|
|
26
26
|
|
|
27
27
|
graded_question.point_value = rubric.score_question(graded_question)
|
|
28
28
|
graded_set.graded_questions.append(graded_question)
|
|
29
29
|
|
|
30
30
|
graded_set.points = sum(q.point_value for q in graded_set.graded_questions)
|
|
31
31
|
graded_set.max_points = sum(q.max_point_value for q in graded_set.graded_questions)
|
|
32
|
-
return
|
|
32
|
+
return GradeResult(
|
|
33
|
+
state=GradeState.SUCCESS,
|
|
34
|
+
graded_set=graded_set,
|
|
35
|
+
other_info=other_info if other_info else None,
|
|
36
|
+
)
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
class MCQGrader:
|
|
@@ -38,7 +42,7 @@ class MCQGrader:
|
|
|
38
42
|
self.rubric = rubric
|
|
39
43
|
self.regex_pattern = regex_pattern
|
|
40
44
|
|
|
41
|
-
def grade(self, parsed_set: ParsedSet | None = None, student_answer: str | Path = None) ->
|
|
45
|
+
def grade(self, parsed_set: ParsedSet | None = None, student_answer: str | Path = None) -> GradeResult:
|
|
42
46
|
if parsed_set is None:
|
|
43
47
|
raise ValueError("MCQGrader.grade requires a ParsedSet in mcqpy-core")
|
|
44
48
|
return grade_parsed_set(self.manifest, self.rubric, parsed_set)
|
|
@@ -1,38 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
@dataclass
|
|
5
|
-
class ParsedQuestion:
|
|
6
|
-
qid: str
|
|
7
|
-
slug: str
|
|
8
|
-
answers: list[int]
|
|
9
|
-
onehot: list[int]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass
|
|
13
|
-
class ParsedSet:
|
|
14
|
-
student_info: dict[str, str]
|
|
15
|
-
questions: list[ParsedQuestion]
|
|
16
|
-
file: str | None = None
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@dataclass
|
|
20
|
-
class GradedQuestion:
|
|
21
|
-
qid: str
|
|
22
|
-
slug: str
|
|
23
|
-
student_answers: list[int]
|
|
24
|
-
correct_answers: list[int]
|
|
25
|
-
max_point_value: int
|
|
26
|
-
point_value: int = 0
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@dataclass
|
|
30
|
-
class GradedSet:
|
|
31
|
-
student_info: dict[str, str]
|
|
32
|
-
graded_questions: list[GradedQuestion]
|
|
33
|
-
points: int = 0
|
|
34
|
-
max_points: int = 0
|
|
35
|
-
|
|
1
|
+
from mcqpy_core.grading.types import GradedSet
|
|
36
2
|
|
|
37
3
|
def get_grade_dataframe(
|
|
38
4
|
graded_sets: list[GradedSet], sort_key: str | None = None
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class ParsedQuestion:
|
|
6
|
+
qid: str
|
|
7
|
+
slug: str
|
|
8
|
+
answers: list[int]
|
|
9
|
+
onehot: list[int]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ParsedSet:
|
|
14
|
+
student_info: dict[str, str]
|
|
15
|
+
questions: list[ParsedQuestion]
|
|
16
|
+
file: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class GradedQuestion:
|
|
21
|
+
qid: str
|
|
22
|
+
slug: str
|
|
23
|
+
student_answers: list[int]
|
|
24
|
+
correct_answers: list[int]
|
|
25
|
+
max_point_value: int
|
|
26
|
+
point_value: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class GradedSet:
|
|
31
|
+
student_info: dict[str, str]
|
|
32
|
+
graded_questions: list[GradedQuestion]
|
|
33
|
+
points: int = 0
|
|
34
|
+
max_points: int = 0
|
|
35
|
+
|
|
36
|
+
class ParseState(Enum):
|
|
37
|
+
READER_ERROR = "Error reading PDF file. The file may be corrupted or not a valid PDF."
|
|
38
|
+
SUCCESS = "Successfully parsed PDF file."
|
|
39
|
+
|
|
40
|
+
class GradeState(Enum):
|
|
41
|
+
READER_ERROR = ParseState.READER_ERROR.value
|
|
42
|
+
SUCCESS = "Successfully graded the parsed set."
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ParseResult:
|
|
46
|
+
state: ParseState
|
|
47
|
+
parsed_set: ParsedSet | None = None
|
|
48
|
+
error_message: str | None = None
|
|
49
|
+
other_info: dict | None = None
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class GradeResult:
|
|
53
|
+
state: GradeState
|
|
54
|
+
graded_set: GradedSet | None = None
|
|
55
|
+
error_message: str | None = None
|
|
56
|
+
other_info: dict | None = None
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
CLI Build Command
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import rich_click as click
|
|
8
|
-
from rich.pretty import Pretty
|
|
9
|
-
from rich.console import Console
|
|
10
|
-
|
|
11
|
-
from mcqpy_core.cli._selection import select_questions, _build_filter, build_filters
|
|
12
|
-
from mcqpy_core.cli.config import QuizConfig
|
|
13
|
-
from mcqpy_core.cli.main import main
|
|
14
|
-
from mcqpy_pdf.compile import MultipleChoiceQuiz
|
|
15
|
-
from mcqpy_core.manifest import Manifest
|
|
16
|
-
from mcqpy_core.question import QuestionBank
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Backward-compatible private alias kept for older tests/importers.
|
|
20
|
-
_select_questions = select_questions
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def build_solution(questions, manifest, output_path: Path):
|
|
24
|
-
from mcqpy_pdf.compile.solution_pdf import SolutionPDF
|
|
25
|
-
|
|
26
|
-
solution_pdf = SolutionPDF(file=output_path, questions=questions, manifest=manifest)
|
|
27
|
-
solution_pdf.build(generate_pdf=True)
|
|
28
|
-
|
|
29
|
-
@main.command(
|
|
30
|
-
name="build",
|
|
31
|
-
help="""Build the quiz PDF from question files based on the provided configuration.
|
|
32
|
-
This command reads the quiz configuration, selects questions from the question bank,
|
|
33
|
-
and generates the quiz PDF along with a solution PDF.
|
|
34
|
-
""",
|
|
35
|
-
)
|
|
36
|
-
@click.option(
|
|
37
|
-
"-c",
|
|
38
|
-
"--config",
|
|
39
|
-
type=click.Path(exists=True, path_type=Path),
|
|
40
|
-
default="config.yaml",
|
|
41
|
-
help="Path to the config file",
|
|
42
|
-
show_default=True,
|
|
43
|
-
)
|
|
44
|
-
def build_command(config):
|
|
45
|
-
"""
|
|
46
|
-
Build the quiz PDF from question files based on the provided configuration.
|
|
47
|
-
This command reads the quiz configuration, selects questions from the question bank,
|
|
48
|
-
and generates the quiz PDF along with a solution PDF.
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
mcqpy build --config config.yaml
|
|
52
|
-
```
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
config = QuizConfig.read_yaml(config)
|
|
56
|
-
question_bank = QuestionBank.from_directories(
|
|
57
|
-
config.questions_paths, seed=config.selection.seed
|
|
58
|
-
)
|
|
59
|
-
questions = select_questions(question_bank, config.selection)
|
|
60
|
-
|
|
61
|
-
console = Console()
|
|
62
|
-
console.print("[bold green]Quiz Configuration:[/bold green]")
|
|
63
|
-
console.print(Pretty(config))
|
|
64
|
-
console.print(
|
|
65
|
-
f"[bold green]Total questions in bank:[/bold green] {len(question_bank)}"
|
|
66
|
-
)
|
|
67
|
-
console.print(f"[bold green]Selected questions:[/bold green] {len(questions)}")
|
|
68
|
-
|
|
69
|
-
## Paths:
|
|
70
|
-
for path in [config.root_directory, config.output_directory, config.submission_directory]:
|
|
71
|
-
if path and not path.exists():
|
|
72
|
-
path.mkdir(parents=True, exist_ok=True) # pragma: no cover
|
|
73
|
-
|
|
74
|
-
mcq = MultipleChoiceQuiz(
|
|
75
|
-
file=config.file_path,
|
|
76
|
-
questions=questions,
|
|
77
|
-
front_matter=config.front_matter,
|
|
78
|
-
header_footer=config.header,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
mcq.build(generate_pdf=True)
|
|
82
|
-
|
|
83
|
-
# Build solution PDF
|
|
84
|
-
manifest_path = mcq.get_manifest_path()
|
|
85
|
-
manifest = Manifest.load_from_file(manifest_path)
|
|
86
|
-
solution_output_path = (
|
|
87
|
-
config.output_directory / f"{config.file_name.replace('.pdf', '')}_solution.pdf"
|
|
88
|
-
)
|
|
89
|
-
build_solution(questions, manifest, solution_output_path)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
CLI command to check LaTeX installation and configuration.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from mcqpy_core.cli import main
|
|
6
|
-
|
|
7
|
-
@main.command('check-latex', help="Check LaTeX installation and configuration.")
|
|
8
|
-
def check_latex_command():
|
|
9
|
-
"""
|
|
10
|
-
Check if LaTeX is installed and properly configured.
|
|
11
|
-
Provides the CLI command:
|
|
12
|
-
```
|
|
13
|
-
mcqpy check-latex
|
|
14
|
-
```
|
|
15
|
-
This command verifies the presence of LaTeX tools such as `pdflatex` and `latexmk`,
|
|
16
|
-
and attempts to compile a simple LaTeX document to ensure everything is functioning correctly.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from mcqpy_pdf.utils.check_latex import check_latex_installation
|
|
20
|
-
from rich.console import Console
|
|
21
|
-
|
|
22
|
-
console = Console()
|
|
23
|
-
success, details = check_latex_installation()
|
|
24
|
-
|
|
25
|
-
if success:
|
|
26
|
-
console.print("[bold green]✓ LaTeX is properly installed![/bold green]")
|
|
27
|
-
console.print(f"[green]pdflatex version[/green]: {details['pdflatex'].version}")
|
|
28
|
-
console.print(f"[green]latexmk version[/green]: {details['latexmk'].version}")
|
|
29
|
-
console.print("[green]Compilation test passed successfully.[/green]")
|
|
30
|
-
else:
|
|
31
|
-
console.print(f"[bold red]✗ LaTeX installation issue: {details['error_message']}[/bold red]")
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"""Grade student submissions from a quiz."""
|
|
2
|
-
|
|
3
|
-
import rich_click as click
|
|
4
|
-
from mcqpy_core.cli.main import main
|
|
5
|
-
from mcqpy_core.cli.config import QuizConfig
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from mcqpy_core.grading import MCQGrader, get_grade_dataframe
|
|
9
|
-
from mcqpy_core.manifest import Manifest
|
|
10
|
-
from mcqpy_core.grading.rubric import StrictRubric
|
|
11
|
-
from rich.progress import track
|
|
12
|
-
|
|
13
|
-
from mcqpy_core.question.question_bank import QuestionBank
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@main.command(
|
|
17
|
-
name="grade",
|
|
18
|
-
help="""Grade student submissions.
|
|
19
|
-
Generates a grade report in the specified format (Excel or CSV).
|
|
20
|
-
Students submissions are expected to be in submission directory specified in the config file.
|
|
21
|
-
""",
|
|
22
|
-
)
|
|
23
|
-
@click.option(
|
|
24
|
-
"-c",
|
|
25
|
-
"--config",
|
|
26
|
-
type=click.Path(exists=True, path_type=Path),
|
|
27
|
-
default="config.yaml",
|
|
28
|
-
help="Path to the config file",
|
|
29
|
-
show_default=True,
|
|
30
|
-
)
|
|
31
|
-
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
|
|
32
|
-
@click.option(
|
|
33
|
-
"-f",
|
|
34
|
-
"--file-format",
|
|
35
|
-
type=click.Choice(["xlsx", "csv"]),
|
|
36
|
-
default="xlsx",
|
|
37
|
-
help="Output format for the grades",
|
|
38
|
-
show_default=True,
|
|
39
|
-
)
|
|
40
|
-
@click.option(
|
|
41
|
-
"-a",
|
|
42
|
-
"--analysis",
|
|
43
|
-
is_flag=True,
|
|
44
|
-
help="Generate question analysis reports",
|
|
45
|
-
default=False,
|
|
46
|
-
)
|
|
47
|
-
def grade_command(config, verbose: bool, file_format: str, analysis: bool):
|
|
48
|
-
"""
|
|
49
|
-
Grade student submissions based on the provided configuration file.
|
|
50
|
-
Generates a grade report in the specified format (Excel or CSV).
|
|
51
|
-
Students submissions are expected to be in submission directory specified in the config file.
|
|
52
|
-
|
|
53
|
-
Provides the CLI command:
|
|
54
|
-
```
|
|
55
|
-
mcqpy grade --config config.yaml --file-format xlsx --analysis
|
|
56
|
-
```
|
|
57
|
-
"""
|
|
58
|
-
# Load config
|
|
59
|
-
config = QuizConfig.read_yaml(config)
|
|
60
|
-
manifest_path = config.output_directory / f"{config.file_path.stem}_manifest.json"
|
|
61
|
-
manifest = Manifest.load_from_file(manifest_path)
|
|
62
|
-
|
|
63
|
-
# Read & Grade submissions
|
|
64
|
-
graded_sets = []
|
|
65
|
-
grader = MCQGrader(
|
|
66
|
-
manifest, StrictRubric(), regex_pattern=config.grading.anonymous_pattern
|
|
67
|
-
)
|
|
68
|
-
submissions = list(config.submission_directory.glob("*.pdf"))
|
|
69
|
-
for submission in track(
|
|
70
|
-
submissions,
|
|
71
|
-
description=f"Grading submissions ({len(submissions)})",
|
|
72
|
-
total=len(submissions),
|
|
73
|
-
):
|
|
74
|
-
graded_set = grader.grade(submission)
|
|
75
|
-
graded_sets.append(graded_set)
|
|
76
|
-
|
|
77
|
-
# Export grades to dataframe
|
|
78
|
-
df = get_grade_dataframe(graded_sets, sort_key=config.grading.output_sort_key)
|
|
79
|
-
output_path = (config.root_directory / f"{config.file_path.stem}_grades.{file_format}")
|
|
80
|
-
if file_format == "xlsx":
|
|
81
|
-
df.to_excel(output_path, index=False)
|
|
82
|
-
elif file_format == "csv":
|
|
83
|
-
df.to_csv(output_path, index=False)
|
|
84
|
-
|
|
85
|
-
if analysis:
|
|
86
|
-
from mcqpy_pdf.analysis.report import QuizAnalysis
|
|
87
|
-
|
|
88
|
-
analysis_directory = config.root_directory / "analysis/"
|
|
89
|
-
analysis_directory.mkdir(exist_ok=True)
|
|
90
|
-
|
|
91
|
-
question_bank = QuestionBank.from_directories(config.questions_paths)
|
|
92
|
-
print(f"Question bank loaded for analysis - {len(question_bank)}")
|
|
93
|
-
|
|
94
|
-
quiz_analysis = QuizAnalysis(
|
|
95
|
-
graded_sets,
|
|
96
|
-
question_bank=question_bank,
|
|
97
|
-
output_dir=analysis_directory,
|
|
98
|
-
grading_config=config.grading,
|
|
99
|
-
)
|
|
100
|
-
quiz_analysis.build()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|