ar-book-labels 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.
- ar_book_labels-0.1.0/LICENSE +21 -0
- ar_book_labels-0.1.0/PKG-INFO +217 -0
- ar_book_labels-0.1.0/README.md +198 -0
- ar_book_labels-0.1.0/ar_book_labels/__init__.py +5 -0
- ar_book_labels-0.1.0/ar_book_labels/__main__.py +5 -0
- ar_book_labels-0.1.0/ar_book_labels/cli.py +126 -0
- ar_book_labels-0.1.0/ar_book_labels/generator.py +327 -0
- ar_book_labels-0.1.0/ar_book_labels/templates/ar_template.xlsx +0 -0
- ar_book_labels-0.1.0/ar_book_labels.egg-info/PKG-INFO +217 -0
- ar_book_labels-0.1.0/ar_book_labels.egg-info/SOURCES.txt +15 -0
- ar_book_labels-0.1.0/ar_book_labels.egg-info/dependency_links.txt +1 -0
- ar_book_labels-0.1.0/ar_book_labels.egg-info/entry_points.txt +2 -0
- ar_book_labels-0.1.0/ar_book_labels.egg-info/requires.txt +1 -0
- ar_book_labels-0.1.0/ar_book_labels.egg-info/top_level.txt +1 -0
- ar_book_labels-0.1.0/pyproject.toml +37 -0
- ar_book_labels-0.1.0/setup.cfg +4 -0
- ar_book_labels-0.1.0/tests/test_generator.py +98 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TonyBlur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ar-book-labels
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate printable Accelerated Reader book labels from an Excel spreadsheet.
|
|
5
|
+
Author: TonyBlur
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TonyBlur/ar-book-labels
|
|
8
|
+
Project-URL: Issues, https://github.com/TonyBlur/ar-book-labels/issues
|
|
9
|
+
Keywords: accelerated-reader,labels,education,printing
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Education
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: openpyxl>=3.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# ar-book-labels
|
|
21
|
+
|
|
22
|
+
English | **[简体中文](README.zh.md)**
|
|
23
|
+
|
|
24
|
+
Generate printable [Accelerated Reader](https://www.renaissance.com/accelerated-reader/) book labels from an Excel spreadsheet. Labels include book title, author, AR level (with standard color coding), points, and quiz number — formatted for sticker-style printing and sticking on books.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Standard AR color coding**: 12 color ranges from yellow (0.1–1.5) to brown (6.6+)
|
|
29
|
+
- **Print-ready HTML output**: 4 columns x 9 rows = 36 labels per page, A4 size, `@page` CSS for direct printing
|
|
30
|
+
- **Smart text truncation**: Titles wrap to 2 lines with ellipsis; authors on 1 line
|
|
31
|
+
- **Author-first layout**: Author name appears above title for easy shelf sorting
|
|
32
|
+
- **Screen preview**: SVG viewBox scaling for crisp browser preview
|
|
33
|
+
- **Template included**: Reference Excel template with sample data and column documentation
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install ar-book-labels
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or install from source:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/TonyBlur/ar-book-labels.git
|
|
45
|
+
cd ar-book-labels
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
1. **Get the template** (optional — if you don't have an Excel file yet):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ar-book-labels --template
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This copies `ar_template.xlsx` to your current directory. Fill it with your book data.
|
|
58
|
+
|
|
59
|
+
2. **Generate labels**:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ar-book-labels books.xlsx -o labels.html
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
3. **Open** `labels.html` in a browser to preview, then print (Ctrl/Cmd+P).
|
|
66
|
+
|
|
67
|
+
> **Print tip**: In the browser print dialog, choose **"Actual size"** (or **"No margins"** / **"None"** for margins) to prevent the browser from auto-adding page margins that shift label positions. The HTML already declares `@page { margin: 0 }`; adding browser margins on top causes misalignment.
|
|
68
|
+
|
|
69
|
+
## CLI Usage
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
ar-book-labels <excel> [options]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Arguments
|
|
76
|
+
|
|
77
|
+
| Argument | Description |
|
|
78
|
+
|----------|-------------|
|
|
79
|
+
| `excel` | Path to the Excel file (.xlsx) |
|
|
80
|
+
|
|
81
|
+
### Options
|
|
82
|
+
|
|
83
|
+
| Option | Default | Description |
|
|
84
|
+
|--------|---------|-------------|
|
|
85
|
+
| `-o, --output` | `AR_Book_Labels.html` | Output HTML file path |
|
|
86
|
+
| `-s, --sheet` | first sheet | Sheet name to read (defaults to first sheet) |
|
|
87
|
+
| `--col-title` | `AR Title` | Excel column name for book title |
|
|
88
|
+
| `--col-author` | `AR Author` | Excel column name for author |
|
|
89
|
+
| `--col-level` | `Book Level` | Excel column name for book level |
|
|
90
|
+
| `--col-points` | `AR Points` | Excel column name for AR points |
|
|
91
|
+
| `--col-quiz` | `Quiz Number` | Excel column name for quiz number |
|
|
92
|
+
| `--start-row` | `2` | 1-indexed row where data begins (1 = header row) |
|
|
93
|
+
| `--scale` | `1` | Display scale factor for screen preview |
|
|
94
|
+
| `--bw` | — | Black-and-white mode: white circle with thin black outline, black level number |
|
|
95
|
+
| `--template` | — | Copy the reference Excel template to cwd and exit |
|
|
96
|
+
| `-V, --version` | — | Show version and exit |
|
|
97
|
+
|
|
98
|
+
### Examples
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Basic usage
|
|
102
|
+
ar-book-labels my_books.xlsx
|
|
103
|
+
|
|
104
|
+
# Custom output path and sheet name
|
|
105
|
+
ar-book-labels my_books.xlsx -o output/labels.html -s "Book Data"
|
|
106
|
+
|
|
107
|
+
# Custom column names (if your Excel uses different headers)
|
|
108
|
+
ar-book-labels my_books.xlsx --col-title "Title" --col-author "Author Name" --col-level "Level"
|
|
109
|
+
|
|
110
|
+
# Custom start row (e.g. data starts on row 3)
|
|
111
|
+
ar-book-labels my_books.xlsx --start-row 3
|
|
112
|
+
|
|
113
|
+
# Copy the template for reference
|
|
114
|
+
ar-book-labels --template
|
|
115
|
+
|
|
116
|
+
# Black-and-white mode for economical printing
|
|
117
|
+
ar-book-labels my_books.xlsx --bw
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Excel Format
|
|
121
|
+
|
|
122
|
+
The spreadsheet must contain these columns (default names shown; use `--col-*` options to map custom names):
|
|
123
|
+
|
|
124
|
+
| Internal Key | Default Column | Type | Description |
|
|
125
|
+
|--------------|----------------|------|-------------|
|
|
126
|
+
| `title` | `AR Title` | text | Book title |
|
|
127
|
+
| `author` | `AR Author` | text | Author name |
|
|
128
|
+
| `level` | `Book Level` | number | AR ATOS level (e.g. 5.1) |
|
|
129
|
+
| `points` | `AR Points` | number | Points value |
|
|
130
|
+
| `quiz` | `Quiz Number` | number/text | Quiz ID |
|
|
131
|
+
|
|
132
|
+
Rows with missing required fields are skipped with a warning printed to stderr.
|
|
133
|
+
|
|
134
|
+
Use `ar-book-labels --template` to get a pre-formatted template with sample data.
|
|
135
|
+
|
|
136
|
+
## AR Level Color Chart
|
|
137
|
+
|
|
138
|
+
| Level Range | Color | Hex |
|
|
139
|
+
|-------------|-------|-----|
|
|
140
|
+
| 0.1 – 1.5 | Yellow | `#FFD700` |
|
|
141
|
+
| 1.6 – 2.0 | Green | `#2E8B57` |
|
|
142
|
+
| 2.1 – 2.5 | Dark Blue | `#00008B` |
|
|
143
|
+
| 2.6 – 3.0 | Red | `#DC143C` |
|
|
144
|
+
| 3.1 – 3.5 | Pink | `#FF69B4` |
|
|
145
|
+
| 3.6 – 4.0 | Purple | `#800080` |
|
|
146
|
+
| 4.1 – 4.5 | Orange | `#FF8C00` |
|
|
147
|
+
| 4.6 – 5.0 | Light Blue | `#00BFFF` |
|
|
148
|
+
| 5.1 – 5.5 | Neon Orange | `#FF6600` |
|
|
149
|
+
| 5.6 – 6.0 | Neon Green | `#39FF14` |
|
|
150
|
+
| 6.1 – 6.5 | Black | `#1C1C1C` |
|
|
151
|
+
| 6.6+ | Brown | `#8B4513` |
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
### Setup
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git clone https://github.com/TonyBlur/ar-book-labels.git
|
|
159
|
+
cd ar-book-labels
|
|
160
|
+
python -m venv .venv
|
|
161
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
162
|
+
pip install -e ".[dev]"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Project Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
ar-book-labels/
|
|
169
|
+
ar_book_labels/
|
|
170
|
+
__init__.py # Package metadata and public API
|
|
171
|
+
generator.py # Core label generation logic
|
|
172
|
+
cli.py # CLI entry point (argparse)
|
|
173
|
+
__main__.py # python -m ar_book_labels support
|
|
174
|
+
templates/
|
|
175
|
+
ar_template.xlsx # Reference Excel template
|
|
176
|
+
tests/
|
|
177
|
+
test_generator.py # Unit tests
|
|
178
|
+
pyproject.toml # Build configuration (setuptools)
|
|
179
|
+
LICENSE # MIT License
|
|
180
|
+
README.md # This file (English)
|
|
181
|
+
README.zh.md # 中文文档
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Running Tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python -m pytest tests/ -v
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Building & Publishing
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install build twine
|
|
194
|
+
python -m build
|
|
195
|
+
twine check dist/*
|
|
196
|
+
twine upload dist/*
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Automated Publishing (GitHub Actions)
|
|
200
|
+
|
|
201
|
+
This project uses a GitHub Actions workflow to automatically publish to PyPI when a new release is created:
|
|
202
|
+
|
|
203
|
+
1. Bump the version in `pyproject.toml`
|
|
204
|
+
2. Create a new release on GitHub (via the web UI or `gh release create`)
|
|
205
|
+
3. The `publish.yml` workflow will automatically build the package and publish it to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no API token needed)
|
|
206
|
+
|
|
207
|
+
**Prerequisites**: Configure a [Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) on PyPI for the `ar-book-labels` project, linking it to the `TonyBlur/ar-book-labels` repository and the `pypi` environment.
|
|
208
|
+
|
|
209
|
+
### Code Style
|
|
210
|
+
|
|
211
|
+
- Python 3.8+
|
|
212
|
+
- No external dependencies beyond `openpyxl`
|
|
213
|
+
- Keep the generator logic self-contained and testable
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# ar-book-labels
|
|
2
|
+
|
|
3
|
+
English | **[简体中文](README.zh.md)**
|
|
4
|
+
|
|
5
|
+
Generate printable [Accelerated Reader](https://www.renaissance.com/accelerated-reader/) book labels from an Excel spreadsheet. Labels include book title, author, AR level (with standard color coding), points, and quiz number — formatted for sticker-style printing and sticking on books.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Standard AR color coding**: 12 color ranges from yellow (0.1–1.5) to brown (6.6+)
|
|
10
|
+
- **Print-ready HTML output**: 4 columns x 9 rows = 36 labels per page, A4 size, `@page` CSS for direct printing
|
|
11
|
+
- **Smart text truncation**: Titles wrap to 2 lines with ellipsis; authors on 1 line
|
|
12
|
+
- **Author-first layout**: Author name appears above title for easy shelf sorting
|
|
13
|
+
- **Screen preview**: SVG viewBox scaling for crisp browser preview
|
|
14
|
+
- **Template included**: Reference Excel template with sample data and column documentation
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install ar-book-labels
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install from source:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/TonyBlur/ar-book-labels.git
|
|
26
|
+
cd ar-book-labels
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
1. **Get the template** (optional — if you don't have an Excel file yet):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
ar-book-labels --template
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This copies `ar_template.xlsx` to your current directory. Fill it with your book data.
|
|
39
|
+
|
|
40
|
+
2. **Generate labels**:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ar-book-labels books.xlsx -o labels.html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
3. **Open** `labels.html` in a browser to preview, then print (Ctrl/Cmd+P).
|
|
47
|
+
|
|
48
|
+
> **Print tip**: In the browser print dialog, choose **"Actual size"** (or **"No margins"** / **"None"** for margins) to prevent the browser from auto-adding page margins that shift label positions. The HTML already declares `@page { margin: 0 }`; adding browser margins on top causes misalignment.
|
|
49
|
+
|
|
50
|
+
## CLI Usage
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
ar-book-labels <excel> [options]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Arguments
|
|
57
|
+
|
|
58
|
+
| Argument | Description |
|
|
59
|
+
|----------|-------------|
|
|
60
|
+
| `excel` | Path to the Excel file (.xlsx) |
|
|
61
|
+
|
|
62
|
+
### Options
|
|
63
|
+
|
|
64
|
+
| Option | Default | Description |
|
|
65
|
+
|--------|---------|-------------|
|
|
66
|
+
| `-o, --output` | `AR_Book_Labels.html` | Output HTML file path |
|
|
67
|
+
| `-s, --sheet` | first sheet | Sheet name to read (defaults to first sheet) |
|
|
68
|
+
| `--col-title` | `AR Title` | Excel column name for book title |
|
|
69
|
+
| `--col-author` | `AR Author` | Excel column name for author |
|
|
70
|
+
| `--col-level` | `Book Level` | Excel column name for book level |
|
|
71
|
+
| `--col-points` | `AR Points` | Excel column name for AR points |
|
|
72
|
+
| `--col-quiz` | `Quiz Number` | Excel column name for quiz number |
|
|
73
|
+
| `--start-row` | `2` | 1-indexed row where data begins (1 = header row) |
|
|
74
|
+
| `--scale` | `1` | Display scale factor for screen preview |
|
|
75
|
+
| `--bw` | — | Black-and-white mode: white circle with thin black outline, black level number |
|
|
76
|
+
| `--template` | — | Copy the reference Excel template to cwd and exit |
|
|
77
|
+
| `-V, --version` | — | Show version and exit |
|
|
78
|
+
|
|
79
|
+
### Examples
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Basic usage
|
|
83
|
+
ar-book-labels my_books.xlsx
|
|
84
|
+
|
|
85
|
+
# Custom output path and sheet name
|
|
86
|
+
ar-book-labels my_books.xlsx -o output/labels.html -s "Book Data"
|
|
87
|
+
|
|
88
|
+
# Custom column names (if your Excel uses different headers)
|
|
89
|
+
ar-book-labels my_books.xlsx --col-title "Title" --col-author "Author Name" --col-level "Level"
|
|
90
|
+
|
|
91
|
+
# Custom start row (e.g. data starts on row 3)
|
|
92
|
+
ar-book-labels my_books.xlsx --start-row 3
|
|
93
|
+
|
|
94
|
+
# Copy the template for reference
|
|
95
|
+
ar-book-labels --template
|
|
96
|
+
|
|
97
|
+
# Black-and-white mode for economical printing
|
|
98
|
+
ar-book-labels my_books.xlsx --bw
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Excel Format
|
|
102
|
+
|
|
103
|
+
The spreadsheet must contain these columns (default names shown; use `--col-*` options to map custom names):
|
|
104
|
+
|
|
105
|
+
| Internal Key | Default Column | Type | Description |
|
|
106
|
+
|--------------|----------------|------|-------------|
|
|
107
|
+
| `title` | `AR Title` | text | Book title |
|
|
108
|
+
| `author` | `AR Author` | text | Author name |
|
|
109
|
+
| `level` | `Book Level` | number | AR ATOS level (e.g. 5.1) |
|
|
110
|
+
| `points` | `AR Points` | number | Points value |
|
|
111
|
+
| `quiz` | `Quiz Number` | number/text | Quiz ID |
|
|
112
|
+
|
|
113
|
+
Rows with missing required fields are skipped with a warning printed to stderr.
|
|
114
|
+
|
|
115
|
+
Use `ar-book-labels --template` to get a pre-formatted template with sample data.
|
|
116
|
+
|
|
117
|
+
## AR Level Color Chart
|
|
118
|
+
|
|
119
|
+
| Level Range | Color | Hex |
|
|
120
|
+
|-------------|-------|-----|
|
|
121
|
+
| 0.1 – 1.5 | Yellow | `#FFD700` |
|
|
122
|
+
| 1.6 – 2.0 | Green | `#2E8B57` |
|
|
123
|
+
| 2.1 – 2.5 | Dark Blue | `#00008B` |
|
|
124
|
+
| 2.6 – 3.0 | Red | `#DC143C` |
|
|
125
|
+
| 3.1 – 3.5 | Pink | `#FF69B4` |
|
|
126
|
+
| 3.6 – 4.0 | Purple | `#800080` |
|
|
127
|
+
| 4.1 – 4.5 | Orange | `#FF8C00` |
|
|
128
|
+
| 4.6 – 5.0 | Light Blue | `#00BFFF` |
|
|
129
|
+
| 5.1 – 5.5 | Neon Orange | `#FF6600` |
|
|
130
|
+
| 5.6 – 6.0 | Neon Green | `#39FF14` |
|
|
131
|
+
| 6.1 – 6.5 | Black | `#1C1C1C` |
|
|
132
|
+
| 6.6+ | Brown | `#8B4513` |
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
### Setup
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
git clone https://github.com/TonyBlur/ar-book-labels.git
|
|
140
|
+
cd ar-book-labels
|
|
141
|
+
python -m venv .venv
|
|
142
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
143
|
+
pip install -e ".[dev]"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Project Structure
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
ar-book-labels/
|
|
150
|
+
ar_book_labels/
|
|
151
|
+
__init__.py # Package metadata and public API
|
|
152
|
+
generator.py # Core label generation logic
|
|
153
|
+
cli.py # CLI entry point (argparse)
|
|
154
|
+
__main__.py # python -m ar_book_labels support
|
|
155
|
+
templates/
|
|
156
|
+
ar_template.xlsx # Reference Excel template
|
|
157
|
+
tests/
|
|
158
|
+
test_generator.py # Unit tests
|
|
159
|
+
pyproject.toml # Build configuration (setuptools)
|
|
160
|
+
LICENSE # MIT License
|
|
161
|
+
README.md # This file (English)
|
|
162
|
+
README.zh.md # 中文文档
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Running Tests
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python -m pytest tests/ -v
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Building & Publishing
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pip install build twine
|
|
175
|
+
python -m build
|
|
176
|
+
twine check dist/*
|
|
177
|
+
twine upload dist/*
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Automated Publishing (GitHub Actions)
|
|
181
|
+
|
|
182
|
+
This project uses a GitHub Actions workflow to automatically publish to PyPI when a new release is created:
|
|
183
|
+
|
|
184
|
+
1. Bump the version in `pyproject.toml`
|
|
185
|
+
2. Create a new release on GitHub (via the web UI or `gh release create`)
|
|
186
|
+
3. The `publish.yml` workflow will automatically build the package and publish it to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no API token needed)
|
|
187
|
+
|
|
188
|
+
**Prerequisites**: Configure a [Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) on PyPI for the `ar-book-labels` project, linking it to the `TonyBlur/ar-book-labels` repository and the `pypi` environment.
|
|
189
|
+
|
|
190
|
+
### Code Style
|
|
191
|
+
|
|
192
|
+
- Python 3.8+
|
|
193
|
+
- No external dependencies beyond `openpyxl`
|
|
194
|
+
- Keep the generator logic self-contained and testable
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""CLI entry point for ar-book-labels."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ar_book_labels import __version__
|
|
8
|
+
from ar_book_labels.generator import generate, DEFAULT_COLUMNS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="ar-book-labels",
|
|
14
|
+
description="Generate printable Accelerated Reader book labels from an Excel file.",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("excel", nargs="?", help="Path to the Excel file (.xlsx)")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"-o", "--output", default="AR_Book_Labels.html",
|
|
19
|
+
help="Output HTML file path (default: AR_Book_Labels.html)",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-s", "--sheet", default=None,
|
|
23
|
+
help="Sheet name to read (default: first sheet in the workbook)",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--col-title", default=DEFAULT_COLUMNS["title"],
|
|
27
|
+
help=f"Excel column name for book title (default: {DEFAULT_COLUMNS['title']})",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--col-author", default=DEFAULT_COLUMNS["author"],
|
|
31
|
+
help=f"Excel column name for author (default: {DEFAULT_COLUMNS['author']})",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--col-level", default=DEFAULT_COLUMNS["level"],
|
|
35
|
+
help=f"Excel column name for book level (default: {DEFAULT_COLUMNS['level']})",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--col-points", default=DEFAULT_COLUMNS["points"],
|
|
39
|
+
help=f"Excel column name for AR points (default: {DEFAULT_COLUMNS['points']})",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--col-quiz", default=DEFAULT_COLUMNS["quiz"],
|
|
43
|
+
help=f"Excel column name for quiz number (default: {DEFAULT_COLUMNS['quiz']})",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--start-row", type=int, default=2,
|
|
47
|
+
help="1-indexed row number where data begins (default: 2, i.e. row 1 is header)",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--scale", type=int, default=1,
|
|
51
|
+
help="Display scale factor for screen preview (default: 1)",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--bw", action="store_true",
|
|
55
|
+
help="Black-and-white mode: white circle with thin black outline, black level number",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--template", action="store_true",
|
|
59
|
+
help="Copy the reference Excel template to the current directory and exit",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"-V", "--version", action="version", version=f"%(prog)s {__version__}",
|
|
63
|
+
)
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
|
|
66
|
+
if args.template:
|
|
67
|
+
_copy_template()
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if not args.excel:
|
|
71
|
+
parser.error("the following arguments are required: excel")
|
|
72
|
+
|
|
73
|
+
excel_path = Path(args.excel)
|
|
74
|
+
if not excel_path.exists():
|
|
75
|
+
print(f"Error: file not found: {excel_path}", file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
output_path = Path(args.output)
|
|
79
|
+
|
|
80
|
+
# Build column mapping from CLI args
|
|
81
|
+
column_mapping = {
|
|
82
|
+
"title": args.col_title,
|
|
83
|
+
"author": args.col_author,
|
|
84
|
+
"level": args.col_level,
|
|
85
|
+
"points": args.col_points,
|
|
86
|
+
"quiz": args.col_quiz,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
n_books, n_pages, warnings = generate(
|
|
91
|
+
excel_path=str(excel_path),
|
|
92
|
+
output_path=str(output_path),
|
|
93
|
+
sheet_name=args.sheet,
|
|
94
|
+
column_mapping=column_mapping,
|
|
95
|
+
start_row=args.start_row,
|
|
96
|
+
display_scale=args.scale,
|
|
97
|
+
bw=args.bw,
|
|
98
|
+
)
|
|
99
|
+
except KeyError as e:
|
|
100
|
+
print(f"Error: sheet not found: {e}", file=sys.stderr)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
except ValueError as e:
|
|
103
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
# Print warnings to stderr
|
|
107
|
+
for w in warnings:
|
|
108
|
+
print(f"Warning: {w}", file=sys.stderr)
|
|
109
|
+
|
|
110
|
+
if n_books == 0:
|
|
111
|
+
print("Warning: no books found in the spreadsheet.", file=sys.stderr)
|
|
112
|
+
sys.exit(0)
|
|
113
|
+
|
|
114
|
+
print(f"Generated {n_books} labels ({n_pages} pages) -> {output_path}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _copy_template():
|
|
118
|
+
import shutil
|
|
119
|
+
src = Path(__file__).parent / "templates" / "ar_template.xlsx"
|
|
120
|
+
dst = Path.cwd() / "ar_template.xlsx"
|
|
121
|
+
shutil.copy2(src, dst)
|
|
122
|
+
print(f"Template copied to: {dst}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
main()
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Core label generation logic."""
|
|
2
|
+
|
|
3
|
+
import html as html_module
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from openpyxl import load_workbook
|
|
7
|
+
|
|
8
|
+
# ==================== AR Level Standard Colors ====================
|
|
9
|
+
LEVEL_COLORS = [
|
|
10
|
+
(0.1, 1.5, "#FFD700"), # yellow
|
|
11
|
+
(1.6, 2.0, "#2E8B57"), # green
|
|
12
|
+
(2.1, 2.5, "#00008B"), # dark blue
|
|
13
|
+
(2.6, 3.0, "#DC143C"), # red
|
|
14
|
+
(3.1, 3.5, "#FF69B4"), # pink
|
|
15
|
+
(3.6, 4.0, "#800080"), # purple
|
|
16
|
+
(4.1, 4.5, "#FF8C00"), # orange
|
|
17
|
+
(4.6, 5.0, "#00BFFF"), # light blue
|
|
18
|
+
(5.1, 5.5, "#FF6600"), # neon orange
|
|
19
|
+
(5.6, 6.0, "#39FF14"), # neon green
|
|
20
|
+
(6.1, 6.5, "#1C1C1C"), # black
|
|
21
|
+
(6.6, 99.0, "#8B4513"), # brown
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# ==================== SVG Layout Constants ====================
|
|
25
|
+
# Page (A4 portrait, units in mm)
|
|
26
|
+
PAGE_W = 210
|
|
27
|
+
PAGE_H = 297
|
|
28
|
+
|
|
29
|
+
# Grid: 4 columns x 9 rows = 36 labels per page
|
|
30
|
+
COLS_X = [2, 54, 106, 158]
|
|
31
|
+
ROWS_Y = [13.5, 43.5, 73.5, 103.5, 133.5, 163.5, 193.5, 223.5, 253.5]
|
|
32
|
+
LABEL_W = 50
|
|
33
|
+
LABEL_H = 30
|
|
34
|
+
LABEL_RX = 4
|
|
35
|
+
LABELS_PER_PAGE = len(COLS_X) * len(ROWS_Y) # 36
|
|
36
|
+
|
|
37
|
+
FONT = "'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif"
|
|
38
|
+
|
|
39
|
+
# Default column names (matching the template)
|
|
40
|
+
DEFAULT_COLUMNS = {
|
|
41
|
+
"title": "AR Title",
|
|
42
|
+
"author": "AR Author",
|
|
43
|
+
"level": "Book Level",
|
|
44
|
+
"points": "AR Points",
|
|
45
|
+
"quiz": "Quiz Number",
|
|
46
|
+
}
|
|
47
|
+
REQUIRED_FIELDS = ["title", "author", "level", "points", "quiz"]
|
|
48
|
+
|
|
49
|
+
# Label internal coordinates (50 wide x 30 high, units in mm)
|
|
50
|
+
CX, CY, CR = 11, 18, 6.5 # Badge circle: cx=11, cy=18(下移2), bottom=24.5
|
|
51
|
+
RX = 21 # Right-side text area x start
|
|
52
|
+
VX = 34 # Points/Quiz value x (center anchor, 右移4)
|
|
53
|
+
TOP_Y = 4 # Top text baseline/start y
|
|
54
|
+
AUTHOR_LH = 3.2 # Author line height
|
|
55
|
+
TITLE_LH = 3.6 # Title line height
|
|
56
|
+
POINTS_Y = 17.2 # Points row (下移2)
|
|
57
|
+
QUIZ_Y = 21.7 # Quiz row (下移2); bottom ≈ 24.5 = circle bottom
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_level_color(level):
|
|
61
|
+
try:
|
|
62
|
+
level = float(level)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return "#999999"
|
|
65
|
+
for low, high, color in LEVEL_COLORS:
|
|
66
|
+
if low <= level <= high:
|
|
67
|
+
return color
|
|
68
|
+
return "#999999"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_badge_text_color(bg_color):
|
|
72
|
+
light = {"#FFD700", "#39FF14"}
|
|
73
|
+
return "#000000" if bg_color in light else "#FFFFFF"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def split_text_lines(text, max_chars_per_line):
|
|
77
|
+
"""Wrap text into at most 2 lines; truncate with ellipsis only if >2 lines."""
|
|
78
|
+
text = str(text).strip()
|
|
79
|
+
if not text:
|
|
80
|
+
return [text]
|
|
81
|
+
if len(text) <= max_chars_per_line:
|
|
82
|
+
return [text]
|
|
83
|
+
split_pos = text.rfind(" ", 0, max_chars_per_line)
|
|
84
|
+
if split_pos == -1:
|
|
85
|
+
split_pos = max_chars_per_line
|
|
86
|
+
line1 = text[:split_pos].rstrip()
|
|
87
|
+
remainder = text[split_pos:].lstrip()
|
|
88
|
+
if len(remainder) <= max_chars_per_line:
|
|
89
|
+
return [line1, remainder]
|
|
90
|
+
return [line1, remainder[:max_chars_per_line - 1].rstrip() + "\u2026"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def read_books(excel_path, sheet_name, columns=None, start_row=2):
|
|
94
|
+
"""Read book data from an Excel file.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
excel_path: Path to the .xlsx file.
|
|
98
|
+
sheet_name: Name of the sheet to read.
|
|
99
|
+
columns: Dict mapping internal keys to Excel column names.
|
|
100
|
+
Missing keys fall back to DEFAULT_COLUMNS.
|
|
101
|
+
start_row: 1-indexed row number where data begins (1 = header row).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
tuple: (books_list, warnings_list)
|
|
105
|
+
"""
|
|
106
|
+
cols = {**DEFAULT_COLUMNS, **(columns or {})}
|
|
107
|
+
wb = load_workbook(excel_path, read_only=True, data_only=True)
|
|
108
|
+
if sheet_name is None:
|
|
109
|
+
ws = wb.worksheets[0]
|
|
110
|
+
else:
|
|
111
|
+
ws = wb[sheet_name]
|
|
112
|
+
actual_sheet_name = ws.title
|
|
113
|
+
rows = list(ws.iter_rows(values_only=True))
|
|
114
|
+
headers = [str(h) for h in rows[0] if h is not None]
|
|
115
|
+
|
|
116
|
+
# Check that required columns exist in the sheet
|
|
117
|
+
missing_cols = []
|
|
118
|
+
for key in REQUIRED_FIELDS:
|
|
119
|
+
if cols[key] not in headers:
|
|
120
|
+
missing_cols.append(f"{key} ('{cols[key]}')")
|
|
121
|
+
if missing_cols:
|
|
122
|
+
wb.close()
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"Columns not found in sheet '{actual_sheet_name}': {', '.join(missing_cols)}.\n"
|
|
125
|
+
f"Available columns: {', '.join(headers)}\n"
|
|
126
|
+
f"Use --col-* options to map your column names."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
books = []
|
|
130
|
+
warnings = []
|
|
131
|
+
data_rows = rows[start_row - 1:] # start_row is 1-indexed
|
|
132
|
+
for i, row in enumerate(data_rows):
|
|
133
|
+
row_num = start_row + i
|
|
134
|
+
vals = list(row[:len(headers)])
|
|
135
|
+
if all(v is None for v in vals):
|
|
136
|
+
continue
|
|
137
|
+
raw = dict(zip(headers, vals))
|
|
138
|
+
|
|
139
|
+
# Validate required fields
|
|
140
|
+
missing = []
|
|
141
|
+
for key in REQUIRED_FIELDS:
|
|
142
|
+
val = raw.get(cols[key])
|
|
143
|
+
if val is None or (isinstance(val, str) and not val.strip()):
|
|
144
|
+
missing.append(cols[key])
|
|
145
|
+
if missing:
|
|
146
|
+
warnings.append(f"Row {row_num}: skipped — missing: {', '.join(missing)}")
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
books.append({
|
|
150
|
+
"title": raw[cols["title"]],
|
|
151
|
+
"author": raw[cols["author"]],
|
|
152
|
+
"level": raw[cols["level"]],
|
|
153
|
+
"points": raw[cols["points"]],
|
|
154
|
+
"quiz": raw[cols["quiz"]],
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
wb.close()
|
|
158
|
+
return books, warnings
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _generate_label_svg(book, bw=False):
|
|
162
|
+
"""Generate SVG markup for a single label.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
book: Dict with title, author, level, points, quiz.
|
|
166
|
+
bw: When True, render in black-and-white mode — white circle with a
|
|
167
|
+
thin black outline and a black level number. When False (default),
|
|
168
|
+
use the standard AR color-coded badge.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
SVG markup string for the label (a ``<g>`` element).
|
|
172
|
+
"""
|
|
173
|
+
title_raw = book.get("title", "") or ""
|
|
174
|
+
level = book.get("level", 0)
|
|
175
|
+
points = str(book.get("points", ""))
|
|
176
|
+
quiz = str(book.get("quiz", ""))
|
|
177
|
+
|
|
178
|
+
level_str = f"{float(level):.1f}" if isinstance(level, (int, float)) else str(level)
|
|
179
|
+
badge_color = get_level_color(level)
|
|
180
|
+
badge_text = get_badge_text_color(badge_color)
|
|
181
|
+
|
|
182
|
+
# bw mode overrides the badge fill/outline and the level number color.
|
|
183
|
+
circle_fill = "white" if bw else badge_color
|
|
184
|
+
circle_stroke = ' stroke="black" stroke-width="0.3"' if bw else ""
|
|
185
|
+
level_fill = "black" if bw else badge_text
|
|
186
|
+
|
|
187
|
+
author_raw = str(book.get("author", "") or "").strip()
|
|
188
|
+
author_display = html_module.escape(
|
|
189
|
+
author_raw if len(author_raw) <= 17 else author_raw[:16].rstrip() + "\u2026"
|
|
190
|
+
)
|
|
191
|
+
title_lines = [html_module.escape(l) for l in split_text_lines(title_raw, 14)]
|
|
192
|
+
title_start = TOP_Y + AUTHOR_LH + 0.5
|
|
193
|
+
title_ys = [title_start + i * TITLE_LH for i in range(len(title_lines))]
|
|
194
|
+
|
|
195
|
+
p = []
|
|
196
|
+
p.append(f'<rect class="label-outline" x="0" y="0" width="{LABEL_W}" height="{LABEL_H}" rx="{LABEL_RX}" fill="none"/>')
|
|
197
|
+
p.append(f'<circle cx="{CX}" cy="{CY}" r="{CR}" fill="{circle_fill}"{circle_stroke}/>')
|
|
198
|
+
p.append(f'<text x="{CX}" y="{TOP_Y}" text-anchor="middle" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="4.5" font-weight="700" letter-spacing="0.3">ATOS</text>')
|
|
199
|
+
p.append(f'<text x="{CX}" y="{CY + 2.5}" text-anchor="middle" fill="{level_fill}" font-family="{FONT}" font-size="5.5" font-weight="700">{level_str}</text>')
|
|
200
|
+
p.append(f'<text x="{RX}" y="{TOP_Y}" dominant-baseline="hanging" fill="#555" font-family="{FONT}" font-size="2.6">{author_display}</text>')
|
|
201
|
+
for y in title_ys:
|
|
202
|
+
p.append(f'<text x="{RX}" y="{y}" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="3.2" font-weight="600">{title_lines[title_ys.index(y)]}</text>')
|
|
203
|
+
p.append(f'<text x="{RX}" y="{POINTS_Y}" dominant-baseline="hanging" fill="#666" font-family="{FONT}" font-size="2.5">Points:</text>')
|
|
204
|
+
p.append(f'<text x="{VX}" y="{POINTS_Y}" text-anchor="middle" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="2.8" font-weight="700">{points}</text>')
|
|
205
|
+
p.append(f'<text x="{RX}" y="{QUIZ_Y}" dominant-baseline="hanging" fill="#666" font-family="{FONT}" font-size="2.5">Quiz:</text>')
|
|
206
|
+
p.append(f'<text x="{VX}" y="{QUIZ_Y}" text-anchor="middle" dominant-baseline="hanging" fill="black" font-family="{FONT}" font-size="2.8" font-weight="700">{quiz}</text>')
|
|
207
|
+
|
|
208
|
+
return "<g>\n " + "\n ".join(p) + "\n</g>"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def build_html(books, display_scale=1, bw=False):
|
|
212
|
+
"""Build a multi-page HTML document with SVG labels.
|
|
213
|
+
|
|
214
|
+
Page dimensions are in millimetres (A4 portrait: 210mm x 297mm) so that
|
|
215
|
+
printing is 1:1. On screen, each page is enlarged via CSS ``transform:
|
|
216
|
+
scale(display_scale)``; ``@media print`` resets the transform to guarantee
|
|
217
|
+
exact physical dimensions.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
books: List of book dicts.
|
|
221
|
+
display_scale: Scale factor for screen preview.
|
|
222
|
+
bw: When True, render labels in black-and-white mode.
|
|
223
|
+
"""
|
|
224
|
+
pages = []
|
|
225
|
+
for i in range(0, len(books), LABELS_PER_PAGE):
|
|
226
|
+
pages.append(books[i:i + LABELS_PER_PAGE])
|
|
227
|
+
|
|
228
|
+
s = display_scale
|
|
229
|
+
|
|
230
|
+
page_blocks = []
|
|
231
|
+
for page_idx, page_books in enumerate(pages):
|
|
232
|
+
labels_svg = ""
|
|
233
|
+
for i, book in enumerate(page_books):
|
|
234
|
+
row = i // len(COLS_X)
|
|
235
|
+
col = i % len(COLS_X)
|
|
236
|
+
x = COLS_X[col]
|
|
237
|
+
y = ROWS_Y[row]
|
|
238
|
+
labels_svg += f'<g transform="translate({x},{y})">{_generate_label_svg(book, bw=bw)}</g>\n'
|
|
239
|
+
|
|
240
|
+
pb = ' style="page-break-after: always;"' if page_idx < len(pages) - 1 else ''
|
|
241
|
+
page_blocks.append(f'''<div class="page"{pb}>
|
|
242
|
+
<svg width="{PAGE_W}mm" height="{PAGE_H}mm" viewBox="0 0 {PAGE_W} {PAGE_H}" xmlns="http://www.w3.org/2000/svg">
|
|
243
|
+
<rect width="{PAGE_W}" height="{PAGE_H}" fill="white"/>
|
|
244
|
+
{labels_svg}
|
|
245
|
+
</svg>
|
|
246
|
+
</div>''')
|
|
247
|
+
|
|
248
|
+
return f'''<!DOCTYPE html>
|
|
249
|
+
<html lang="en">
|
|
250
|
+
<head>
|
|
251
|
+
<meta charset="UTF-8">
|
|
252
|
+
<title>AR Book Labels</title>
|
|
253
|
+
<style>
|
|
254
|
+
@page {{ size: {PAGE_W}mm {PAGE_H}mm; margin: 0; }}
|
|
255
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
256
|
+
body {{
|
|
257
|
+
background: #e0e0e0;
|
|
258
|
+
-webkit-print-color-adjust: exact !important;
|
|
259
|
+
print-color-adjust: exact !important;
|
|
260
|
+
color-adjust: exact !important;
|
|
261
|
+
}}
|
|
262
|
+
.page {{
|
|
263
|
+
width: {PAGE_W}mm;
|
|
264
|
+
height: {PAGE_H}mm;
|
|
265
|
+
margin: 0 auto;
|
|
266
|
+
margin-bottom: calc(10px + {PAGE_H}mm * ({s} - 1));
|
|
267
|
+
background: white;
|
|
268
|
+
transform: scale({s});
|
|
269
|
+
transform-origin: top center;
|
|
270
|
+
}}
|
|
271
|
+
.page svg {{ display: block; }}
|
|
272
|
+
.label-outline {{
|
|
273
|
+
fill: none;
|
|
274
|
+
stroke: #999999;
|
|
275
|
+
stroke-width: 0.2;
|
|
276
|
+
stroke-dasharray: 1.2, 1.2;
|
|
277
|
+
}}
|
|
278
|
+
@media print {{
|
|
279
|
+
body {{ margin: 0; background: white; }}
|
|
280
|
+
.page {{
|
|
281
|
+
transform: none;
|
|
282
|
+
margin: 0 !important;
|
|
283
|
+
page-break-after: always;
|
|
284
|
+
}}
|
|
285
|
+
.page:last-child {{ page-break-after: auto; }}
|
|
286
|
+
.label-outline {{ stroke: none; }}
|
|
287
|
+
}}
|
|
288
|
+
</style>
|
|
289
|
+
</head>
|
|
290
|
+
<body>
|
|
291
|
+
{"".join(page_blocks)}
|
|
292
|
+
</body>
|
|
293
|
+
</html>'''
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def generate(excel_path, output_path, sheet_name=None,
|
|
297
|
+
column_mapping=None, start_row=2, display_scale=1, bw=False):
|
|
298
|
+
"""High-level entry point: read Excel, generate labels, write HTML.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
excel_path: Path to the .xlsx file.
|
|
302
|
+
output_path: Output HTML file path.
|
|
303
|
+
sheet_name: Sheet name to read; None means the first sheet.
|
|
304
|
+
column_mapping: Optional dict mapping internal keys to Excel column names.
|
|
305
|
+
start_row: 1-indexed row number where data begins.
|
|
306
|
+
display_scale: Scale factor for screen preview.
|
|
307
|
+
bw: When True, render labels in black-and-white mode (white circle,
|
|
308
|
+
thin black outline, black level number).
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
tuple: (number_of_books, number_of_pages, warnings_list)
|
|
312
|
+
"""
|
|
313
|
+
books, warnings = read_books(
|
|
314
|
+
excel_path, sheet_name, columns=column_mapping, start_row=start_row
|
|
315
|
+
)
|
|
316
|
+
if not books:
|
|
317
|
+
html = build_html([], display_scale, bw=bw)
|
|
318
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
319
|
+
f.write(html)
|
|
320
|
+
return 0, 0, warnings
|
|
321
|
+
|
|
322
|
+
html = build_html(books, display_scale, bw=bw)
|
|
323
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
324
|
+
f.write(html)
|
|
325
|
+
|
|
326
|
+
n_pages = (len(books) + LABELS_PER_PAGE - 1) // LABELS_PER_PAGE
|
|
327
|
+
return len(books), n_pages, warnings
|
|
Binary file
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ar-book-labels
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate printable Accelerated Reader book labels from an Excel spreadsheet.
|
|
5
|
+
Author: TonyBlur
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TonyBlur/ar-book-labels
|
|
8
|
+
Project-URL: Issues, https://github.com/TonyBlur/ar-book-labels/issues
|
|
9
|
+
Keywords: accelerated-reader,labels,education,printing
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Education
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: openpyxl>=3.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# ar-book-labels
|
|
21
|
+
|
|
22
|
+
English | **[简体中文](README.zh.md)**
|
|
23
|
+
|
|
24
|
+
Generate printable [Accelerated Reader](https://www.renaissance.com/accelerated-reader/) book labels from an Excel spreadsheet. Labels include book title, author, AR level (with standard color coding), points, and quiz number — formatted for sticker-style printing and sticking on books.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Standard AR color coding**: 12 color ranges from yellow (0.1–1.5) to brown (6.6+)
|
|
29
|
+
- **Print-ready HTML output**: 4 columns x 9 rows = 36 labels per page, A4 size, `@page` CSS for direct printing
|
|
30
|
+
- **Smart text truncation**: Titles wrap to 2 lines with ellipsis; authors on 1 line
|
|
31
|
+
- **Author-first layout**: Author name appears above title for easy shelf sorting
|
|
32
|
+
- **Screen preview**: SVG viewBox scaling for crisp browser preview
|
|
33
|
+
- **Template included**: Reference Excel template with sample data and column documentation
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install ar-book-labels
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or install from source:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/TonyBlur/ar-book-labels.git
|
|
45
|
+
cd ar-book-labels
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
1. **Get the template** (optional — if you don't have an Excel file yet):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ar-book-labels --template
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This copies `ar_template.xlsx` to your current directory. Fill it with your book data.
|
|
58
|
+
|
|
59
|
+
2. **Generate labels**:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ar-book-labels books.xlsx -o labels.html
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
3. **Open** `labels.html` in a browser to preview, then print (Ctrl/Cmd+P).
|
|
66
|
+
|
|
67
|
+
> **Print tip**: In the browser print dialog, choose **"Actual size"** (or **"No margins"** / **"None"** for margins) to prevent the browser from auto-adding page margins that shift label positions. The HTML already declares `@page { margin: 0 }`; adding browser margins on top causes misalignment.
|
|
68
|
+
|
|
69
|
+
## CLI Usage
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
ar-book-labels <excel> [options]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Arguments
|
|
76
|
+
|
|
77
|
+
| Argument | Description |
|
|
78
|
+
|----------|-------------|
|
|
79
|
+
| `excel` | Path to the Excel file (.xlsx) |
|
|
80
|
+
|
|
81
|
+
### Options
|
|
82
|
+
|
|
83
|
+
| Option | Default | Description |
|
|
84
|
+
|--------|---------|-------------|
|
|
85
|
+
| `-o, --output` | `AR_Book_Labels.html` | Output HTML file path |
|
|
86
|
+
| `-s, --sheet` | first sheet | Sheet name to read (defaults to first sheet) |
|
|
87
|
+
| `--col-title` | `AR Title` | Excel column name for book title |
|
|
88
|
+
| `--col-author` | `AR Author` | Excel column name for author |
|
|
89
|
+
| `--col-level` | `Book Level` | Excel column name for book level |
|
|
90
|
+
| `--col-points` | `AR Points` | Excel column name for AR points |
|
|
91
|
+
| `--col-quiz` | `Quiz Number` | Excel column name for quiz number |
|
|
92
|
+
| `--start-row` | `2` | 1-indexed row where data begins (1 = header row) |
|
|
93
|
+
| `--scale` | `1` | Display scale factor for screen preview |
|
|
94
|
+
| `--bw` | — | Black-and-white mode: white circle with thin black outline, black level number |
|
|
95
|
+
| `--template` | — | Copy the reference Excel template to cwd and exit |
|
|
96
|
+
| `-V, --version` | — | Show version and exit |
|
|
97
|
+
|
|
98
|
+
### Examples
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Basic usage
|
|
102
|
+
ar-book-labels my_books.xlsx
|
|
103
|
+
|
|
104
|
+
# Custom output path and sheet name
|
|
105
|
+
ar-book-labels my_books.xlsx -o output/labels.html -s "Book Data"
|
|
106
|
+
|
|
107
|
+
# Custom column names (if your Excel uses different headers)
|
|
108
|
+
ar-book-labels my_books.xlsx --col-title "Title" --col-author "Author Name" --col-level "Level"
|
|
109
|
+
|
|
110
|
+
# Custom start row (e.g. data starts on row 3)
|
|
111
|
+
ar-book-labels my_books.xlsx --start-row 3
|
|
112
|
+
|
|
113
|
+
# Copy the template for reference
|
|
114
|
+
ar-book-labels --template
|
|
115
|
+
|
|
116
|
+
# Black-and-white mode for economical printing
|
|
117
|
+
ar-book-labels my_books.xlsx --bw
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Excel Format
|
|
121
|
+
|
|
122
|
+
The spreadsheet must contain these columns (default names shown; use `--col-*` options to map custom names):
|
|
123
|
+
|
|
124
|
+
| Internal Key | Default Column | Type | Description |
|
|
125
|
+
|--------------|----------------|------|-------------|
|
|
126
|
+
| `title` | `AR Title` | text | Book title |
|
|
127
|
+
| `author` | `AR Author` | text | Author name |
|
|
128
|
+
| `level` | `Book Level` | number | AR ATOS level (e.g. 5.1) |
|
|
129
|
+
| `points` | `AR Points` | number | Points value |
|
|
130
|
+
| `quiz` | `Quiz Number` | number/text | Quiz ID |
|
|
131
|
+
|
|
132
|
+
Rows with missing required fields are skipped with a warning printed to stderr.
|
|
133
|
+
|
|
134
|
+
Use `ar-book-labels --template` to get a pre-formatted template with sample data.
|
|
135
|
+
|
|
136
|
+
## AR Level Color Chart
|
|
137
|
+
|
|
138
|
+
| Level Range | Color | Hex |
|
|
139
|
+
|-------------|-------|-----|
|
|
140
|
+
| 0.1 – 1.5 | Yellow | `#FFD700` |
|
|
141
|
+
| 1.6 – 2.0 | Green | `#2E8B57` |
|
|
142
|
+
| 2.1 – 2.5 | Dark Blue | `#00008B` |
|
|
143
|
+
| 2.6 – 3.0 | Red | `#DC143C` |
|
|
144
|
+
| 3.1 – 3.5 | Pink | `#FF69B4` |
|
|
145
|
+
| 3.6 – 4.0 | Purple | `#800080` |
|
|
146
|
+
| 4.1 – 4.5 | Orange | `#FF8C00` |
|
|
147
|
+
| 4.6 – 5.0 | Light Blue | `#00BFFF` |
|
|
148
|
+
| 5.1 – 5.5 | Neon Orange | `#FF6600` |
|
|
149
|
+
| 5.6 – 6.0 | Neon Green | `#39FF14` |
|
|
150
|
+
| 6.1 – 6.5 | Black | `#1C1C1C` |
|
|
151
|
+
| 6.6+ | Brown | `#8B4513` |
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
### Setup
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git clone https://github.com/TonyBlur/ar-book-labels.git
|
|
159
|
+
cd ar-book-labels
|
|
160
|
+
python -m venv .venv
|
|
161
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
162
|
+
pip install -e ".[dev]"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Project Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
ar-book-labels/
|
|
169
|
+
ar_book_labels/
|
|
170
|
+
__init__.py # Package metadata and public API
|
|
171
|
+
generator.py # Core label generation logic
|
|
172
|
+
cli.py # CLI entry point (argparse)
|
|
173
|
+
__main__.py # python -m ar_book_labels support
|
|
174
|
+
templates/
|
|
175
|
+
ar_template.xlsx # Reference Excel template
|
|
176
|
+
tests/
|
|
177
|
+
test_generator.py # Unit tests
|
|
178
|
+
pyproject.toml # Build configuration (setuptools)
|
|
179
|
+
LICENSE # MIT License
|
|
180
|
+
README.md # This file (English)
|
|
181
|
+
README.zh.md # 中文文档
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Running Tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python -m pytest tests/ -v
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Building & Publishing
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install build twine
|
|
194
|
+
python -m build
|
|
195
|
+
twine check dist/*
|
|
196
|
+
twine upload dist/*
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Automated Publishing (GitHub Actions)
|
|
200
|
+
|
|
201
|
+
This project uses a GitHub Actions workflow to automatically publish to PyPI when a new release is created:
|
|
202
|
+
|
|
203
|
+
1. Bump the version in `pyproject.toml`
|
|
204
|
+
2. Create a new release on GitHub (via the web UI or `gh release create`)
|
|
205
|
+
3. The `publish.yml` workflow will automatically build the package and publish it to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no API token needed)
|
|
206
|
+
|
|
207
|
+
**Prerequisites**: Configure a [Trusted Publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) on PyPI for the `ar-book-labels` project, linking it to the `TonyBlur/ar-book-labels` repository and the `pypi` environment.
|
|
208
|
+
|
|
209
|
+
### Code Style
|
|
210
|
+
|
|
211
|
+
- Python 3.8+
|
|
212
|
+
- No external dependencies beyond `openpyxl`
|
|
213
|
+
- Keep the generator logic self-contained and testable
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ar_book_labels/__init__.py
|
|
5
|
+
ar_book_labels/__main__.py
|
|
6
|
+
ar_book_labels/cli.py
|
|
7
|
+
ar_book_labels/generator.py
|
|
8
|
+
ar_book_labels.egg-info/PKG-INFO
|
|
9
|
+
ar_book_labels.egg-info/SOURCES.txt
|
|
10
|
+
ar_book_labels.egg-info/dependency_links.txt
|
|
11
|
+
ar_book_labels.egg-info/entry_points.txt
|
|
12
|
+
ar_book_labels.egg-info/requires.txt
|
|
13
|
+
ar_book_labels.egg-info/top_level.txt
|
|
14
|
+
ar_book_labels/templates/ar_template.xlsx
|
|
15
|
+
tests/test_generator.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openpyxl>=3.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ar_book_labels
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ar-book-labels"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Generate printable Accelerated Reader book labels from an Excel spreadsheet."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "TonyBlur" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["accelerated-reader", "labels", "education", "printing"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Education",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Education",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"openpyxl>=3.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
ar-book-labels = "ar_book_labels.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/TonyBlur/ar-book-labels"
|
|
31
|
+
Issues = "https://github.com/TonyBlur/ar-book-labels/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["ar_book_labels*"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
"ar_book_labels" = ["templates/*.xlsx"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Tests for ar-book-labels generator."""
|
|
2
|
+
|
|
3
|
+
from ar_book_labels.generator import (
|
|
4
|
+
get_level_color,
|
|
5
|
+
get_badge_text_color,
|
|
6
|
+
split_text_lines,
|
|
7
|
+
read_books,
|
|
8
|
+
build_html,
|
|
9
|
+
LABELS_PER_PAGE,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_level_colors():
|
|
14
|
+
assert get_level_color(1.0) == "#FFD700" # yellow
|
|
15
|
+
assert get_level_color(1.8) == "#2E8B57" # green
|
|
16
|
+
assert get_level_color(2.3) == "#00008B" # dark blue
|
|
17
|
+
assert get_level_color(2.8) == "#DC143C" # red
|
|
18
|
+
assert get_level_color(3.3) == "#FF69B4" # pink
|
|
19
|
+
assert get_level_color(3.8) == "#800080" # purple
|
|
20
|
+
assert get_level_color(4.3) == "#FF8C00" # orange
|
|
21
|
+
assert get_level_color(4.8) == "#00BFFF" # light blue
|
|
22
|
+
assert get_level_color(5.1) == "#FF6600" # neon orange
|
|
23
|
+
assert get_level_color(5.8) == "#39FF14" # neon green
|
|
24
|
+
assert get_level_color(6.3) == "#1C1C1C" # black
|
|
25
|
+
assert get_level_color(7.0) == "#8B4513" # brown
|
|
26
|
+
assert get_level_color(0.0) == "#999999" # out of range → grey
|
|
27
|
+
assert get_level_color("invalid") == "#999999"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_badge_text_color():
|
|
31
|
+
assert get_badge_text_color("#FFD700") == "#000000" # yellow → black text
|
|
32
|
+
assert get_badge_text_color("#39FF14") == "#000000" # neon green → black text
|
|
33
|
+
assert get_badge_text_color("#00008B") == "#FFFFFF" # dark blue → white text
|
|
34
|
+
assert get_badge_text_color("#1C1C1C") == "#FFFFFF" # black → white text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_split_text_lines_single():
|
|
38
|
+
assert split_text_lines("Short", 19) == ["Short"]
|
|
39
|
+
assert split_text_lines("", 19) == [""]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_split_text_lines_wrap():
|
|
43
|
+
result = split_text_lines("Diary of a Wimpy Kid: The Last Straw", 19)
|
|
44
|
+
assert len(result) == 2
|
|
45
|
+
assert result[0] == "Diary of a Wimpy"
|
|
46
|
+
assert result[1] == "Kid: The Last Straw"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_split_text_lines_truncate():
|
|
50
|
+
# This text needs 3 lines at 19 chars, so line 2 gets truncated
|
|
51
|
+
result = split_text_lines("The Journey to the Center of the Earth is Great", 19)
|
|
52
|
+
assert len(result) == 2
|
|
53
|
+
assert result[0] == "The Journey to the"
|
|
54
|
+
assert result[1].endswith("\u2026") # ellipsis on truncated line 2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_split_text_lines_no_space():
|
|
58
|
+
# Very long single word exceeding 2 lines
|
|
59
|
+
result = split_text_lines("A" * 50, 19)
|
|
60
|
+
assert len(result) == 2
|
|
61
|
+
assert result[1].endswith("\u2026")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_build_html_empty():
|
|
65
|
+
html = build_html([])
|
|
66
|
+
assert "<!DOCTYPE html>" in html
|
|
67
|
+
assert "AR Book Labels" in html
|
|
68
|
+
assert "<svg" not in html # no pages
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_build_html_single_book():
|
|
72
|
+
book = {
|
|
73
|
+
"title": "Test Book",
|
|
74
|
+
"author": "Test Author",
|
|
75
|
+
"level": 3.5,
|
|
76
|
+
"points": 5,
|
|
77
|
+
"quiz": 12345,
|
|
78
|
+
}
|
|
79
|
+
html = build_html([book])
|
|
80
|
+
assert "Test Book" in html
|
|
81
|
+
assert "Test Author" in html
|
|
82
|
+
assert "12345" in html
|
|
83
|
+
assert html.count('<div class="page"') == 1
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_build_html_multi_page():
|
|
87
|
+
books = [
|
|
88
|
+
{
|
|
89
|
+
"title": f"Book {i}",
|
|
90
|
+
"author": f"Author {i}",
|
|
91
|
+
"level": 2.0,
|
|
92
|
+
"points": 1,
|
|
93
|
+
"quiz": i,
|
|
94
|
+
}
|
|
95
|
+
for i in range(LABELS_PER_PAGE + 1)
|
|
96
|
+
]
|
|
97
|
+
html = build_html(books)
|
|
98
|
+
assert html.count('<div class="page"') == 2
|