mcqpy 0.1.0__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-0.1.0/PKG-INFO +118 -0
- mcqpy-0.1.0/README.md +95 -0
- mcqpy-0.1.0/pyproject.toml +56 -0
- mcqpy-0.1.0/src/mcqpy/__init__.py +2 -0
- mcqpy-0.1.0/src/mcqpy/cli/__init__.py +7 -0
- mcqpy-0.1.0/src/mcqpy/cli/autofill.py +56 -0
- mcqpy-0.1.0/src/mcqpy/cli/build.py +88 -0
- mcqpy-0.1.0/src/mcqpy/cli/check_latex.py +18 -0
- mcqpy-0.1.0/src/mcqpy/cli/config.py +43 -0
- mcqpy-0.1.0/src/mcqpy/cli/grade.py +51 -0
- mcqpy-0.1.0/src/mcqpy/cli/init.py +76 -0
- mcqpy-0.1.0/src/mcqpy/cli/main.py +10 -0
- mcqpy-0.1.0/src/mcqpy/cli/question/__init__.py +4 -0
- mcqpy-0.1.0/src/mcqpy/cli/question/init.py +25 -0
- mcqpy-0.1.0/src/mcqpy/cli/question/main.py +9 -0
- mcqpy-0.1.0/src/mcqpy/cli/question/render.py +58 -0
- mcqpy-0.1.0/src/mcqpy/cli/question/validate.py +25 -0
- mcqpy-0.1.0/src/mcqpy/compile/__init__.py +4 -0
- mcqpy-0.1.0/src/mcqpy/compile/front_config.py +12 -0
- mcqpy-0.1.0/src/mcqpy/compile/header_config.py +13 -0
- mcqpy-0.1.0/src/mcqpy/compile/latex_helpers.py +52 -0
- mcqpy-0.1.0/src/mcqpy/compile/latex_questions.py +187 -0
- mcqpy-0.1.0/src/mcqpy/compile/manifest.py +84 -0
- mcqpy-0.1.0/src/mcqpy/compile/mcq.py +176 -0
- mcqpy-0.1.0/src/mcqpy/compile/preamble.py +15 -0
- mcqpy-0.1.0/src/mcqpy/compile/solution_pdf.py +90 -0
- mcqpy-0.1.0/src/mcqpy/grade/__init__.py +2 -0
- mcqpy-0.1.0/src/mcqpy/grade/analysis.py +241 -0
- mcqpy-0.1.0/src/mcqpy/grade/grader.py +50 -0
- mcqpy-0.1.0/src/mcqpy/grade/parse_pdf.py +80 -0
- mcqpy-0.1.0/src/mcqpy/grade/rubric.py +17 -0
- mcqpy-0.1.0/src/mcqpy/grade/utils.py +34 -0
- mcqpy-0.1.0/src/mcqpy/question/__init__.py +15 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/__init__.py +8 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/base_filter.py +53 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/date.py +118 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/difficulty.py +64 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/factory.py +34 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/manifest.py +43 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/slug.py +15 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/stratified.py +46 -0
- mcqpy-0.1.0/src/mcqpy/question/filter/tag.py +49 -0
- mcqpy-0.1.0/src/mcqpy/question/question.py +378 -0
- mcqpy-0.1.0/src/mcqpy/question/question_bank.py +90 -0
- mcqpy-0.1.0/src/mcqpy/question/utils.py +91 -0
- mcqpy-0.1.0/src/mcqpy/utils/__init__.py +0 -0
- mcqpy-0.1.0/src/mcqpy/utils/check_latex.py +86 -0
- mcqpy-0.1.0/src/mcqpy/utils/fill_form.py +90 -0
- mcqpy-0.1.0/src/mcqpy/utils/image.py +92 -0
mcqpy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mcqpy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multiple choice quiz generation and grading
|
|
5
|
+
Author: Mads-Peter
|
|
6
|
+
Author-email: Mads-Peter <machri@phys.au.dk>
|
|
7
|
+
Requires-Dist: click>=8.3.0
|
|
8
|
+
Requires-Dist: matplotlib>=3.10.7
|
|
9
|
+
Requires-Dist: numpy>=2.3.3
|
|
10
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
11
|
+
Requires-Dist: pandas>=2.3.3
|
|
12
|
+
Requires-Dist: pillow>=11.3.0
|
|
13
|
+
Requires-Dist: pydantic>=2.12.0
|
|
14
|
+
Requires-Dist: pylatex>=1.4.2
|
|
15
|
+
Requires-Dist: pylatexenc>=2.10
|
|
16
|
+
Requires-Dist: pypdf>=6.1.1
|
|
17
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
18
|
+
Requires-Dist: requests>=2.28.0
|
|
19
|
+
Requires-Dist: rich>=14.2.0
|
|
20
|
+
Requires-Dist: rich-click>=1.9.3
|
|
21
|
+
Requires-Python: >=3.13
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# mcqpy
|
|
25
|
+
|
|
26
|
+
[](https://codecov.io/github/au-mbg/mcqpy)
|
|
27
|
+
|
|
28
|
+
Generate and grade interactive multiple-choice quiz PDF documents.
|
|
29
|
+
Rendered documents with
|
|
30
|
+
|
|
31
|
+
- LaTeX formatting - including for math/equations.
|
|
32
|
+
- Include figures from files or from links.
|
|
33
|
+
- Code snippets with highlighted code.
|
|
34
|
+
- Headers and footers.
|
|
35
|
+
|
|
36
|
+
Easily grade hundreds of quizzes with grades exported to `.xlsx` or `.csv` while
|
|
37
|
+
obtaining question-level statistics.
|
|
38
|
+
|
|
39
|
+
## Example
|
|
40
|
+
|
|
41
|
+
The simplest way to use `mcqpy` is through the command line interface (CLI). A project
|
|
42
|
+
can be initialized like so
|
|
43
|
+
```
|
|
44
|
+
mcqpy init <PROJECT_NAME>
|
|
45
|
+
```
|
|
46
|
+
Which will create a directory `<PROJECT_NAME>` containing the following:
|
|
47
|
+
|
|
48
|
+
- `config.yaml`: Overall configuration of the project, including author, document name, header, footer and preface options.
|
|
49
|
+
- `questions/`: A directory where the project expects question `.yaml` files to be located.
|
|
50
|
+
- `output/`: Where the built documents will be put.
|
|
51
|
+
- `submissions/`: A directory where submitted quizzes should be put for grading.
|
|
52
|
+
|
|
53
|
+
For a quiz to be interesting it needs questions, the template structure of a question
|
|
54
|
+
can be created using
|
|
55
|
+
```
|
|
56
|
+
mcqpy question init <QUESTION_PATH>
|
|
57
|
+
```
|
|
58
|
+
where `<QUESTION_PATH>` could be `questions/q1.yaml`. Once the desired number of questions have been
|
|
59
|
+
written the quiz can be compiled using
|
|
60
|
+
```
|
|
61
|
+
mcqpy build
|
|
62
|
+
```
|
|
63
|
+
Which produces a number of files in the `output/` directory the important ones being
|
|
64
|
+
|
|
65
|
+
- `<NAME>.pdf`: The quiz document
|
|
66
|
+
- `<NAME>_solution.pdf`: A human readable solution key to all questions contained in the quiz.
|
|
67
|
+
- `<NAME>_manifest.json`: Quiz key used by `mcqpy` to grade quizzes.
|
|
68
|
+
|
|
69
|
+
The `<NMAE>.pdf` document can be distributed to quiz takers through any means and once
|
|
70
|
+
returned and placed in the `submissions/` directory be graded by the program.
|
|
71
|
+
A number of test submissions can be created using
|
|
72
|
+
```
|
|
73
|
+
mcqpy test-autofill -n 50
|
|
74
|
+
```
|
|
75
|
+
Which here generates 50 randomly filled quizzes. To grade run
|
|
76
|
+
```
|
|
77
|
+
mcqpy grade -a
|
|
78
|
+
```
|
|
79
|
+
Which will produce the files `<NAME>_grades.xlsx` containing the grades of all submissions and `analysis/<NAME>_analysis.pdf` containing statistics about overall point distributions as well as question level statistics.
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
`mcqpy` requires Python, a small number of Python packages and a working LaTeX installation.
|
|
84
|
+
|
|
85
|
+
### Installing `mcqpy` in a venv.
|
|
86
|
+
|
|
87
|
+
If you have a working Python installation we recommend installing `mcqpy` in a suitable virtual environment (venv) using `pip`
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
pip install git+https://github.com/au-mbg/mcqpy.git
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Installing Python & `mcqpy`.
|
|
94
|
+
|
|
95
|
+
If you do not have a suitable Python version, `uv` can be recommended for installing and managing Python, see [Installing uv](https://docs.astral.sh/uv/getting-started/installation/). With uv installed you can create a venv with `mcqpy` using
|
|
96
|
+
```
|
|
97
|
+
uv venv
|
|
98
|
+
source .venv/bin/activate # On Mac/Linux
|
|
99
|
+
uv pip install git+https://github.com/au-mbg/mcqpy.git
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Install LaTeX
|
|
103
|
+
|
|
104
|
+
You will also need a working LaTeX installation, once `mcqpy` is installed you can check for that using
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
mcqpy check-latex
|
|
108
|
+
```
|
|
109
|
+
Which will output the versions of `pdflatex` and `latexmk` if they are installed, if not you should install an OS
|
|
110
|
+
appropriate LaTeX distribution for example from one of these sources:
|
|
111
|
+
|
|
112
|
+
- **macOS**: [MacTeX](https://www.tug.org/mactex/)
|
|
113
|
+
- **Windows**: [TeX Live](https://www.tug.org/texlive/) or [MiKTeX](https://miktex.org/)
|
|
114
|
+
- **Linux**: TeX Live (usually available through your package manager, e.g., `sudo apt install texlive-full` on Ubuntu/Debian)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
mcqpy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# mcqpy
|
|
2
|
+
|
|
3
|
+
[](https://codecov.io/github/au-mbg/mcqpy)
|
|
4
|
+
|
|
5
|
+
Generate and grade interactive multiple-choice quiz PDF documents.
|
|
6
|
+
Rendered documents with
|
|
7
|
+
|
|
8
|
+
- LaTeX formatting - including for math/equations.
|
|
9
|
+
- Include figures from files or from links.
|
|
10
|
+
- Code snippets with highlighted code.
|
|
11
|
+
- Headers and footers.
|
|
12
|
+
|
|
13
|
+
Easily grade hundreds of quizzes with grades exported to `.xlsx` or `.csv` while
|
|
14
|
+
obtaining question-level statistics.
|
|
15
|
+
|
|
16
|
+
## Example
|
|
17
|
+
|
|
18
|
+
The simplest way to use `mcqpy` is through the command line interface (CLI). A project
|
|
19
|
+
can be initialized like so
|
|
20
|
+
```
|
|
21
|
+
mcqpy init <PROJECT_NAME>
|
|
22
|
+
```
|
|
23
|
+
Which will create a directory `<PROJECT_NAME>` containing the following:
|
|
24
|
+
|
|
25
|
+
- `config.yaml`: Overall configuration of the project, including author, document name, header, footer and preface options.
|
|
26
|
+
- `questions/`: A directory where the project expects question `.yaml` files to be located.
|
|
27
|
+
- `output/`: Where the built documents will be put.
|
|
28
|
+
- `submissions/`: A directory where submitted quizzes should be put for grading.
|
|
29
|
+
|
|
30
|
+
For a quiz to be interesting it needs questions, the template structure of a question
|
|
31
|
+
can be created using
|
|
32
|
+
```
|
|
33
|
+
mcqpy question init <QUESTION_PATH>
|
|
34
|
+
```
|
|
35
|
+
where `<QUESTION_PATH>` could be `questions/q1.yaml`. Once the desired number of questions have been
|
|
36
|
+
written the quiz can be compiled using
|
|
37
|
+
```
|
|
38
|
+
mcqpy build
|
|
39
|
+
```
|
|
40
|
+
Which produces a number of files in the `output/` directory the important ones being
|
|
41
|
+
|
|
42
|
+
- `<NAME>.pdf`: The quiz document
|
|
43
|
+
- `<NAME>_solution.pdf`: A human readable solution key to all questions contained in the quiz.
|
|
44
|
+
- `<NAME>_manifest.json`: Quiz key used by `mcqpy` to grade quizzes.
|
|
45
|
+
|
|
46
|
+
The `<NMAE>.pdf` document can be distributed to quiz takers through any means and once
|
|
47
|
+
returned and placed in the `submissions/` directory be graded by the program.
|
|
48
|
+
A number of test submissions can be created using
|
|
49
|
+
```
|
|
50
|
+
mcqpy test-autofill -n 50
|
|
51
|
+
```
|
|
52
|
+
Which here generates 50 randomly filled quizzes. To grade run
|
|
53
|
+
```
|
|
54
|
+
mcqpy grade -a
|
|
55
|
+
```
|
|
56
|
+
Which will produce the files `<NAME>_grades.xlsx` containing the grades of all submissions and `analysis/<NAME>_analysis.pdf` containing statistics about overall point distributions as well as question level statistics.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
`mcqpy` requires Python, a small number of Python packages and a working LaTeX installation.
|
|
61
|
+
|
|
62
|
+
### Installing `mcqpy` in a venv.
|
|
63
|
+
|
|
64
|
+
If you have a working Python installation we recommend installing `mcqpy` in a suitable virtual environment (venv) using `pip`
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
pip install git+https://github.com/au-mbg/mcqpy.git
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Installing Python & `mcqpy`.
|
|
71
|
+
|
|
72
|
+
If you do not have a suitable Python version, `uv` can be recommended for installing and managing Python, see [Installing uv](https://docs.astral.sh/uv/getting-started/installation/). With uv installed you can create a venv with `mcqpy` using
|
|
73
|
+
```
|
|
74
|
+
uv venv
|
|
75
|
+
source .venv/bin/activate # On Mac/Linux
|
|
76
|
+
uv pip install git+https://github.com/au-mbg/mcqpy.git
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Install LaTeX
|
|
80
|
+
|
|
81
|
+
You will also need a working LaTeX installation, once `mcqpy` is installed you can check for that using
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
mcqpy check-latex
|
|
85
|
+
```
|
|
86
|
+
Which will output the versions of `pdflatex` and `latexmk` if they are installed, if not you should install an OS
|
|
87
|
+
appropriate LaTeX distribution for example from one of these sources:
|
|
88
|
+
|
|
89
|
+
- **macOS**: [MacTeX](https://www.tug.org/mactex/)
|
|
90
|
+
- **Windows**: [TeX Live](https://www.tug.org/texlive/) or [MiKTeX](https://miktex.org/)
|
|
91
|
+
- **Linux**: TeX Live (usually available through your package manager, e.g., `sudo apt install texlive-full` on Ubuntu/Debian)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcqpy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Multiple choice quiz generation and grading"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Mads-Peter", email = "machri@phys.au.dk" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"click>=8.3.0",
|
|
12
|
+
"matplotlib>=3.10.7",
|
|
13
|
+
"numpy>=2.3.3",
|
|
14
|
+
"openpyxl>=3.1.5",
|
|
15
|
+
"pandas>=2.3.3",
|
|
16
|
+
"pillow>=11.3.0",
|
|
17
|
+
"pydantic>=2.12.0",
|
|
18
|
+
"pylatex>=1.4.2",
|
|
19
|
+
"pylatexenc>=2.10",
|
|
20
|
+
"pypdf>=6.1.1",
|
|
21
|
+
"pyyaml>=6.0.3",
|
|
22
|
+
"requests>=2.28.0",
|
|
23
|
+
"rich>=14.2.0",
|
|
24
|
+
"rich-click>=1.9.3",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
mcqpy = "mcqpy.cli.main:main"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.8.18,<0.9.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
line-length = 88
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["pytest/"]
|
|
40
|
+
addopts = [
|
|
41
|
+
"--cov-report", "term",
|
|
42
|
+
"--cov-report", "html:.coverage-html",
|
|
43
|
+
"--cov-report", "xml:.coverage.xml",
|
|
44
|
+
"--junitxml", ".report.xml",
|
|
45
|
+
"--cov", "src/mcqpy"
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.coverage.run]
|
|
49
|
+
omit = []
|
|
50
|
+
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=9.0.1",
|
|
54
|
+
"pytest-cov>=7.0.0",
|
|
55
|
+
"pytest-mock>=3.15.1",
|
|
56
|
+
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from mcqpy.cli.main import main
|
|
2
|
+
from mcqpy.cli.init import init_command
|
|
3
|
+
from mcqpy.cli.build import build_command
|
|
4
|
+
from mcqpy.cli.grade import grade_command
|
|
5
|
+
from mcqpy.cli.autofill import autofill_command
|
|
6
|
+
from mcqpy.cli.question import question_group
|
|
7
|
+
from mcqpy.cli.check_latex import check_latex_command
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import io
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import rich_click as click
|
|
6
|
+
from rich.progress import track
|
|
7
|
+
|
|
8
|
+
from mcqpy.cli.config import QuizConfig
|
|
9
|
+
from mcqpy.cli.main import main
|
|
10
|
+
from mcqpy.compile.manifest import Manifest
|
|
11
|
+
from mcqpy.utils.fill_form import fill_pdf_form
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@main.command(
|
|
15
|
+
name="test-autofill",
|
|
16
|
+
help="Make answered versions of quiz to test mcqpy functionality",
|
|
17
|
+
)
|
|
18
|
+
@click.option(
|
|
19
|
+
"-c",
|
|
20
|
+
"--config",
|
|
21
|
+
type=click.Path(exists=True, path_type=Path),
|
|
22
|
+
default="config.yaml",
|
|
23
|
+
help="Path to the config file",
|
|
24
|
+
show_default=True,
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"-n",
|
|
28
|
+
"--num-forms",
|
|
29
|
+
type=int,
|
|
30
|
+
default=10,
|
|
31
|
+
help="Number of filled forms to generate",
|
|
32
|
+
show_default=True,
|
|
33
|
+
)
|
|
34
|
+
@click.option('--correct', is_flag=True, help="Fill forms with correct answers")
|
|
35
|
+
def autofill_command(config, num_forms, correct):
|
|
36
|
+
# Directories & files
|
|
37
|
+
config = QuizConfig.read_yaml(config)
|
|
38
|
+
file_path = Path(config.output_directory) / config.file_name
|
|
39
|
+
output_dir = Path(config.submission_directory)
|
|
40
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
file_name = Path(config.file_name).stem
|
|
42
|
+
manifest_path = Path(config.output_directory) / f"{file_name}_manifest.json"
|
|
43
|
+
manifest = Manifest.load_from_file(manifest_path)
|
|
44
|
+
|
|
45
|
+
# In the autofill_command function
|
|
46
|
+
for i in track(range(num_forms), description="Generating filled forms..."):
|
|
47
|
+
stdout_capture = io.StringIO()
|
|
48
|
+
stderr_capture = io.StringIO()
|
|
49
|
+
|
|
50
|
+
with (
|
|
51
|
+
contextlib.redirect_stdout(stdout_capture),
|
|
52
|
+
contextlib.redirect_stderr(stderr_capture),
|
|
53
|
+
):
|
|
54
|
+
fill_pdf_form(file_path, out_path=output_dir, index=i, manifest=manifest, correct_only=correct)
|
|
55
|
+
|
|
56
|
+
click.echo(f"Generated {num_forms} filled forms based on {file_path}")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import rich_click as click
|
|
2
|
+
import numpy as np
|
|
3
|
+
from mcqpy.cli.main import main
|
|
4
|
+
from mcqpy.cli.config import QuizConfig, SelectionConfig
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from mcqpy.question.filter import FilterFactory
|
|
7
|
+
|
|
8
|
+
from mcqpy.compile import MultipleChoiceQuiz
|
|
9
|
+
from mcqpy.question import QuestionBank
|
|
10
|
+
from mcqpy.compile.manifest import Manifest
|
|
11
|
+
|
|
12
|
+
from rich.pretty import Pretty
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_solution(questions, manifest, output_path: Path):
|
|
17
|
+
from mcqpy.compile.solution_pdf import SolutionPDF
|
|
18
|
+
solution_pdf = SolutionPDF(file=output_path, questions=questions, manifest=manifest)
|
|
19
|
+
solution_pdf.build(generate_pdf=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _select_questions(question_bank: QuestionBank, selection_config: SelectionConfig):
|
|
23
|
+
## Setup filters:
|
|
24
|
+
if selection_config.filters:
|
|
25
|
+
filter_objs = []
|
|
26
|
+
for filter_name, filter_params in selection_config.filters.items():
|
|
27
|
+
filter_config = {"type": filter_name, **filter_params}
|
|
28
|
+
filter_obj = FilterFactory.from_config(filter_config)
|
|
29
|
+
filter_objs.append(filter_obj)
|
|
30
|
+
question_bank.add_filter(filter_obj)
|
|
31
|
+
|
|
32
|
+
## Apply filters and select questions
|
|
33
|
+
questions = question_bank.get_filtered_questions(
|
|
34
|
+
number_of_questions=selection_config.number_of_questions,
|
|
35
|
+
shuffle=selection_config.shuffle,
|
|
36
|
+
sorting=selection_config.sort_type,
|
|
37
|
+
)
|
|
38
|
+
return questions
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@main.command(name="build", help="Build the quiz PDF from question files")
|
|
42
|
+
@click.option(
|
|
43
|
+
"-c",
|
|
44
|
+
"--config",
|
|
45
|
+
type=click.Path(exists=True, path_type=Path),
|
|
46
|
+
default="config.yaml",
|
|
47
|
+
help="Path to the config file",
|
|
48
|
+
show_default=True,
|
|
49
|
+
)
|
|
50
|
+
def build_command(config):
|
|
51
|
+
config = QuizConfig.read_yaml(config)
|
|
52
|
+
question_bank = QuestionBank.from_directories(config.questions_paths, seed=config.selection.seed)
|
|
53
|
+
questions = _select_questions(question_bank, config.selection)
|
|
54
|
+
|
|
55
|
+
console = Console()
|
|
56
|
+
console.print("[bold green]Quiz Configuration:[/bold green]")
|
|
57
|
+
console.print(Pretty(config))
|
|
58
|
+
console.print(f"[bold green]Total questions in bank:[/bold green] {len(question_bank)}")
|
|
59
|
+
console.print(f"[bold green]Selected questions:[/bold green] {len(questions)}")
|
|
60
|
+
|
|
61
|
+
## Paths:
|
|
62
|
+
root = Path(config.root_directory)
|
|
63
|
+
output_dir = root / config.output_directory
|
|
64
|
+
file_path = output_dir / config.file_name
|
|
65
|
+
submission_dir = (
|
|
66
|
+
root / config.submission_directory if config.submission_directory else None
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
for path in [root, output_dir, submission_dir]:
|
|
70
|
+
if path and not path.exists():
|
|
71
|
+
path.mkdir(parents=True, exist_ok=True) # pragma: no cover
|
|
72
|
+
|
|
73
|
+
mcq = MultipleChoiceQuiz(
|
|
74
|
+
file=file_path,
|
|
75
|
+
questions=questions,
|
|
76
|
+
front_matter=config.front_matter,
|
|
77
|
+
header_footer=config.header,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
mcq.build(generate_pdf=True)
|
|
81
|
+
|
|
82
|
+
# Build solution PDF
|
|
83
|
+
manifest_path = mcq.get_manifest_path()
|
|
84
|
+
manifest = Manifest.load_from_file(manifest_path)
|
|
85
|
+
solution_output_path = (
|
|
86
|
+
output_dir / f"{config.file_name.replace('.pdf', '')}_solution.pdf"
|
|
87
|
+
)
|
|
88
|
+
build_solution(questions, manifest, solution_output_path)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from mcqpy.cli import main
|
|
2
|
+
|
|
3
|
+
@main.command('check-latex', help="Check LaTeX installation and configuration.")
|
|
4
|
+
def check_latex_command():
|
|
5
|
+
from mcqpy.utils.check_latex import check_latex_installation
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
success, details = check_latex_installation()
|
|
10
|
+
|
|
11
|
+
if success:
|
|
12
|
+
console.print("[bold green]✓ LaTeX is properly installed![/bold green]")
|
|
13
|
+
console.print(f"[green]pdflatex version[/green]: {details['pdflatex'].version}")
|
|
14
|
+
console.print(f"[green]latexmk version[/green]: {details['latexmk'].version}")
|
|
15
|
+
console.print("[green]Compilation test passed successfully.[/green]")
|
|
16
|
+
else:
|
|
17
|
+
console.print(f"[bold red]✗ LaTeX installation issue: {details['error_message']}[/bold red]")
|
|
18
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
2
|
+
import yaml
|
|
3
|
+
from mcqpy.compile import HeaderFooterOptions, FrontMatterOptions
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
class SelectionConfig(BaseModel):
|
|
7
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
8
|
+
number_of_questions: int | None = Field(default=20, description="Number of questions to select")
|
|
9
|
+
filters: dict[str, dict[str, Any]] | None = Field(default=None, description="Filters to apply when selecting questions")
|
|
10
|
+
seed: int | None = Field(default=None, description="Random seed for question selection")
|
|
11
|
+
shuffle: bool = Field(default=False, description="Whether to shuffle selected questions")
|
|
12
|
+
sort_type: Literal['slug', 'none'] = Field(default='none', description="Sort type for selected questions")
|
|
13
|
+
|
|
14
|
+
class QuizConfig(BaseModel):
|
|
15
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
16
|
+
questions_paths: list[str] | str = Field(default=["questions"], description="Paths to question files or directories")
|
|
17
|
+
file_name: str = Field(default="quiz.pdf", description="Name of the output PDF file")
|
|
18
|
+
root_directory: str = Field(default=".", description="Root directory for the quiz project")
|
|
19
|
+
output_directory: str = Field(default="output", description="Directory for output files")
|
|
20
|
+
submission_directory: str | None = Field(default=None, description="Directory for submission files (if any)")
|
|
21
|
+
front_matter: FrontMatterOptions = Field(default_factory=FrontMatterOptions)
|
|
22
|
+
header: HeaderFooterOptions = Field(default_factory=HeaderFooterOptions)
|
|
23
|
+
selection: SelectionConfig = Field(default_factory=SelectionConfig)
|
|
24
|
+
|
|
25
|
+
def yaml_dump(self) -> str:
|
|
26
|
+
"""Dump the current configuration to a YAML string"""
|
|
27
|
+
config_dict = self.model_dump()
|
|
28
|
+
yaml_content = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
|
|
29
|
+
return yaml_content
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def generate_example_yaml(cls) -> str:
|
|
33
|
+
"""Generate example YAML with comments"""
|
|
34
|
+
example = cls()
|
|
35
|
+
return example.yaml_dump()
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def read_yaml(cls, file_path: str) -> "QuizConfig":
|
|
39
|
+
"""Read YAML file and return a QuizConfig instance"""
|
|
40
|
+
with open(file_path, "r") as file:
|
|
41
|
+
yaml_string = file.read()
|
|
42
|
+
data = yaml.safe_load(yaml_string)
|
|
43
|
+
return cls(**data)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import rich_click as click
|
|
2
|
+
from mcqpy.cli.main import main
|
|
3
|
+
from mcqpy.cli.config import QuizConfig
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from mcqpy.grade import MCQGrader, get_grade_dataframe
|
|
7
|
+
from mcqpy.compile.manifest import Manifest
|
|
8
|
+
from mcqpy.grade.rubric import StrictRubric
|
|
9
|
+
from rich.progress import track
|
|
10
|
+
|
|
11
|
+
from mcqpy.question.question_bank import QuestionBank
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@main.command(name="grade", help="Grade student submissions")
|
|
15
|
+
@click.option("-c", "--config", type=click.Path(exists=True, path_type=Path), default="config.yaml", help="Path to the config file", show_default=True)
|
|
16
|
+
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
|
|
17
|
+
@click.option("-f", "--file-format", type=click.Choice(["xlsx", "csv"]), default="xlsx", help="Output format for the grades", show_default=True)
|
|
18
|
+
@click.option('-a', '--analysis', is_flag=True, help="Generate question analysis reports", default=False)
|
|
19
|
+
def grade_command(config, verbose: bool, file_format: str, analysis: bool):
|
|
20
|
+
|
|
21
|
+
# Load config
|
|
22
|
+
config = QuizConfig.read_yaml(config)
|
|
23
|
+
file_name = Path(config.file_name).stem
|
|
24
|
+
manifest_path = Path(config.output_directory) / f"{file_name}_manifest.json"
|
|
25
|
+
manifest = Manifest.load_from_file(manifest_path)
|
|
26
|
+
|
|
27
|
+
# Read & Grade submissions
|
|
28
|
+
graded_sets = []
|
|
29
|
+
submissions = list(Path(config.submission_directory).glob("*.pdf"))
|
|
30
|
+
for submission in track(submissions, description=f"Grading submissions ({len(submissions)})", total=len(submissions)):
|
|
31
|
+
grader = MCQGrader(manifest, StrictRubric())
|
|
32
|
+
graded_set = grader.grade(submission)
|
|
33
|
+
graded_sets.append(graded_set)
|
|
34
|
+
|
|
35
|
+
# Export grades to dataframe
|
|
36
|
+
df = get_grade_dataframe(graded_sets)
|
|
37
|
+
output_path = Path(config.submission_directory).parent / f"{file_name}_grades.{file_format}"
|
|
38
|
+
if file_format == "xlsx":
|
|
39
|
+
df.to_excel(output_path, index=False)
|
|
40
|
+
elif file_format == "csv":
|
|
41
|
+
df.to_csv(output_path, index=False)
|
|
42
|
+
|
|
43
|
+
if analysis:
|
|
44
|
+
from mcqpy.grade.analysis import QuizAnalysis
|
|
45
|
+
analysis_directory = Path('analysis/')
|
|
46
|
+
analysis_directory.mkdir(exist_ok=True)
|
|
47
|
+
|
|
48
|
+
question_bank = QuestionBank.from_directories(config.questions_paths)
|
|
49
|
+
|
|
50
|
+
quiz_analysis = QuizAnalysis(graded_sets, question_bank=question_bank, output_dir=analysis_directory)
|
|
51
|
+
quiz_analysis.build()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import rich_click as click
|
|
2
|
+
from mcqpy.cli.main import main
|
|
3
|
+
from mcqpy.cli.config import QuizConfig
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@main.command(name="init", help="Initialize a new quiz project")
|
|
9
|
+
@click.argument("name", type=str, help="The name of the quiz project")
|
|
10
|
+
@click.option(
|
|
11
|
+
"-f",
|
|
12
|
+
"--file-name",
|
|
13
|
+
type=str,
|
|
14
|
+
default="quiz.pdf",
|
|
15
|
+
help="Name of the output PDF file",
|
|
16
|
+
show_default=True,
|
|
17
|
+
)
|
|
18
|
+
@click.option(
|
|
19
|
+
"-mqd",
|
|
20
|
+
"--make-questions-directory",
|
|
21
|
+
is_flag=True,
|
|
22
|
+
help="Create a questions directory",
|
|
23
|
+
default=True,
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"-o",
|
|
27
|
+
"--output-directory",
|
|
28
|
+
type=str,
|
|
29
|
+
default="output",
|
|
30
|
+
help="Directory for output files",
|
|
31
|
+
show_default=True,
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"-s",
|
|
35
|
+
"--submission-directory",
|
|
36
|
+
type=str,
|
|
37
|
+
default="submissions",
|
|
38
|
+
help="Directory for student submissions",
|
|
39
|
+
show_default=True,
|
|
40
|
+
)
|
|
41
|
+
def init_command(
|
|
42
|
+
name: str,
|
|
43
|
+
file_name: str,
|
|
44
|
+
make_questions_directory: bool,
|
|
45
|
+
output_directory: str,
|
|
46
|
+
submission_directory: str,
|
|
47
|
+
):
|
|
48
|
+
# Create project directory
|
|
49
|
+
project_path = Path(name)
|
|
50
|
+
project_path.mkdir(parents=True, exist_ok=False)
|
|
51
|
+
print(f"Initialized quiz project at: {project_path}")
|
|
52
|
+
|
|
53
|
+
# Create questions directory if flag is set
|
|
54
|
+
if make_questions_directory:
|
|
55
|
+
questions_dir = project_path / "questions"
|
|
56
|
+
questions_dir.mkdir(parents=True, exist_ok=False)
|
|
57
|
+
print(f"Created questions directory at: {questions_dir}")
|
|
58
|
+
|
|
59
|
+
submission_directory_path = project_path / submission_directory
|
|
60
|
+
submission_directory_path.mkdir(parents=True, exist_ok=False)
|
|
61
|
+
|
|
62
|
+
# Create example config.yaml
|
|
63
|
+
config_file = project_path / "config.yaml"
|
|
64
|
+
config = QuizConfig(
|
|
65
|
+
questions_paths=["questions"],
|
|
66
|
+
file_name=file_name,
|
|
67
|
+
output_directory=output_directory,
|
|
68
|
+
submission_directory=submission_directory,
|
|
69
|
+
)
|
|
70
|
+
config_file.write_text(config.yaml_dump())
|
|
71
|
+
print(f"Created config file at: {config_file}")
|
|
72
|
+
|
|
73
|
+
# Output directory
|
|
74
|
+
output_dir = project_path / output_directory
|
|
75
|
+
output_dir.mkdir(parents=True, exist_ok=False)
|
|
76
|
+
print(f"Created output directory at: {output_dir}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import rich_click as click
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from mcqpy.cli.question.main import question_group
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@question_group.command(name="init", help="Initialize question file.")
|
|
8
|
+
@click.argument("path", type=click.Path(exists=False))
|
|
9
|
+
def init_command(path):
|
|
10
|
+
from mcqpy.question import Question
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.pretty import Pretty
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Get the yaml schema for a question
|
|
16
|
+
schema = Question.get_yaml_template()
|
|
17
|
+
# Write to the specified path
|
|
18
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
19
|
+
f.write(schema)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|