docs: one manual, the pages a device has and a printable copy of all of them

Documentation becomes part of the release rather than a job afterwards.
docs/manual is now the only place teacher instructions are written, and
tools/build-manual produces both versions from it:

  classroom-manual.html   installed on every machine, showing only the
                          features THAT device has
  classroom-manual.pdf    the universal printed manual, every feature,
                          with a conditional sentence above each
                          optional page so it stays true in a room that
                          does not have them all

They cannot drift apart because neither is written by hand, and
build-release.sh builds and checks them on every release: a page that
declares a feature with no printed-if fails the build, because that is
exactly how the printed manual comes to state an optional feature as
though every room had it. So does a page naming a feature device.conf
does not have, and an optional feature with no page at all.

WHAT REACHES THE CLASSROOM ACCOUNT. Only a list of setting NAMES, and
only ones the package already defines as optional applications.
classroom-desktop-layout — already the one place that decides what a
device offers — writes them to /run/ekklesia-classroom/features.json,
and classroom-manual filters that against FEATURES before using it. No
values from device.conf, no broker password, no addresses. With no
manifest at all every page is shown, which is the printed manual and is
never wrong, only less specific.

Help is a Quick Launch button beside Parent Call, deliberately not in
the row with the applications: putting it there would move buttons
teachers reach for without looking. Every room gets it, because a
teacher who needs the manual is by definition one who does not know
where to look.

The PDF is written directly rather than converted. A manual is
headings, paragraphs, bullets, steps and one table; writing that with
the standard fonts is a couple of hundred lines and no dependency, so a
release can be built on any machine with Python. Pictures are embedded
in the digital manual as data URIs so it stays one file that works
wherever it is copied and with the network unplugged; the printed
manual names them instead, which is the one thing this builder does not
do and is noted where somebody would look for it.

Two defects found building it, both of which hung or corrupted the
output rather than failing: a paragraph opening with a bold word
matched the bullet guard but not the bullet rule, so nothing consumed
it and the build spun; and a list item wrapped across two source lines
ended the list, so a three-step procedure printed as three steps all
numbered 1.

Also adds the 0.2.6 physical checklist to docs/testing.md and the
release validator's checks for the Wi-Fi regression, the conversion
pipeline on a real Debian, the fonts, the manual and the dock order.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-22 20:21:39 -05:00
co-authored by Claude Opus 5
parent 55f4c183c7
commit f2942a2946
21 changed files with 2533 additions and 3 deletions
@@ -0,0 +1,19 @@
[Desktop Entry]
Type=Application
Name=Help
GenericName=Classroom manual
Comment=How to use this classroom computer
# Breeze's own. A question mark in a circle is the one mark that needs no
# explaining, and this is the one place in the classroom where borrowing
# from Breeze is better than drawing something of our own.
Icon=help-contents
Exec=/usr/local/bin/classroom-manual
Terminal=false
Categories=Documentation;
StartupNotify=true
# Chrome in application mode reports the app id Chrome derives from the
# --app URL, not this entry, so the panel cannot match the manual window
# to this launcher. That is deliberate and harmless: Help is a Quick
# Launch button, which is a launcher and never a task, so there is no
# button here waiting to be matched.
NoDisplay=false
+24
View File
@@ -0,0 +1,24 @@
---
title: Your classroom computer
order: 0
---
This computer is set up for one thing: running your room. It switches
itself on into the classroom, and there is nothing on it to break.
You do not need a password. You do not need to sign in. You do not need
to shut it down at the end — leave it on.
## The bar along the bottom
Everything you need is on the bar at the bottom of the screen.
The buttons in the middle are the things you can open. Which ones are
there depends on what your room is set up for, so your bar may have
fewer buttons than another room's.
The right-hand end shows the network, the volume, any messages and the
clock.
To open something, click its button once. To come back to it later,
click the same button again.
+28
View File
@@ -0,0 +1,28 @@
---
title: The Browser
order: 10
---
The **Browser** button opens the web: YouTube, a song, a video clip, a
website a lesson sends you to.
## Using it
1. Click **Browser** on the bar.
2. Type where you want to go in the bar at the top, or click **Home** to
go back to YouTube.
The room stays signed in to YouTube, so you do not have to.
## Playing a video on the television
Click the full-screen button in the corner of the video, the same as you
would at home. Press **Esc** to come back out of full screen.
## When you have finished
Leave it open, or close the window. The next person can open it again
from the bar.
Anything you sign in to yourself — a personal account, an email — sign
out of before you leave. The computer is shared.
+27
View File
@@ -0,0 +1,27 @@
---
title: Sound and volume
order: 20
---
Sound goes to the television when one is connected, and to the
computer's own speakers when one is not. You do not have to choose.
## Changing the volume
Use the volume keys on the keyboard, or click the speaker at the
right-hand end of the bar and drag the slider.
A large volume bar appears on the screen while you are changing it, so
you can see it from across the room.
## If there is no sound
1. Check the volume on the **television** as well as on the computer.
Both have to be up.
2. Check the television is showing this computer, and not something else
plugged into it.
3. If the television was switched on after the computer, the sound may
still be going to the computer's own speakers. Turn the volume down
and back up once; that is enough to move it across.
If none of that works, tell the office. Do not unplug anything.
+118
View File
@@ -0,0 +1,118 @@
---
title: Presentations
order: 40
feature: PRESENTATIONS_ENABLED
printed-if: If your computer has a Presentations button on the bar
---
**Presentations** holds the slides for your sessions. You download a
presentation in the browser once, and from then on it is on this screen,
ready to show on the television.
## Getting a presentation onto the computer
1. Click **Browser** and go to wherever the presentation is — your
email, a shared folder, wherever it was sent to you.
2. Download it the way you normally would.
3. Wait until the download has finished.
That is all. It appears in **Presentations** by itself a few seconds
later. You do not have to save it anywhere special, move it, or open it.
PowerPoint files and PDF files both work.
Your download stays in the browser exactly as it was. Nothing here
changes it, moves it or deletes it.
## Opening the library
Click **Presentations** on the bar.
![The Presentations library, with a card for each session](images/library.png)
Each session is a card with its name and a picture of one of its slides.
Find tonight's by looking at the picture.
If a session has only just been downloaded, its card says it is being
got ready. It will be ready in a moment.
## Quick Look — checking the slides first
**Quick Look** lets you read through a session before you show it to
anybody.
![Quick Look, showing one slide with Next and Previous underneath](images/quick-look.png)
* **Next** and **Previous** move through the slides.
* **Back to Presentations** returns to the library.
* **Present** starts the slideshow straight away.
Looking through a session in Quick Look does not count as having
presented it.
## Changing the picture on the card
The first slide of a session is often the same opening slide as every
other session, which makes the cards hard to tell apart. Use the slide
that names the session instead — usually the second one.
1. Click **Quick Look**.
2. Press **Next** until you reach the slide you want.
3. Click **Use this slide as the picture**.
The card changes straight away, and stays that way — after you close the
library, and after the computer is switched off and on again.
Choosing the picture does not change where the slideshow starts. It
always starts at the first slide.
## Showing a presentation
Click **Present**, on the card or inside Quick Look.
The slides fill the whole screen. Nothing else is on it.
| To do this | Press |
| --- | --- |
| Next slide | **Space**, the **right arrow**, the **down arrow** or **Page Down** |
| Previous slide | The **left arrow**, the **up arrow** or **Page Up** |
| First slide | **Home** |
| Last slide | **End** |
| Finish | **Esc** |
A presentation remote works too — the forward and back buttons on the
clicker are the same as Page Down and Page Up.
## Finishing
Press **Esc**.
You come straight back to the library. Going past the last slide does
the same thing.
## Removing a presentation
Click **Remove** on its card, and confirm.
That takes it off this screen. Your download in the browser is not
touched, so you can always get it back by downloading it again.
## Presentations you no longer need
The computer tidies up after itself, so the library does not slowly fill
up with old sessions.
A session you have shown is kept for a month after the last time you
showed it. Showing it again starts that month over.
A session you downloaded but never showed is kept for six months.
You do not have to do anything about this, and nothing disappears while
you are using it.
## If a presentation will not open
Its card says so, and offers only **Remove**. Usually the file did not
download properly.
Download it again. Your original download is still there either way.
+35
View File
@@ -0,0 +1,35 @@
---
title: Wonder Ink
order: 50
feature: WONDERINK_ENABLED
printed-if: If Wonder Ink is available in your classroom
---
**Wonder Ink** opens the lesson. It is its own application, not the
browser, and it opens straight into the lesson site.
## Using it
1. Click **Wonder Ink** on the bar.
2. Sign in with your Wonder Ink account.
A lesson often opens several tabs — the lesson, its video, an activity.
They appear in a row across the top. Click a tab to move between them,
and the **×** on a tab to close it.
There is no address bar, on purpose. Wonder Ink can take you anywhere
the lesson needs to go.
## Worksheets and colouring pages
Click the worksheet link in the lesson. It opens in a new tab, ready to
read or print.
Worksheets you download are kept separately from everything else on the
computer, so they never get mixed up with anything.
## When you have finished
**Close Wonder Ink at the end of your session.** Closing it signs you
out, so the next teacher in this room starts with their own account and
not yours.
+28
View File
@@ -0,0 +1,28 @@
---
title: Parent Call
order: 60
feature: PARENT_CALL_ENABLED
printed-if: If your computer has a Parent Call button in the bottom-left corner
---
**Parent Call** asks the sanctuary to fetch a child's grown-up, without
you leaving the room.
## Using it
1. Click **Parent Call**, in the bottom-left corner of the screen.
2. Type the child's name.
3. Click **Send**.
The card tells you what is happening. **Sent to sanctuary** means
somebody there has it.
## What it does and does not promise
**Sent to sanctuary** means the message arrived. It does not mean it has
been read out yet — somebody there decides when.
If it cannot reach the sanctuary it says so plainly. It will never tell
you a call went through when it did not.
If that happens, send somebody.
+30
View File
@@ -0,0 +1,30 @@
---
title: Playing a DVD
order: 70
feature: DVD_ENABLED
printed-if: If your computer has a DVD button and a disc drive
---
## Playing a disc
1. Put the disc in the drive.
2. Wait for **DVD Ready** to appear.
3. Click **DVD** on the bar.
The disc fills the screen and plays on the television. Discs never start
on their own — it always takes a button press.
## While it is playing
Move the mouse to the very bottom of the screen and a row of controls
slides up: previous chapter, play and pause, next chapter, the disc
menu, eject, and close.
**Space** also plays and pauses, and the arrow keys move around the disc
menu.
## Finishing
Click **✕** on that row to stop, or **⏏** to stop and eject the disc.
Take the disc with you.
+23
View File
@@ -0,0 +1,23 @@
---
title: Printing
order: 80
---
Printing goes to the church printer. There is nothing to choose and
nothing to set up.
## Printing something
1. Press **Ctrl+P**, or use the **Print** button on the page you are
looking at.
2. Check the number of copies.
3. Click **Print**.
## If nothing comes out
The printer may be off, out of paper, or somebody else's job may be
ahead of yours. Your job waits in the queue and prints when the printer
is ready again — you do not need to send it twice.
Sending it again two or three times is how ten copies of the same
worksheet appear later.
+35
View File
@@ -0,0 +1,35 @@
---
title: If something goes wrong
order: 90
---
Almost nothing here needs a technical person, and nothing you can do
from the keyboard will break the computer.
## The screen is black
Move the mouse or press a key. If it stays black, check the television
is on and showing this computer.
## A window will not close
Click its button on the bar to bring it back, and close it normally.
## The bar has gone
Wait a few seconds; it puts itself back.
If it does not, switch the computer off at the power button, wait ten
seconds, and switch it on again. It comes back to the same screen it
always does.
## Something asks for a password
Nothing a teacher needs to do asks for a password. If a password box
appears, you have reached something you did not mean to — press **Esc**
or click **Cancel**, and carry on.
## It is still wrong
Tell the office, and say **which room** and **what you were doing when
it happened**. That second part is the one that saves everybody time.
+85
View File
@@ -0,0 +1,85 @@
# The teacher's manual
**This directory is the only place teacher instructions are written.**
Everything a teacher reads comes out of the `.md` files beside this one:
* the **digital manual** on each classroom computer, which shows only the
features that computer actually has; and
* the **printable PDF**, which covers every feature with wording that
stays true in a room that does not have all of them.
Both are built by `tools/build-manual` from these files. There is no
second copy to keep in step, and there is no per-room manual: a room
differs from another room by its `device.conf`, and the manual reads the
same setting the panel does.
## Writing a page
Each page is Markdown with a small header:
```markdown
---
title: Presentations
order: 40
feature: PRESENTATIONS_ENABLED
printed-if: If your computer has a Presentations button
---
## Downloading a presentation
...
```
| Field | Means |
| --- | --- |
| `title` | The heading, and the entry in the contents |
| `order` | Where it comes. Common pages are 00–39, optional ones 40+ |
| `feature` | The `device.conf` setting that decides whether a computer shows this page. Leave it out for a page every room gets |
| `printed-if` | The conditional sentence the printed manual puts above the page. Required whenever `feature` is set |
Only `##` and `###` headings, ordinary paragraphs, `*` bullets, `1.`
numbered steps, simple `|` tables, `![caption](images/x.png)` pictures
and **bold** are supported. That is deliberate: the same text has to come
out well as a web page and as print, and a manual for teachers does not
need anything else.
## Writing style
The existing pages set it, and it matters more than the format:
* Say what the teacher does, not what the computer does.
* One instruction per step, in the order they happen.
* Name things the way the screen names them — **Present**, **Quick
Look**, the **bar** along the bottom.
* No paths, no package names, no settings files, no version numbers.
* If something can go wrong, say what to do about it in the same breath.
## Building
```
tools/build-manual # both, into docs/manual/build/
tools/build-manual --check # verify only; what the release runs
```
`tools/build-release.sh` builds and checks the manual on every release, so
the digital and printed versions cannot drift apart. The check fails the
build if a page declares a `feature` with no `printed-if`, if it names a
feature `device.conf` does not have, or if the built files are missing.
## Screenshots
Pictures live in `docs/manual/images/`. They are captured from the real
interface with `tools/lab/lab shot`, never drawn or mocked up. A manual
showing a screen that does not exist is worse than one with no pictures.
They are **embedded** into the digital manual as data URIs, so it stays a
single file that works wherever it is copied and with the network
unplugged. A picture named by a page that is not in `images/` fails the
build, as does one with no caption.
**The printed manual prints the caption, not the picture** — as
`[ Picture: ... — see the computer's own Help ]`. Embedding a PNG in a
PDF means decoding it and writing an `/XObject`, and this builder exists
so that a release needs nothing but Python. If the printed manual ever
needs real pictures, that is the thing to add, and it is a contained
change: `render_pdf` is the only function that would move.
File diff suppressed because one or more lines are too long
+362
View File
@@ -0,0 +1,362 @@
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R] /Count 11 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 14 0 R >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 15 0 R >>
endobj
5 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 16 0 R >>
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 17 0 R >>
endobj
7 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 18 0 R >>
endobj
8 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 19 0 R >>
endobj
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 20 0 R >>
endobj
10 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 21 0 R >>
endobj
11 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 22 0 R >>
endobj
12 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 23 0 R >>
endobj
13 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 25 0 R /F2 26 0 R >> >> /Contents 24 0 R >>
endobj
14 0 obj
<< /Length 765 >>
stream
BT /F2 26 Tf 1 0 0 1 62.0 720.0 Tm (Classroom manual) Tj ET
BT /F1 13 Tf 1 0 0 1 62.0 677.1 Tm (River of Glory) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 656.6 Tm (Revision 22 September 2026) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 623.7 Tm (This manual covers every classroom computer in the church. Not every room has every button, so) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 608.8 Tm (some pages begin by saying when they apply. If your computer does not have what a page describes,) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 593.9 Tm (that page is not for your room.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 569.0 Tm (Keep this copy in the room. If the date above is older than the one on the computer's own Help, ask) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 554.1 Tm (the office for a new one.) Tj ET
endstream
endobj
15 0 obj
<< /Length 1250 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (Your classroom computer) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F1 10.5 Tf 1 0 0 1 62.0 733.9 Tm (This computer is set up for one thing: running your room. It switches itself on into the) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 719.0 Tm (classroom, and there is nothing on it to break.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 699.0 Tm (You do not need a password. You do not need to sign in. You do not need to shut it down at the end -) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 684.1 Tm (leave it on.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 656.2 Tm (The bar along the bottom) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 635.5 Tm (Everything you need is on the bar at the bottom of the screen.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 615.6 Tm (The buttons in the middle are the things you can open. Which ones are there depends on what your) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 600.7 Tm (room is set up for, so your bar may have fewer buttons than another room's.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 580.7 Tm (The right-hand end shows the network, the volume, any messages and the clock.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 560.8 Tm (To open something, click its button once. To come back to it later, click the same button again.) Tj ET
endstream
endobj
16 0 obj
<< /Length 1408 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (The Browser) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F1 10.5 Tf 1 0 0 1 62.0 733.9 Tm (The Browser button opens the web: YouTube, a song, a video clip, a website a lesson sends you to.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 706.0 Tm (Using it) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 685.2 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 685.2 Tm (Click Browser on the bar.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 670.3 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 670.3 Tm (Type where you want to go in the bar at the top, or click Home to go back to YouTube.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 650.4 Tm (The room stays signed in to YouTube, so you do not have to.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 622.5 Tm (Playing a video on the television) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 601.7 Tm (Click the full-screen button in the corner of the video, the same as you would at home. Press Esc) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 586.8 Tm (to come back out of full screen.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 558.9 Tm (When you have finished) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 538.2 Tm (Leave it open, or close the window. The next person can open it again from the bar.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 518.2 Tm (Anything you sign in to yourself - a personal account, an email - sign out of before you leave.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 503.3 Tm (The computer is shared.) Tj ET
endstream
endobj
17 0 obj
<< /Length 1694 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (Sound and volume) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F1 10.5 Tf 1 0 0 1 62.0 733.9 Tm (Sound goes to the television when one is connected, and to the computer's own speakers when one) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 719.0 Tm (is not. You do not have to choose.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 691.0 Tm (Changing the volume) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 670.3 Tm (Use the volume keys on the keyboard, or click the speaker at the right-hand end of the bar and) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 655.4 Tm (drag the slider.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 635.5 Tm (A large volume bar appears on the screen while you are changing it, so you can see it from across) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 620.6 Tm (the room.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 592.7 Tm (If there is no sound) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 571.9 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 571.9 Tm (Check the volume on the television as well as on the computer. Both have to be up.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 557.0 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 557.0 Tm (Check the television is showing this computer, and not something else plugged into it.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 542.1 Tm (3.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 542.1 Tm (If the television was switched on after the computer, the sound may still be going to the) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 527.2 Tm (computer's own speakers. Turn the volume down and back up once; that is enough to move it) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 512.3 Tm (across.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 492.4 Tm (If none of that works, tell the office. Do not unplug anything.) Tj ET
endstream
endobj
18 0 obj
<< /Length 4084 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (Presentations) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F2 10.5 Tf 1 0 0 1 62.0 733.9 Tm (If your computer has a Presentations button on the bar.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 713.0 Tm (Presentations holds the slides for your sessions. You download a presentation in the browser) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 698.0 Tm (once, and from then on it is on this screen, ready to show on the television.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 670.1 Tm (Getting a presentation onto the computer) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 649.4 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 649.4 Tm (Click Browser and go to wherever the presentation is - your email, a shared folder,) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 634.5 Tm (wherever it was sent to you.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 619.6 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 619.6 Tm (Download it the way you normally would.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 604.7 Tm (3.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 604.7 Tm (Wait until the download has finished.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 584.7 Tm (That is all. It appears in Presentations by itself a few seconds later. You do not have to save) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 569.8 Tm (it anywhere special, move it, or open it.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 549.9 Tm (PowerPoint files and PDF files both work.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 530.0 Tm (Your download stays in the browser exactly as it was. Nothing here changes it, moves it or) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 515.1 Tm (deletes it.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 487.2 Tm (Opening the library) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 466.4 Tm (Click Presentations on the bar.) Tj ET
BT /F1 9.5 Tf 1 0 0 1 62.0 446.5 Tm ([ Picture: The Presentations library, with a card for each session - see the computer's own Help ]) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 428.0 Tm (Each session is a card with its name and a picture of one of its slides. Find tonight's by looking) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 413.1 Tm (at the picture.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 393.2 Tm (If a session has only just been downloaded, its card says it is being got ready. It will be ready) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 378.3 Tm (in a moment.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 350.4 Tm (Quick Look - checking the slides first) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 329.7 Tm (Quick Look lets you read through a session before you show it to anybody.) Tj ET
BT /F1 9.5 Tf 1 0 0 1 62.0 309.7 Tm ([ Picture: Quick Look, showing one slide with Next and Previous underneath - see the computer's own Help ]) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 291.3 Tm (-) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 291.3 Tm (Next and Previous move through the slides.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 276.3 Tm (-) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 276.3 Tm (Back to Presentations returns to the library.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 261.4 Tm (-) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 261.4 Tm (Present starts the slideshow straight away.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 241.5 Tm (Looking through a session in Quick Look does not count as having presented it.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 213.6 Tm (Changing the picture on the card) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 192.9 Tm (The first slide of a session is often the same opening slide as every other session, which makes) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 178.0 Tm (the cards hard to tell apart. Use the slide that names the session instead - usually the second) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 163.0 Tm (one.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 143.1 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 143.1 Tm (Click Quick Look.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 128.2 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 128.2 Tm (Press Next until you reach the slide you want.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 113.3 Tm (3.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 113.3 Tm (Click Use this slide as the picture.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 93.4 Tm (The card changes straight away, and stays that way - after you close the library, and after the) Tj ET
endstream
endobj
19 0 obj
<< /Length 3089 >>
stream
BT /F1 10.5 Tf 1 0 0 1 62.0 780.0 Tm (computer is switched off and on again.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 760.1 Tm (Choosing the picture does not change where the slideshow starts. It always starts at the first) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 745.2 Tm (slide.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 717.3 Tm (Showing a presentation) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 696.5 Tm (Click Present, on the card or inside Quick Look.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 676.6 Tm (The slides fill the whole screen. Nothing else is on it.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 656.7 Tm ( ) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 656.7 Tm (TO DO THIS PRESS) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 641.8 Tm ( ) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 641.8 Tm (Next slide Space, the right arrow, the down arrow or Page Down) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 626.9 Tm ( ) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 626.9 Tm (Previous slide The left arrow, the up arrow or Page Up) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 612.0 Tm ( ) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 612.0 Tm (First slide Home) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 597.1 Tm ( ) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 597.1 Tm (Last slide End) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 582.2 Tm ( ) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 582.2 Tm (Finish Esc) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 562.2 Tm (A presentation remote works too - the forward and back buttons on the clicker are the same as) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 547.3 Tm (Page Down and Page Up.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 519.4 Tm (Finishing) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 498.7 Tm (Press Esc.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 478.8 Tm (You come straight back to the library. Going past the last slide does the same thing.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 450.9 Tm (Removing a presentation) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 430.1 Tm (Click Remove on its card, and confirm.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 410.2 Tm (That takes it off this screen. Your download in the browser is not touched, so you can always get) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 395.3 Tm (it back by downloading it again.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 367.4 Tm (Presentations you no longer need) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 346.6 Tm (The computer tidies up after itself, so the library does not slowly fill up with old sessions.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 326.7 Tm (A session you have shown is kept for a month after the last time you showed it. Showing it again) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 311.8 Tm (starts that month over.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 291.9 Tm (A session you downloaded but never showed is kept for six months.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 272.0 Tm (You do not have to do anything about this, and nothing disappears while you are using it.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 244.1 Tm (If a presentation will not open) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 223.3 Tm (Its card says so, and offers only Remove. Usually the file did not download properly.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 203.4 Tm (Download it again. Your original download is still there either way.) Tj ET
endstream
endobj
20 0 obj
<< /Length 1838 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (Wonder Ink) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F2 10.5 Tf 1 0 0 1 62.0 733.9 Tm (If Wonder Ink is available in your classroom.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 713.0 Tm (Wonder Ink opens the lesson. It is its own application, not the browser, and it opens straight) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 698.0 Tm (into the lesson site.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 670.1 Tm (Using it) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 649.4 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 649.4 Tm (Click Wonder Ink on the bar.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 634.5 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 634.5 Tm (Sign in with your Wonder Ink account.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 614.6 Tm (A lesson often opens several tabs - the lesson, its video, an activity. They appear in a row) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 599.7 Tm (across the top. Click a tab to move between them, and the x on a tab to close it.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 579.7 Tm (There is no address bar, on purpose. Wonder Ink can take you anywhere the lesson needs to go.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 551.8 Tm (Worksheets and colouring pages) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 531.1 Tm (Click the worksheet link in the lesson. It opens in a new tab, ready to read or print.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 511.2 Tm (Worksheets you download are kept separately from everything else on the computer, so they never) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 496.3 Tm (get mixed up with anything.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 468.4 Tm (When you have finished) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 447.6 Tm (Close Wonder Ink at the end of your session. Closing it signs you out, so the next teacher in) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 432.7 Tm (this room starts with their own account and not yours.) Tj ET
endstream
endobj
21 0 obj
<< /Length 1475 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (Parent Call) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F2 10.5 Tf 1 0 0 1 62.0 733.9 Tm (If your computer has a Parent Call button in the bottom-left corner.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 713.0 Tm (Parent Call asks the sanctuary to fetch a child's grown-up, without you leaving the room.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 685.0 Tm (Using it) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 664.3 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 664.3 Tm (Click Parent Call, in the bottom-left corner of the screen.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 649.4 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 649.4 Tm (Type the child's name.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 634.5 Tm (3.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 634.5 Tm (Click Send.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 614.6 Tm (The card tells you what is happening. Sent to sanctuary means somebody there has it.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 586.7 Tm (What it does and does not promise) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 565.9 Tm (Sent to sanctuary means the message arrived. It does not mean it has been read out yet - somebody) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 551.0 Tm (there decides when.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 531.1 Tm (If it cannot reach the sanctuary it says so plainly. It will never tell you a call went through) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 516.2 Tm (when it did not.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 496.3 Tm (If that happens, send somebody.) Tj ET
endstream
endobj
22 0 obj
<< /Length 1500 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (Playing a DVD) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F2 10.5 Tf 1 0 0 1 62.0 733.9 Tm (If your computer has a DVD button and a disc drive.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 705.0 Tm (Playing a disc) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 684.2 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 684.2 Tm (Put the disc in the drive.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 669.3 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 669.3 Tm (Wait for DVD Ready to appear.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 654.4 Tm (3.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 654.4 Tm (Click DVD on the bar.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 634.5 Tm (The disc fills the screen and plays on the television. Discs never start on their own - it always) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 619.6 Tm (takes a button press.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 591.7 Tm (While it is playing) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 570.9 Tm (Move the mouse to the very bottom of the screen and a row of controls slides up: previous chapter,) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 556.0 Tm (play and pause, next chapter, the disc menu, eject, and close.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 536.1 Tm (Space also plays and pauses, and the arrow keys move around the disc menu.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 508.2 Tm (Finishing) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 487.4 Tm (Click X on that row to stop, or eject to stop and eject the disc.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 467.5 Tm (Take the disc with you.) Tj ET
endstream
endobj
23 0 obj
<< /Length 1166 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (Printing) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F1 10.5 Tf 1 0 0 1 62.0 733.9 Tm (Printing goes to the church printer. There is nothing to choose and nothing to set up.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 706.0 Tm (Printing something) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 685.2 Tm (1.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 685.2 Tm (Press Ctrl+P, or use the Print button on the page you are looking at.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 670.3 Tm (2.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 670.3 Tm (Check the number of copies.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 78.0 655.4 Tm (3.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 94.0 655.4 Tm (Click Print.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 627.5 Tm (If nothing comes out) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 606.7 Tm (The printer may be off, out of paper, or somebody else's job may be ahead of yours. Your job waits) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 591.8 Tm (in the queue and prints when the printer is ready again - you do not need to send it twice.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 571.9 Tm (Sending it again two or three times is how ten copies of the same worksheet appear later.) Tj ET
endstream
endobj
24 0 obj
<< /Length 1766 >>
stream
BT /F2 17 Tf 1 0 0 1 62.0 774.0 Tm (If something goes wrong) Tj ET
0.75 w 0.6 0.6 0.6 RG 62 751.9 m 533 751.9 l S
BT /F1 10.5 Tf 1 0 0 1 62.0 733.9 Tm (Almost nothing here needs a technical person, and nothing you can do from the keyboard will) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 719.0 Tm (break the computer.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 691.0 Tm (The screen is black) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 670.3 Tm (Move the mouse or press a key. If it stays black, check the television is on and showing this) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 655.4 Tm (computer.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 627.5 Tm (A window will not close) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 606.7 Tm (Click its button on the bar to bring it back, and close it normally.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 578.8 Tm (The bar has gone) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 558.1 Tm (Wait a few seconds; it puts itself back.) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 538.2 Tm (If it does not, switch the computer off at the power button, wait ten seconds, and switch it on) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 523.2 Tm (again. It comes back to the same screen it always does.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 495.3 Tm (Something asks for a password) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 474.6 Tm (Nothing a teacher needs to do asks for a password. If a password box appears, you have reached) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 459.7 Tm (something you did not mean to - press Esc or click Cancel, and carry on.) Tj ET
BT /F2 12.5 Tf 1 0 0 1 62.0 431.8 Tm (It is still wrong) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 411.0 Tm (Tell the office, and say which room and what you were doing when it happened. That second part is) Tj ET
BT /F1 10.5 Tf 1 0 0 1 62.0 396.1 Tm (the one that saves everybody time.) Tj ET
endstream
endobj
25 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
26 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
endobj
xref
0 27
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000180 00000 n
0000000319 00000 n
0000000458 00000 n
0000000597 00000 n
0000000736 00000 n
0000000875 00000 n
0000001014 00000 n
0000001153 00000 n
0000001293 00000 n
0000001433 00000 n
0000001573 00000 n
0000001713 00000 n
0000002530 00000 n
0000003833 00000 n
0000005294 00000 n
0000007041 00000 n
0000011178 00000 n
0000014320 00000 n
0000016211 00000 n
0000017739 00000 n
0000019292 00000 n
0000020511 00000 n
0000022330 00000 n
0000022428 00000 n
trailer
<< /Size 27 /Root 1 0 R >>
startxref
22531
%%EOF
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

+100
View File
@@ -473,3 +473,103 @@ With `LIVE_ENABLED=true`:
single after reinstall single after reinstall
- [ ] `tools/validate-trixie-release.sh` passes against the 0.1.37 package and - [ ] `tools/validate-trixie-release.sh` passes against the 0.1.37 package and
the 0.1.36 package the 0.1.36 package
## Presentations — 0.2.6, the prayer-room laptop
Everything here needs the real television, the real keyboard and the
pastor's own session files. The automated tests and the Plasma lab cover
the logic and the windows; none of them can tell you that Session 7's
text is the right size from the back of the room.
Enable it first, and reboot:
```
sudo sed -i 's/^PRESENTATIONS_ENABLED=.*/PRESENTATIONS_ENABLED=true/' \
/etc/ekklesia-classroom/device.conf
sudo reboot
```
### Download
- [ ] The bar has a **Presentations** button, after the Browser
- [ ] Wonder Ink and Parent Call are still absent from this laptop
- [ ] Download tonight's session in ordinary Chrome
- [ ] It does **not** appear in the library while it is still downloading
- [ ] It appears by itself within a few seconds of the download finishing
- [ ] The title is readable — not a hash, not a long filename
- [ ] The original download is still in Chrome's downloads, untouched
- [ ] Download the **PDF and the PPTX** of the same session: two cards,
the second titled `(2)`, neither overwriting the other
### Quick Look
- [ ] **Quick Look** opens and shows slide 1
- [ ] **Next** and **Previous** move through every slide
- [ ] The text and layout are right — check a slide with a Bible verse
- [ ] Go to the session-number slide, usually slide 2
- [ ] **Use this slide as the picture** changes the card
- [ ] **Back to Presentations** returns without presenting
- [ ] Close the library, open it again: the chosen picture is still there
- [ ] Reboot, open it again: still there
- [ ] `classroom-presentation-import --status` shows `presented never`
That last line is the one that matters. Quick Look, browsing every slide
and changing the picture must not have counted as a presentation.
### Present
- [ ] **Present** from the card fills the **external television**
- [ ] It starts at slide **1**, not at the slide chosen for the picture
- [ ] **Present** from inside Quick Look does the same
- [ ] Space, the arrow keys and Page Up/Page Down all move slides
- [ ] The presentation remote's forward and back buttons work
- [ ] Fonts and layout are right on the television, from the back
- [ ] **Esc** returns to the library — **not** the Impress editor, not a
terminal, not a blank screen
- [ ] Going past the last slide does the same
- [ ] `--status` now shows a presentation time and a count
- [ ] Present it again: the count rises and the expiry moves out 30 days
- [ ] Unplug the network and present again: it still works
### The awkward ones
- [ ] A PDF and a PPTX of the same session look the same on screen
- [ ] A deliberately broken file gives a short message and only
**Remove** — no editor, no terminal
- [ ] A long session (40+ slides) opens without the library freezing
- [ ] Download a Wonder Ink worksheet: it does **not** appear in the
library
### Remove
Use a **test copy**. Not tomorrow's session.
- [ ] **Remove** asks first
- [ ] The card goes, and its slides and pictures go with it
- [ ] The original download in Chrome is untouched
- [ ] It does not come back on the next scan or after a reboot
### The manual
- [ ] **Help** is on the bar, bottom left
- [ ] It opens with no address bar and no tabs
- [ ] It shows Browser, Presentations, sound and the common pages
- [ ] It does **not** mention Wonder Ink, Parent Call or the DVD
- [ ] It works with the network unplugged
- [ ] On a children's classroom machine it shows the opposite set
- [ ] Print `/usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf`
and check the conditional wording reads sensibly in a room that
does not have every feature
- [ ] The revision date is on the printed copy
### Wi-Fi — look, do not touch
The laptop's Wi-Fi is already working through NetworkManager and its
ifupdown stanza is already gone. **Do not run `--adopt` on it.**
- [ ] `classroom-wifi --check` says there is no wireless interface under
ifupdown's control
- [ ] `classroom-wifi --adopt` says "nothing to adopt" and changes nothing
- [ ] `nmcli -g 802-11-wireless.hidden connection show "ROGC Classroom"`
still reports `yes`
- [ ] The laptop still reconnects after a reboot
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Open this computer's own manual.
classroom-manual open it
classroom-manual --features say what this device would show
One manual is installed on every machine, carrying every page. What makes
it THIS room's manual is that the pages for features this device does not
have are removed before a teacher ever sees them: a Bible study laptop
opens it and reads about the Browser, Presentations and the volume, with
no mention of Wonder Ink or Parent Call to wonder about.
WHAT REACHES THE CLASSROOM ACCOUNT, AND WHAT DOES NOT.
Only a list of setting NAMES — ["WONDERINK_ENABLED", ...] — and only
those the package already defines as optional applications. No values
from device.conf, no broker address, no broker password, no update URL,
no Wi-Fi anything, no other key an administrator has put in that file.
That list is prepared by classroom-desktop-layout, which is already the
one place that decides what this device offers, and left in the runtime
directory where it is readable and carries nothing worth protecting. If
it is not there, every page is shown — which is exactly the printed
manual, and is never wrong, only less specific.
Shown through Chrome in application mode: no address bar, no tabs, one
window that closes. Chrome is on every classroom machine already, the
manual is a local file, and none of it needs the network.
"""
import argparse
import html
import json
import os
import subprocess
import sys
import tempfile
sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
sys.path.insert(
0,
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"),
)
from classroom_config import FEATURES, feature, read_device_config # noqa: E402
MANUAL = "/usr/local/share/ekklesia-classroom/manual/classroom-manual.html"
# Written by classroom-desktop-layout at boot. See the module docstring
# for what is and is not in it.
MANIFEST = "/run/ekklesia-classroom/features.json"
def say(message):
print(f"classroom-manual: {message}")
def enabled_features():
"""The optional applications this device offers, by setting name."""
try:
with open(MANIFEST, "r", encoding="utf-8") as handle:
listed = json.load(handle)
if isinstance(listed, list):
# Filtered against what this package knows, so a stale or
# hand-edited manifest can only ever name a real feature.
return [name for name in listed if name in FEATURES]
except (OSError, ValueError):
pass
# No manifest. Fall back to asking device.conf directly for the
# feature keys and nothing else — the same question, one step less
# removed. A root-run --features does this; a classroom session
# normally has the manifest.
config = read_device_config()
return [name for name in sorted(FEATURES) if feature(config, name)]
def page(features):
"""The installed manual, with this device's features declared in it."""
with open(MANUAL, "r", encoding="utf-8") as handle:
text = handle.read()
declaration = (
"<script>window.CLASSROOM_FEATURES = "
+ json.dumps(sorted(features))
+ ";</script>"
)
# Before the viewer script, which reads it. Nothing else in the file
# is touched, so what a teacher reads is the file the package
# installed.
return text.replace("<body>", "<body>" + declaration, 1)
def main():
parser = argparse.ArgumentParser(
description="Open this computer's manual.")
parser.add_argument("--features", action="store_true",
help="say what this device would show; opens nothing")
arguments = parser.parse_args()
features = enabled_features()
if arguments.features:
for name in sorted(FEATURES):
print(f" {name:<24} {'shown' if name in features else 'hidden'}")
return 0
if not os.path.exists(MANUAL):
say(f"the manual is not installed at {MANUAL}")
return 1
# Composed into the teacher's own cache rather than beside the
# installed file, which is root's and read-only.
cache = os.path.join(
os.environ.get("XDG_CACHE_HOME")
or os.path.expanduser("~/.cache"),
"ekklesia-classroom")
os.makedirs(cache, exist_ok=True)
try:
composed = os.path.join(cache, "manual.html")
with tempfile.NamedTemporaryFile(
"w", encoding="utf-8", dir=cache, delete=False,
suffix=".html") as handle:
handle.write(page(features))
temporary = handle.name
os.replace(temporary, composed)
except OSError as error:
say(f"could not prepare the manual: {error}")
return 1
scale = "1"
try:
scale = subprocess.run(
["/usr/local/bin/classroom-ui-scale"], capture_output=True,
text=True, timeout=10, check=False).stdout.strip() or "1"
except (OSError, subprocess.SubprocessError):
pass
try:
# Application mode: the manual in a window of its own, with no
# address bar for a teacher to end up somewhere else in. The
# room's ordinary browser profile is deliberately not used, so
# opening Help cannot disturb a lesson that is already open in
# Chrome.
os.execvp("google-chrome", [
"google-chrome",
"--ozone-platform=wayland",
f"--force-device-scale-factor={scale}",
f"--user-data-dir={os.path.join(cache, 'manual-profile')}",
"--no-first-run",
"--no-default-browser-check",
"--password-store=basic",
"--hide-crash-restore-bubble",
f"--app=file://{composed}",
])
except OSError as error:
say(f"could not open the browser: {error}")
say(f"the manual itself is at {composed}")
return 1
if __name__ == "__main__":
sys.exit(main())
+263
View File
@@ -0,0 +1,263 @@
#!/bin/bash
# The teacher's manual: one source, two versions, and no drift.
#
# Two halves, because the manual has two failure modes and they are not
# alike. The source can be wrong — a page for an optional feature with
# no conditional sentence makes the PRINTED manual state it
# unconditionally, which is wrong in every room that does not have it.
# And the built page can be wrong — it can show a teacher a feature
# their computer does not have, or hide one it does, or carry something
# out of device.conf that the classroom account has no business seeing.
#
# The second half runs the manual's own viewer script in node against a
# stub DOM, the same way test-classroom-network.sh evaluates the polkit
# rule: it is the actual shipped code, not a description of it.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
echo "A. the source, and the release gate"
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
import subprocess
import sys
from pathlib import Path
root = Path(sys.argv[1])
sys.path.insert(0, str(root / "lib"))
from classroom_config import FEATURES
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
if detail:
print(f" {detail}")
failed += 1
source = root / "docs" / "manual"
build = source / "build"
pages = sorted(p for p in source.glob("*.md") if p.name != "README.md")
check("there is a manual source directory", source.is_dir())
check("with pages in it", len(pages) >= 5, str(len(pages)))
headers = {}
for path in pages:
text = path.read_text(encoding="utf-8")
header = {}
if text.startswith("---\n"):
end = text.index("\n---\n", 3)
for line in text[4:end].splitlines():
if ":" in line:
key, value = line.split(":", 1)
header[key.strip()] = value.strip()
headers[path.name] = header
for name, header in headers.items():
check(f"{name} has a title", bool(header.get("title")))
# THE RULE THAT KEEPS THE PRINTED MANUAL TRUE.
for name, header in headers.items():
if header.get("feature"):
check(f"{name} says when it applies, for the printed manual",
bool(header.get("printed-if")),
"without it the printed manual states an optional feature "
"as though every room had it")
check(f"{name} names a real device.conf feature",
header["feature"] in FEATURES,
f"{header.get('feature')} is not one of "
f"{', '.join(sorted(FEATURES))}")
documented = {h.get("feature") for h in headers.values() if h.get("feature")}
for name in FEATURES:
check(f"{name} has a manual page", name in documented,
"a room would get a button the manual never mentions")
orders = [h.get("order") for h in headers.values()]
check("no two pages claim the same place", len(orders) == len(set(orders)),
str(sorted(orders)))
print(" -- the builder itself")
builder = root / "tools" / "build-manual"
check("it exists and is runnable", builder.exists()
and builder.stat().st_mode & 0o111)
result = subprocess.run([str(builder)], capture_output=True, text=True,
timeout=120)
check("it builds without complaint", result.returncode == 0,
result.stderr.strip())
check("it produced the digital manual",
(build / "classroom-manual.html").exists())
check("and the printable one", (build / "classroom-manual.pdf").exists())
pdf = (build / "classroom-manual.pdf").read_bytes()
check("the PDF is a real PDF", pdf.startswith(b"%PDF-")
and pdf.rstrip().endswith(b"%%EOF"), str(pdf[:8]))
check("with more than one page", pdf.count(b"/Type /Page ") > 1,
str(pdf.count(b"/Type /Page ")))
check("and a revision date on it, so an old printout can be spotted",
b"Revision" in pdf)
# Every conditional sentence has to actually reach the printed manual.
for name, header in headers.items():
if header.get("printed-if"):
words = header["printed-if"].encode("latin-1", "replace")
check(f"{name}'s conditional sentence is in the PDF",
words in pdf, header["printed-if"])
check("the check mode passes on what was just built",
subprocess.run([str(builder), "--check"], capture_output=True,
timeout=120).returncode == 0)
html = (build / "classroom-manual.html").read_text(encoding="utf-8")
for name, header in headers.items():
if header.get("feature"):
check(f"{name} is marked in the HTML with its feature",
f'data-feature="{header["feature"]}"' in html)
check("the digital manual needs nothing from the network",
"http://" not in html and "https://" not in html,
"it has to work in a building with no internet")
print()
print(f"{passed} passed, {failed} failed")
raise SystemExit(1 if failed else 0)
PY
status=$?
[[ $status -ne 0 ]] && exit $status
echo
echo "B. what a device actually shows"
if ! command -v node >/dev/null 2>&1; then
echo " SKIP node is not installed; the viewer script was not evaluated"
exit 0
fi
node - "$ROOT" <<'JS'
const fs = require("fs");
const path = require("path");
const root = process.argv[2];
let passed = 0, failed = 0;
function check(label, condition, detail) {
if (condition) { console.log(" ok " + label); passed++; }
else { console.log(" FAIL " + label); if (detail) console.log(" " + detail); failed++; }
}
const html = fs.readFileSync(
path.join(root, "docs/manual/build/classroom-manual.html"), "utf8");
// The shipped viewer, pulled out of the built page and run for real.
const script = html.slice(html.lastIndexOf("<script>") + 8,
html.lastIndexOf("</script>"));
// Enough DOM for it. Sections carry the feature they need, exactly as
// the builder wrote them.
function sectionsOf(text) {
const found = [];
const re = /<section id="([^"]+)"(?: data-feature="([^"]+)")?>/g;
let m;
while ((m = re.exec(text)) !== null) {
found.push({id: m[1], feature: m[2] || null});
}
return found;
}
function show(features) {
const sections = sectionsOf(html).map(s => ({
id: s.id,
removed: false,
getAttribute: function (name) {
return name === "data-feature" ? s.feature : null;
},
remove: function () { this.removed = true; },
querySelector: function () { return {textContent: s.id}; },
}));
const contents = {children: [], appendChild: function (c) { this.children.push(c); }};
const window = {CLASSROOM_FEATURES: features};
const document = {
querySelectorAll: () => sections,
getElementById: () => contents,
createElement: () => ({}),
};
new Function("window", "document", script)(window, document);
return sections.filter(s => !s.removed).map(s => s.id);
}
const all = sectionsOf(html).map(s => s.id);
const optional = sectionsOf(html).filter(s => s.feature);
check("the built manual carries every page",
all.length >= 5, all.join(", "));
check("and some of them are optional", optional.length >= 3,
optional.map(s => s.feature).join(", "));
console.log(" -- the Bible study laptop: Presentations, and nothing else optional");
const laptop = show(["PRESENTATIONS_ENABLED"]);
check("the Presentations page is shown",
laptop.some(id => id.includes("presentations")), laptop.join(", "));
check("the Browser page is shown, because every room has one",
laptop.some(id => id.includes("browser")));
check("sound and volume are shown", laptop.some(id => id.includes("sound")));
check("Wonder Ink is NOT shown",
!laptop.some(id => id.includes("wonderink")), laptop.join(", "));
check("Parent Call is NOT shown",
!laptop.some(id => id.includes("parent-call")), laptop.join(", "));
check("the DVD page is NOT shown",
!laptop.some(id => id.includes("dvd")), laptop.join(", "));
console.log(" -- a children's classroom: everything but Presentations");
const classroom = show(["WONDERINK_ENABLED", "PARENT_CALL_ENABLED", "DVD_ENABLED"]);
check("Wonder Ink is shown", classroom.some(id => id.includes("wonderink")));
check("Parent Call is shown", classroom.some(id => id.includes("parent-call")));
check("the DVD page is shown", classroom.some(id => id.includes("dvd")));
check("Presentations is NOT shown",
!classroom.some(id => id.includes("presentations")), classroom.join(", "));
console.log(" -- a device that could not say what it has");
// No manifest: show everything, which is the printed manual and is never
// wrong, only less specific.
const unknown = show(null);
check("every page is shown rather than none",
unknown.length === all.length, unknown.length + " of " + all.length);
console.log(" -- nothing optional at all");
const bare = show([]);
check("only the common pages remain",
bare.length === all.length - optional.length,
bare.join(", "));
console.log(" -- what the page is allowed to know about the device");
const manual = fs.readFileSync(path.join(root, "scripts/classroom-manual"), "utf8");
check("the manual is handed a list of setting NAMES, not device.conf",
/window\.CLASSROOM_FEATURES/.test(manual)
&& /json\.dumps\(sorted\(features\)\)/.test(manual));
check("and the names are filtered against the features the package knows",
/name for name in listed if name in FEATURES/.test(manual),
"a stale manifest must not be able to name anything else");
const layout = fs.readFileSync(path.join(root, "scripts/classroom-desktop-layout"), "utf8");
check("the manifest is written from the enabled features alone",
/setting for variable, setting in FEATURE_LINES if answers\.get\(variable\)/.test(layout));
check("no value from device.conf is written into it",
!/read_device_config\(\)\[/.test(layout));
check("it lives in the runtime directory, so it cannot go stale",
/\/run\/ekklesia-classroom\/features\.json/.test(layout));
console.log();
console.log(passed + " passed, " + failed + " failed");
process.exit(failed ? 1 : 0);
JS
+736
View File
@@ -0,0 +1,736 @@
#!/usr/bin/env python3
"""Build the teacher's manual, both of it, from docs/manual.
tools/build-manual build everything into docs/manual/build
tools/build-manual --check verify only; what the release runs
TWO OUTPUTS, ONE SOURCE.
classroom-manual.html installed on every classroom computer. It
carries every page, each marked with the
device.conf setting that decides whether that
computer shows it, and hides the ones this
device does not have when it is opened.
classroom-manual.pdf the universal printed manual. Every page,
every feature, with a conditional sentence
above the optional ones so that it stays true
in a room that does not have them all.
They cannot drift apart because neither is written: both are generated
from the same Markdown, and --check fails the release if a page is
missing what the printed version needs.
WHY THE PDF IS WRITTEN HERE RATHER THAN CONVERTED.
A manual is headings, paragraphs, bullets, numbered steps and one small
table. Writing that into a PDF directly is a couple of hundred lines with
the standard fonts, no embedding and no dependency — and it means a
release can be built on any machine with Python. Converting the HTML
through LibreOffice was the alternative: it needs LibreOffice on whatever
builds the release, it lays a web page out like a web page, and its
output changes with the version installed. This does not.
"""
import argparse
import base64
import html
import mimetypes
import re
import sys
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / "docs" / "manual"
BUILD = SOURCE / "build"
sys.path.insert(0, str(ROOT / "lib"))
from classroom_config import FEATURES # noqa: E402
# ------------------------------------------------------------ reading
def parse(path):
"""One manual page: its header, and its body as a list of blocks."""
text = path.read_text(encoding="utf-8")
header = {}
if text.startswith("---\n"):
end = text.index("\n---\n", 3)
for line in text[4:end].splitlines():
if ":" in line:
key, value = line.split(":", 1)
header[key.strip()] = value.strip()
text = text[end + 5:]
return header, blocks(text)
_IMAGE = re.compile(r"^!\[([^\]]*)\]\(([^)]+)\)\s*$")
def starts_block(line):
"""Whether this line begins something that is not a paragraph.
One test, used by every branch below AND by the paragraph loop, so
the two cannot disagree about a line. They did: the paragraph loop
stopped at any line beginning with "*", while the bullet rule needed
"* " — so a paragraph opening with a bold word consumed nothing,
advanced nothing, and the build hung. Every page whose first line is
bold hit it.
"""
return (line.startswith(("## ", "### ", "* ", "|"))
or bool(re.match(r"^\d+\. ", line))
or bool(_IMAGE.match(line)))
def gather(lines, index, marker):
"""A whole list, with each item's wrapped continuation lines folded in.
A step written across two lines in the source is one step, not a step
and a paragraph. Without this the list ended at the first wrapped
item and the next marker started a NEW list — so a three-step
procedure printed as three separate steps all numbered 1, which is
the worst possible way to get an instruction wrong.
"""
items = []
pattern = re.compile(marker)
while index < len(lines):
line = lines[index]
if pattern.match(line):
items.append(re.sub(marker, "", line).strip())
index += 1
elif items and line.strip() and line[0] in " \t":
# An indented line under an item belongs to that item.
items[-1] += " " + line.strip()
index += 1
else:
break
return items, index
def blocks(text):
"""Markdown, as much of it as a teacher's manual needs.
Headings, paragraphs, bullets, numbered steps and a simple table.
Deliberately not a Markdown library: the same text has to come out
right as a web page and as typeset print, and the small fixed set
that does is easier to keep honest than a general one.
"""
out = []
lines = text.splitlines()
index = 0
while index < len(lines):
line = lines[index].rstrip()
if not line.strip():
index += 1
continue
if _IMAGE.match(line):
match = _IMAGE.match(line)
out.append(("image", (match.group(1), match.group(2))))
index += 1
elif line.startswith("### "):
out.append(("h3", line[4:].strip()))
index += 1
elif line.startswith("## "):
out.append(("h2", line[3:].strip()))
index += 1
elif line.startswith("| "):
rows = []
while index < len(lines) and lines[index].startswith("|"):
cells = [c.strip() for c in lines[index].strip().strip("|").split("|")]
# The |---|---| rule under a header row is not data.
if not all(set(c) <= set("-: ") for c in cells):
rows.append(cells)
index += 1
out.append(("table", rows))
elif line.startswith("* "):
items, index = gather(lines, index, r"^\* ")
out.append(("bullets", items))
elif re.match(r"^\d+\. ", line):
items, index = gather(lines, index, r"^\d+\. ")
out.append(("steps", items))
else:
# Always consumes the line it was given, whatever it is, so
# this loop can never fail to advance.
paragraph = [line.strip()]
index += 1
while index < len(lines) and lines[index].strip() and \
not starts_block(lines[index]):
paragraph.append(lines[index].strip())
index += 1
out.append(("p", " ".join(paragraph)))
return out
def pages():
found = []
for path in sorted(SOURCE.glob("*.md")):
if path.name == "README.md":
continue
header, body = parse(path)
header["path"] = path
found.append((header, body))
found.sort(key=lambda page: int(page[0].get("order", "99")))
return found
# ------------------------------------------------------------ marking
_BOLD = re.compile(r"\*\*(.+?)\*\*")
def inline_html(text):
return _BOLD.sub(r"<strong>\1</strong>", html.escape(text))
def plain(text):
"""The same text with its marking removed, for the PDF."""
return _BOLD.sub(r"\1", text)
# --------------------------------------------------------------- HTML
STYLE = """
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0; padding: 0 24px 80px;
background: #141c20; color: #e6edef;
font-family: "Noto Sans", "DejaVu Sans", sans-serif;
font-size: 19px; line-height: 1.65;
}
.sheet { max-width: 46rem; margin: 0 auto; }
h1 { font-size: 2.1rem; margin: 2rem 0 0.2rem; }
h2 { font-size: 1.5rem; margin: 2.4rem 0 0.4rem;
border-bottom: 1px solid #25333a; padding-bottom: .3rem; }
h3 { font-size: 1.15rem; margin: 1.6rem 0 0.3rem; color: #9fd8d1; }
p, li { margin: .5rem 0; }
ol, ul { padding-left: 1.4rem; }
strong { color: #ffffff; }
table { border-collapse: collapse; margin: 1rem 0; width: 100%; }
td, th { border: 1px solid #25333a; padding: .5rem .7rem; text-align: left; }
th { background: #0f171a; }
nav { margin: 1.5rem 0 2.5rem; padding: 1rem 1.2rem;
background: #0f171a; border: 1px solid #25333a; border-radius: 14px; }
nav a { color: #7fd0c6; text-decoration: none; display: block; padding: .2rem 0; }
nav a:hover { text-decoration: underline; }
figure { margin: 1.2rem 0; }
figure img { max-width: 100%; height: auto; display: block;
border: 1px solid #25333a; border-radius: 12px; }
figcaption { color: #93a5aa; font-size: .9rem; margin-top: .4rem; }
.revision { color: #93a5aa; font-size: .95rem; }
.top { color: #7fd0c6; text-decoration: none; font-size: .95rem; }
@media print {
body { background: #fff; color: #000; font-size: 11pt; }
nav, .top { display: none; }
}
"""
def embedded(source):
"""A picture as a data: URI, so the manual stays ONE file.
The installed manual is composed into the teacher's cache with this
device's features declared in it, so a relative path would point at a
directory that is not beside the copy being opened. Embedding means
the file works wherever it is put, with nothing to install alongside
it and nothing to go missing — which is also what makes it work with
the network unplugged.
"""
path = SOURCE / source
kind = mimetypes.guess_type(path.name)[0] or "image/png"
data = base64.b64encode(path.read_bytes()).decode("ascii")
return f"data:{kind};base64,{data}"
def render_html(all_pages, revision):
parts = [
"<!DOCTYPE html>", '<html lang="en">', "<head>",
'<meta charset="utf-8">',
'<meta name="viewport" content="width=device-width, initial-scale=1">',
"<title>Classroom manual</title>",
f"<style>{STYLE}</style>", "</head>", "<body>", '<div class="sheet">',
"<h1>Classroom manual</h1>",
f'<p class="revision">River of Glory &middot; {revision}</p>',
"<nav id='contents'></nav>",
]
for header, body in all_pages:
feature = header.get("feature", "")
anchor = header["path"].stem
# The one thing the page carries into the browser: which setting
# decides whether this device shows it. No value, no other
# setting, and nothing about the machine.
attribute = f' data-feature="{html.escape(feature)}"' if feature else ""
parts.append(f'<section id="{anchor}"{attribute}>')
parts.append(f'<h2>{inline_html(header["title"])}</h2>')
for kind, content in body:
if kind == "h2":
parts.append(f"<h3>{inline_html(content)}</h3>")
elif kind == "h3":
parts.append(f"<h4>{inline_html(content)}</h4>")
elif kind == "p":
parts.append(f"<p>{inline_html(content)}</p>")
elif kind == "bullets":
parts.append("<ul>" + "".join(
f"<li>{inline_html(i)}</li>" for i in content) + "</ul>")
elif kind == "steps":
parts.append("<ol>" + "".join(
f"<li>{inline_html(i)}</li>" for i in content) + "</ol>")
elif kind == "image":
alt, source = content
parts.append(
f'<figure><img src="{embedded(source)}" '
f'alt="{html.escape(alt)}">'
f'<figcaption>{inline_html(alt)}</figcaption></figure>')
elif kind == "table":
rows = []
for number, row in enumerate(content):
tag = "th" if number == 0 else "td"
rows.append("<tr>" + "".join(
f"<{tag}>{inline_html(c)}</{tag}>" for c in row) +
"</tr>")
parts.append("<table>" + "".join(rows) + "</table>")
parts.append('<p><a class="top" href="#contents">Back to the top</a></p>')
parts.append("</section>")
parts.append("</div>")
parts.append(f"<script>{VIEWER}</script>")
parts.append("</body></html>")
return "\n".join(parts)
# What the manual does when a teacher opens it.
#
# classroom-manual writes the device's enabled features into
# window.CLASSROOM_FEATURES before this runs. It is a list of setting
# names and nothing else — no values from device.conf, no broker
# password, no addresses, nothing an administrator set. With no list at
# all, every page is shown, which is the printed manual's behaviour and
# is always true, just less specific.
VIEWER = """
(function () {
var have = window.CLASSROOM_FEATURES || null;
var sections = document.querySelectorAll('section');
var contents = document.getElementById('contents');
for (var i = 0; i < sections.length; i++) {
var needs = sections[i].getAttribute('data-feature');
if (needs && have && have.indexOf(needs) === -1) {
sections[i].remove();
continue;
}
var link = document.createElement('a');
link.href = '#' + sections[i].id;
link.textContent = sections[i].querySelector('h2').textContent;
contents.appendChild(link);
}
})();
"""
# ---------------------------------------------------------------- PDF
# The 14 fonts every PDF reader has. Nothing is embedded, so the file is
# small and identical everywhere.
BODY_FONT = "Helvetica"
BOLD_FONT = "Helvetica-Bold"
PAGE_WIDTH, PAGE_HEIGHT = 595, 842 # A4 in points
MARGIN_X, MARGIN_TOP, MARGIN_BOTTOM = 62, 780, 64
# Helvetica's own widths, per 1000 units, for the printable ASCII range.
# Wrapping by character count puts a line of capitals off the page and
# leaves a line of commas half empty, and this is a manual people print.
_WIDTHS = {
" ": 278, "!": 278, '"': 355, "#": 556, "$": 556, "%": 889, "&": 667,
"'": 191, "(": 333, ")": 333, "*": 389, "+": 584, ",": 278, "-": 333,
".": 278, "/": 278, ":": 278, ";": 278, "<": 584, "=": 584, ">": 584,
"?": 556, "@": 1015, "[": 278, "\\": 278, "]": 278, "^": 469, "_": 556,
"`": 333, "{": 334, "|": 260, "}": 334, "~": 584,
}
def width_of(text, size, bold=False):
total = 0
for character in text:
if character.isdigit():
total += 556
elif character.isupper():
total += 722 if bold else 667
elif character.islower():
total += 556 if bold else 500
else:
total += _WIDTHS.get(character, 556)
return total * size / 1000.0
def wrap(text, size, available, bold=False):
lines = []
current = ""
for word in text.split():
candidate = f"{current} {word}".strip()
if current and width_of(candidate, size, bold) > available:
lines.append(current)
current = word
else:
current = candidate
if current:
lines.append(current)
return lines or [""]
def escape_pdf(text):
out = []
for character in text:
if character in "()\\":
out.append("\\" + character)
elif ord(character) < 128:
out.append(character)
else:
# The standard encoding has no place for a curly quote or a
# dash a writer typed without thinking. Replaced rather than
# dropped, so a printed line never loses a word.
out.append({
"’": "'", "‘": "'", "“": '"', "”": '"',
"—": "-", "–": "-", " ": " ", "·": "-",
"→": "->", "←": "<-", "↑": "up",
"↓": "down", "×": "x", "⏏": "eject",
"✕": "X", "◀": "<", "▶": ">",
}.get(character, "?"))
return "".join(out)
class Pdf:
"""A very small PDF writer: text, in two weights, on A4 pages."""
def __init__(self):
self.pages = []
self.current = []
self.y = MARGIN_TOP
def _show(self, text, x, y, size, bold):
font = "F2" if bold else "F1"
self.current.append(
f"BT /{font} {size} Tf 1 0 0 1 {x:.1f} {y:.1f} Tm "
f"({escape_pdf(text)}) Tj ET")
def break_page(self):
if self.current:
self.pages.append(self.current)
self.current = []
self.y = MARGIN_TOP
def space(self, amount):
self.y -= amount
def need(self, amount):
if self.y - amount < MARGIN_BOTTOM:
self.break_page()
def text(self, content, size=10.5, bold=False, indent=0, leading=None):
leading = leading or size * 1.42
available = PAGE_WIDTH - 2 * MARGIN_X - indent
for line in wrap(content, size, available, bold):
self.need(leading)
self._show(line, MARGIN_X + indent, self.y, size, bold)
self.y -= leading
def bullet(self, mark, content, size=10.5, indent=16):
leading = size * 1.42
available = PAGE_WIDTH - 2 * MARGIN_X - indent - 14
lines = wrap(content, size, available)
self.need(leading * len(lines))
self._show(mark, MARGIN_X + indent, self.y, size, False)
for number, line in enumerate(lines):
if number:
self.need(leading)
self._show(line, MARGIN_X + indent + 16, self.y, size, False)
self.y -= leading
def rule(self):
self.need(10)
self.current.append(
f"0.75 w 0.6 0.6 0.6 RG {MARGIN_X} {self.y + 4:.1f} m "
f"{PAGE_WIDTH - MARGIN_X} {self.y + 4:.1f} l S")
self.y -= 8
def build(self):
self.break_page()
objects = []
count = len(self.pages)
# 1 catalog, 2 pages, 3..(2+n) page objects, then the contents,
# then the two fonts.
kids = " ".join(f"{3 + i} 0 R" for i in range(count))
objects.append("<< /Type /Catalog /Pages 2 0 R >>")
objects.append(
f"<< /Type /Pages /Kids [{kids}] /Count {count} >>")
for index in range(count):
contents = 3 + count + index
objects.append(
f"<< /Type /Page /Parent 2 0 R "
f"/MediaBox [0 0 {PAGE_WIDTH} {PAGE_HEIGHT}] "
f"/Resources << /Font << /F1 {3 + 2 * count} 0 R "
f"/F2 {4 + 2 * count} 0 R >> >> "
f"/Contents {contents} 0 R >>")
for page in self.pages:
stream = "\n".join(page)
objects.append(
f"<< /Length {len(stream.encode('latin-1', 'replace'))} >>\n"
f"stream\n{stream}\nendstream")
for font in (BODY_FONT, BOLD_FONT):
objects.append(
f"<< /Type /Font /Subtype /Type1 /BaseFont /{font} "
f"/Encoding /WinAnsiEncoding >>")
out = ["%PDF-1.4"]
offsets = []
position = len(out[0]) + 1
for number, body in enumerate(objects, start=1):
chunk = f"{number} 0 obj\n{body}\nendobj"
offsets.append(position)
out.append(chunk)
position += len(chunk.encode("latin-1", "replace")) + 1
table = [f"xref\n0 {len(objects) + 1}", "0000000000 65535 f "]
table += [f"{offset:010d} 00000 n " for offset in offsets]
out.append("\n".join(table))
out.append(
f"trailer\n<< /Size {len(objects) + 1} /Root 1 0 R >>\n"
f"startxref\n{position}\n%%EOF")
return ("\n".join(out)).encode("latin-1", "replace")
def render_pdf(all_pages, revision):
pdf = Pdf()
pdf.space(60)
pdf.text("Classroom manual", size=26, bold=True)
pdf.space(6)
pdf.text("River of Glory", size=13)
pdf.space(2)
pdf.text(revision, size=10.5)
pdf.space(18)
pdf.text(
"This manual covers every classroom computer in the church. Not "
"every room has every button, so some pages begin by saying when "
"they apply. If your computer does not have what a page "
"describes, that page is not for your room.", size=10.5)
pdf.space(10)
pdf.text(
"Keep this copy in the room. If the date above is older than the "
"one on the computer's own Help, ask the office for a new one.",
size=10.5)
pdf.break_page()
for header, body in all_pages:
pdf.space(6)
pdf.text(plain(header["title"]), size=17, bold=True)
pdf.space(2)
pdf.rule()
pdf.space(6)
# THE CONDITIONAL SENTENCE.
#
# This is what lets one printed manual be true in every room. A
# page for something optional says so before it says anything
# else, so a teacher in a room without it knows within one line.
if header.get("printed-if"):
pdf.text(plain(header["printed-if"]) + ".", size=10.5, bold=True)
pdf.space(6)
for kind, content in body:
if kind == "h2":
pdf.space(8)
pdf.text(plain(content), size=12.5, bold=True)
pdf.space(3)
elif kind == "h3":
pdf.space(5)
pdf.text(plain(content), size=11, bold=True)
elif kind == "p":
pdf.text(plain(content))
pdf.space(5)
elif kind == "bullets":
for item in content:
pdf.bullet("-", plain(item))
pdf.space(5)
elif kind == "steps":
for number, item in enumerate(content, start=1):
pdf.bullet(f"{number}.", plain(item))
pdf.space(5)
elif kind == "image":
pdf.text(f"[ Picture: {plain(content[0])} — see the "
"computer's own Help ]", size=9.5)
pdf.space(5)
elif kind == "table":
for number, row in enumerate(content):
pdf.bullet(
" ", " ".join(plain(c) for c in row)
if number else
" ".join(plain(c).upper() for c in row))
pdf.space(5)
pdf.break_page()
return pdf.build()
# -------------------------------------------------------------- check
def check(all_pages):
"""Everything that would let the two versions drift apart."""
problems = []
if not all_pages:
problems.append("there are no manual pages at all")
seen = set()
for header, body in all_pages:
name = header["path"].name
for required in ("title", "order"):
if not header.get(required):
problems.append(f"{name}: no {required}")
if header.get("order") in seen:
problems.append(f"{name}: another page already has order "
f"{header.get('order')}")
seen.add(header.get("order"))
feature = header.get("feature")
if feature:
# A page for an optional feature MUST carry the sentence the
# printed manual puts above it, or the printed manual quietly
# becomes wrong for every room without that feature.
if not header.get("printed-if"):
problems.append(
f"{name}: declares feature {feature} but has no "
"printed-if, so the printed manual would state it "
"unconditionally")
if feature not in FEATURES:
problems.append(
f"{name}: {feature} is not a device.conf feature "
f"(known: {', '.join(sorted(FEATURES))})")
if not body:
problems.append(f"{name}: no content")
for kind, content in body:
if kind != "image":
continue
picture = SOURCE / content[1]
if not picture.exists():
problems.append(f"{name}: {content[1]} is not there")
elif not content[0].strip():
# The printed manual prints the alt text as the caption,
# so a picture with none leaves a bare "[ Picture: ]".
problems.append(
f"{name}: {content[1]} has no caption, which the "
"printed manual needs")
# Every optional feature the package has should be documented, or a
# room gets a button the manual never mentions.
documented = {h.get("feature") for h, _ in all_pages if h.get("feature")}
for feature in FEATURES:
if feature not in documented:
problems.append(
f"{feature} is an optional feature with no manual page")
return problems
def main():
parser = argparse.ArgumentParser(
description="Build the teacher's manual, digital and printed.")
parser.add_argument("--check", action="store_true",
help="verify the source and the built files only")
arguments = parser.parse_args()
all_pages = pages()
problems = check(all_pages)
for problem in problems:
print(f"build-manual: {problem}", file=sys.stderr)
if problems:
return 1
revision = f"Revision {date.today():%d %B %Y}"
if arguments.check:
for name in ("classroom-manual.html", "classroom-manual.pdf"):
if not (BUILD / name).exists():
print(f"build-manual: {name} has not been built; run "
"tools/build-manual", file=sys.stderr)
return 1
print(f"build-manual: {len(all_pages)} pages, "
f"{len([h for h, _ in all_pages if h.get('feature')])} of them "
"optional, and both versions are built")
return 0
BUILD.mkdir(parents=True, exist_ok=True)
(BUILD / "classroom-manual.html").write_text(
render_html(all_pages, revision), encoding="utf-8")
(BUILD / "classroom-manual.pdf").write_bytes(
render_pdf(all_pages, revision))
print(f"build-manual: {BUILD / 'classroom-manual.html'}")
print(f"build-manual: {BUILD / 'classroom-manual.pdf'}")
print(f"build-manual: {revision}")
return 0
if __name__ == "__main__":
sys.exit(main())
+59 -3
View File
@@ -52,6 +52,15 @@ done
find "$ROOT/scripts" "$ROOT/lib" "$ROOT/tests" \ find "$ROOT/scripts" "$ROOT/lib" "$ROOT/tests" \
-name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
# THE MANUAL IS PART OF THE RELEASE, NOT A JOB FOR AFTERWARDS.
#
# Built from docs/manual before the tests run, so that the test which
# checks the digital and printed versions against each other is checking
# what this package is about to ship rather than whatever was built last.
# A page that would make the printed manual wrong fails the build here.
echo "Building the teacher's manual"
"$ROOT/tools/build-manual"
for test in "$ROOT"/tests/test-*.sh; do for test in "$ROOT"/tests/test-*.sh; do
[[ -x "$test" ]] || continue [[ -x "$test" ]] || continue
@@ -348,9 +357,10 @@ install -Dm644 \
"$ROOT/config/dbus/org.kde.LogoutPrompt.service" \ "$ROOT/config/dbus/org.kde.LogoutPrompt.service" \
"$STAGE/usr/local/share/dbus-1/services/org.kde.LogoutPrompt.service" "$STAGE/usr/local/share/dbus-1/services/org.kde.LogoutPrompt.service"
# Two marks the classroom draws itself, because nothing in Breeze says # Three marks the classroom draws itself, because nothing in Breeze says
# either thing: a lesson, and fetching a child's grown-up. # any of them: a lesson, fetching a child's grown-up, and tonight's
for icon in ekklesia-wonderink ekklesia-parent-call; do # slides.
for icon in ekklesia-wonderink ekklesia-parent-call ekklesia-presentations; do
install -Dm644 \ install -Dm644 \
"$ROOT/config/icons/${icon}.svg" \ "$ROOT/config/icons/${icon}.svg" \
"$STAGE/usr/share/icons/hicolor/scalable/apps/${icon}.svg" "$STAGE/usr/share/icons/hicolor/scalable/apps/${icon}.svg"
@@ -374,6 +384,50 @@ install -Dm755 \
"$ROOT/scripts/classroom-wonderink" \ "$ROOT/scripts/classroom-wonderink" \
"$STAGE/usr/local/bin/classroom-wonderink" "$STAGE/usr/local/bin/classroom-wonderink"
# PRESENTATIONS.
#
# The library a teacher sees, and the watcher that fills it from their
# downloads. Both are installed on every machine; PRESENTATIONS_ENABLED
# in device.conf is what decides whether either ever runs, and it
# defaults to false so an existing classroom upgrades unchanged.
# THE MANUAL.
#
# One HTML file carrying every page, and the command that opens it with
# the pages this device has no use for taken out. Installed on every
# machine including ones with no optional features at all, because the
# common pages — the bar, the browser, the volume, what to do when
# something is wrong — are the ones a new teacher needs most.
install -Dm644 \
"$ROOT/docs/manual/build/classroom-manual.html" \
"$STAGE/usr/local/share/ekklesia-classroom/manual/classroom-manual.html"
# The printable one goes on the machine too. A room whose printed copy has
# gone missing can print another without anybody fetching a file from a
# laptop, and its revision date is the same one the digital manual shows.
install -Dm644 \
"$ROOT/docs/manual/build/classroom-manual.pdf" \
"$STAGE/usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf"
install -Dm755 \
"$ROOT/scripts/classroom-manual" \
"$STAGE/usr/local/bin/classroom-manual"
install -Dm755 \
"$ROOT/scripts/classroom-presentations" \
"$STAGE/usr/local/bin/classroom-presentations"
install -Dm755 \
"$ROOT/scripts/classroom-presentation-import" \
"$STAGE/usr/local/bin/classroom-presentation-import"
install -Dm644 \
"$ROOT/lib/classroom_presentations.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_presentations.py"
install -Dm644 \
"$ROOT/systemd/user/ekklesia-classroom-presentations.service" \
"$STAGE/etc/systemd/user/ekklesia-classroom-presentations.service"
install -Dm755 \ install -Dm755 \
"$ROOT/scripts/classroom-audio" \ "$ROOT/scripts/classroom-audio" \
"$STAGE/usr/local/bin/classroom-audio" "$STAGE/usr/local/bin/classroom-audio"
@@ -482,6 +536,8 @@ install -Dm644 \
"$ROOT/lib/classroom_icons.py" \ "$ROOT/lib/classroom_icons.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_icons.py" "$STAGE/usr/local/lib/ekklesia-classroom/classroom_icons.py"
# The palette and the overlay measurements. The DVD control bar, Parent
# Call and the Presentations library all draw themselves from this.
install -Dm644 \ install -Dm644 \
"$ROOT/lib/classroom_style.py" \ "$ROOT/lib/classroom_style.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_style.py" "$STAGE/usr/local/lib/ekklesia-classroom/classroom_style.py"
+175
View File
@@ -143,6 +143,181 @@ grep -q "classroom-wifi --check" /usr/local/bin/classroom-migrate
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \ PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
'import classroom_network; classroom_network.parse("")' 'import classroom_network; classroom_network.parse("")'
# THE 0.2.5 WI-FI REGRESSION, ASSERTED ON A REAL INSTALL.
#
# The prayer-room laptop's network is hidden and its name has a space in
# it. The migration dropped the hidden flag, so the connection it built
# could never associate. Checked against the module the package actually
# installed, not the one in a checkout.
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 <<'WIFI_CHECK'
import classroom_network as n
stanza = n.wireless(n.parse(
'allow-hotplug wlp2s0\n'
'iface wlp2s0 inet dhcp\n'
'\twpa-ssid "ROGC Classroom"\n'
'\twpa-psk se#cret\n'
'\twpa-scan-ssid 1\n'
), lambda device: True)[0]
found = n.profile(stanza)
assert found["ssid"] == "ROGC Classroom", found
assert found["hidden"] is True, "the hidden flag was dropped again"
assert found["psk"] == "se#cret", "a # in the password was truncated again"
assert found["key_mgmt"] == "wpa-psk", found
# A rollback puts back exactly what it commented out, and nothing else.
before = ("auto enp1s0\niface enp1s0 inet static\n\taddress 10.40.0.9/24\n\n"
"allow-hotplug wlp2s0\niface wlp2s0 inet dhcp\n\twpa-ssid Sanctuary\n")
after = n.comment_out(before, n.wireless(n.parse(before),
lambda d: d == "wlp2s0")[0])
restored, count = n.uncomment(after)
assert count, "nothing was marked, so a rollback would have nothing to undo"
assert restored == before, "the rollback did not restore the file exactly"
WIFI_CHECK
# And the command itself carries both halves of the fix.
grep -q '802-11-wireless.hidden' /usr/local/bin/classroom-wifi
grep -q 'managed", "yes"' /usr/local/bin/classroom-wifi
# PRESENTATIONS.
#
# Off by default, with everything it needs present on every machine, so
# enabling it on a device is one line in device.conf and a reboot.
command -v classroom-presentations >/dev/null
command -v classroom-presentation-import >/dev/null
command -v soffice >/dev/null
command -v pdftoppm >/dev/null
command -v pdfinfo >/dev/null
[[ -f /etc/systemd/user/ekklesia-classroom-presentations.service ]]
# Enabled the same way the audio policy is, so it actually reaches a
# session rather than merely being installed. A unit file nobody starts
# is a teacher whose download never appears.
[[ -L /etc/systemd/user/plasma-workspace.target.wants/ekklesia-classroom-presentations.service ]]
[[ -f /usr/share/applications/ekklesia-presentations.desktop ]]
[[ -f /usr/share/icons/hicolor/scalable/apps/ekklesia-presentations.svg ]]
# A classroom that has never heard of the setting comes up without it.
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
'from classroom_config import feature; assert feature({}, "PRESENTATIONS_ENABLED") is False'
# Disabled is the default, so both commands decline to run and say why.
classroom-presentations 2>&1 | grep -q 'PRESENTATIONS_ENABLED is false'
classroom-presentation-import --once 2>&1 | grep -q 'PRESENTATIONS_ENABLED is false'
# THE CONVERSION THIS WHOLE FEATURE RESTS ON.
#
# On a real Debian, with the fonts this package installs. No window, no
# splash, no first-run dialog, no editor — measured rather than assumed,
# because "LibreOffice is installed" is not the same claim as "a PPTX
# becomes slides a teacher can be shown".
mkdir -p /tmp/deck
python3 <<'MAKE_DECK'
page = ('<draw:page draw:name="p%d"><draw:frame svg:width="20cm" '
'svg:height="3cm" svg:x="1cm" svg:y="2cm"><draw:text-box>'
'<text:p>%s</text:p></draw:text-box></draw:frame></draw:page>')
open("/tmp/deck/deck.fodp", "w").write(
'<?xml version="1.0" encoding="UTF-8"?>'
'<office:document '
'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" '
'xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" '
'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" '
'xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" '
'xmlns:presentation='
'"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" '
'office:version="1.3" '
'office:mimetype="application/vnd.oasis.opendocument.presentation">'
'<office:body><office:presentation>'
+ (page % (1, "Wednesday Bible Study"))
+ (page % (2, "Session 7"))
+ '</office:presentation></office:body></office:document>')
MAKE_DECK
HOME=/tmp/deck timeout 240 soffice --headless --norestore --nologo \
--convert-to pptx --outdir /tmp/deck /tmp/deck/deck.fodp >/dev/null 2>&1
[[ -f /tmp/deck/deck.pptx ]]
PYTHONPATH=/usr/local/lib/ekklesia-classroom HOME=/tmp/deck python3 <<'CONVERT_CHECK'
import pathlib
import classroom_presentations as library
ok, error = library.convert_to_pdf("/tmp/deck/deck.pptx", "/tmp/deck/slides.pdf")
assert ok, f"PPTX could not be converted: {error}"
pages = library.page_count("/tmp/deck/slides.pdf")
assert pages == 2, f"expected 2 slides, got {pages}"
made, error = library.render_pages("/tmp/deck/slides.pdf", "/tmp/deck/pages")
assert made == 2, f"expected 2 rendered pages, got {made}: {error}"
# The library asks for page-001.png whatever the page count's width is.
for number in (1, 2):
path = pathlib.Path(f"/tmp/deck/pages/page-{number:03d}.png")
assert path.exists(), f"{path} was not rendered"
assert path.stat().st_size > 0, f"{path} is empty"
# Slide 2 is the one a teacher picks as the card picture, so its text has
# to have survived the conversion.
text = library.first_text("/tmp/deck/slides.pdf", pages=2)
assert "Session 7" in text, f"text did not survive the conversion: {text!r}"
CONVERT_CHECK
# The fonts a Google Slides export asks for, by their real names.
fc-list 2>/dev/null | grep -qi 'Liberation Sans'
fc-list 2>/dev/null | grep -qi 'Carlito'
fc-list 2>/dev/null | grep -qi 'Caladea'
# And the one that must NOT be here, because it prompts for a licence.
! dpkg -l ttf-mscorefonts-installer 2>/dev/null | grep -q '^ii'
# THE MANUAL.
#
# Installed on every machine, works with no network at all, and shows
# only the features this device has.
[[ -f /usr/local/share/ekklesia-classroom/manual/classroom-manual.html ]]
[[ -f /usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf ]]
command -v classroom-manual >/dev/null
[[ -f /usr/share/applications/ekklesia-manual.desktop ]]
head -c 5 /usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf \
| grep -q '%PDF-'
grep -aq 'Revision' /usr/local/share/ekklesia-classroom/manual/classroom-manual.pdf
! grep -qE 'https?://' \
/usr/local/share/ekklesia-classroom/manual/classroom-manual.html
# The manifest the manual reads carries feature NAMES and nothing else:
# no broker password, no addresses, nothing else out of device.conf.
printf 'PRESENTATIONS_ENABLED=true\nWONDERINK_ENABLED=false\nMQTT_PASSWORD=never-a-teacher\n' \
>> /etc/ekklesia-classroom/device.conf
classroom-desktop-layout >/dev/null 2>&1 || true
[[ -f /run/ekklesia-classroom/features.json ]]
grep -q 'PRESENTATIONS_ENABLED' /run/ekklesia-classroom/features.json
! grep -q 'WONDERINK_ENABLED' /run/ekklesia-classroom/features.json
! grep -q 'never-a-teacher' /run/ekklesia-classroom/features.json
! grep -q '10.40.0.2' /run/ekklesia-classroom/features.json
classroom-manual --features | grep -qE 'PRESENTATIONS_ENABLED +shown'
classroom-manual --features | grep -qE 'WONDERINK_ENABLED +hidden'
# The panel offers Presentations only where it is enabled, and the three
# applications that were already on the bar keep their order.
layout=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js
grep -q 'var PRESENTATIONS = true;' "$layout"
grep -q 'ekklesia-presentations.desktop' "$layout"
grep -q 'ekklesia-manual.desktop' "$layout"
python3 - "$layout" <<'ORDER_CHECK'
import re
import sys
text = open(sys.argv[1]).read()
order = re.findall(r'launchers\.push\("applications:([^"]+)"\)', text)
expected = ["ekklesia-wonderink.desktop", "google-chrome.desktop",
"ekklesia-dvd.desktop", "ekklesia-presentations.desktop"]
assert order == expected, f"the dock order changed: {order}"
ORDER_CHECK
sed -i '/^PRESENTATIONS_ENABLED=true$/d;/^WONDERINK_ENABLED=false$/d;/^MQTT_PASSWORD=never-a-teacher$/d' \
/etc/ekklesia-classroom/device.conf
# The policy module and the daemon agree on what they import, on a machine # The policy module and the daemon agree on what they import, on a machine
# rather than in a checkout. # rather than in a checkout.
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \ PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \