webuntis-api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +241 -0
- data/examples/class_teachers.rb +66 -0
- data/lib/webuntis/client.rb +540 -0
- data/lib/webuntis/cookie_jar.rb +55 -0
- data/lib/webuntis/element_type.rb +49 -0
- data/lib/webuntis/errors.rb +64 -0
- data/lib/webuntis/http/fake.rb +45 -0
- data/lib/webuntis/http/net_http.rb +93 -0
- data/lib/webuntis/http.rb +81 -0
- data/lib/webuntis/rest.rb +319 -0
- data/lib/webuntis/rpc.rb +182 -0
- data/lib/webuntis/school.rb +146 -0
- data/lib/webuntis/session.rb +33 -0
- data/lib/webuntis/totp.rb +29 -0
- data/lib/webuntis/util.rb +85 -0
- data/lib/webuntis/version.rb +5 -0
- data/lib/webuntis.rb +29 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1dcd96e7abfbd4f863336185dfbedae0aceaf43689e1ba9a8efd9f9b527fd017
|
|
4
|
+
data.tar.gz: 5bceaf819d35ade46f500e5f0dc1c4db7a07050e3fc6c6e91f1f9c4c7ae6bd98
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d24bda2f99e561945d6ab560fb6b9f3df2a16098d4b838035b2d31742e31f6f90528d3684efc14a7a6c014c3ffd22d2126ecb08efae3edcc647a05500ec33362
|
|
7
|
+
data.tar.gz: 33a36b3307c68b7969b750c5869ef75c88b75b953c4a603eddba20c4f64aab78152091563b54037145342711affedf8cb68809e1ae0e01111e91f0a326ca688a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
**Initial Release**
|
|
6
|
+
|
|
7
|
+
- REST calls re-authenticate when `api/token/new` refuses to mint a JWT and the call then
|
|
8
|
+
fails; the view API answers token-less requests with 404, which previously left the
|
|
9
|
+
client stuck on a dead session.
|
|
10
|
+
- `rest.calendar_entry_detail` sends the numeric `elementType` the endpoint expects.
|
|
11
|
+
- `rest.student_absences` defaults `student_id` to `-1` (all students); the endpoint
|
|
12
|
+
answers 500 without it.
|
|
13
|
+
- `rpc.exams` defaults `exam_type_id` to `0` (every type); `getExams` rejects the call
|
|
14
|
+
without `examTypeId`.
|
|
15
|
+
|
|
16
|
+
- `WebUntis::School.search` / `.find` for tenant discovery via `schoolquery2`.
|
|
17
|
+
- `WebUntis::Client` with four login flows (`:rpc`, `:form`, `:secret`, `:anonymous`),
|
|
18
|
+
a cookie jar, lazy JWT minting from `api/token/new`, tenant-id resolution and
|
|
19
|
+
re-authenticate-once-and-retry on expired sessions.
|
|
20
|
+
- `client.rpc`: the documented JSON-RPC API with snake_case parameters.
|
|
21
|
+
- `client.rest`: the modern `api/rest/view/v1|v2|v3|v4` endpoints the web app uses,
|
|
22
|
+
plus the older cookie-only `api/...` endpoints; `timetable_entries` reads the tenant's
|
|
23
|
+
default format from `timetable/grid`.
|
|
24
|
+
- `client.class_teachers` from `timetable/filter` (default) or by joining `getKlassen`
|
|
25
|
+
to `getTeachers` (`source: :rpc`).
|
|
26
|
+
- `WebUntis::HTTP::NetHttp` and an injectable `WebUntis::HTTP::Fake` adapter.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Meow the Cat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# webuntis-api
|
|
2
|
+
|
|
3
|
+
A dependency-free Ruby client for **WebUntis**, covering both API surfaces a school
|
|
4
|
+
account can reach:
|
|
5
|
+
|
|
6
|
+
- the documented **JSON-RPC** API at `/WebUntis/jsonrpc.do`, and
|
|
7
|
+
- the **modern REST** API at `/WebUntis/api/rest/view/...` that the WebUntis web app
|
|
8
|
+
itself calls (plus the older cookie-only `/WebUntis/api/...` endpoints).
|
|
9
|
+
|
|
10
|
+
Zero runtime dependencies: only `net/http`, `uri`, `json`, `securerandom`, `date`,
|
|
11
|
+
`time` and `openssl` from the standard library. Ruby >= 3.2.
|
|
12
|
+
|
|
13
|
+
This is the successor to the author's older `untis` gem (`UntisWorker`, JSON-RPC only,
|
|
14
|
+
raw hashes and error hashes). The big behavioural change: **this gem raises instead of
|
|
15
|
+
returning error hashes** - see [Errors](#errors).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "webuntis-api"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
gem install webuntis-api
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "webuntis"
|
|
31
|
+
|
|
32
|
+
client = WebUntis::Client.new(
|
|
33
|
+
school: "heidelberg-college", # login name (preferred) or display name
|
|
34
|
+
username: ENV.fetch("WEBUNTIS_USER"),
|
|
35
|
+
password: ENV.fetch("WEBUNTIS_PASSWORD")
|
|
36
|
+
# server: "heidelberg-college.webuntis.com" # skip the discovery round-trip
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
client.rpc.classes.each { |klasse| puts klasse["name"] }
|
|
40
|
+
client.logout!
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The client logs in lazily on the first call that needs a session, so `login!` is
|
|
44
|
+
optional. `client.session` exposes `session_id`, `person_id`, `person_type`,
|
|
45
|
+
`klasse_id`, `tenant_id`, `token`, `token_expires_at` and `school_year_id`.
|
|
46
|
+
|
|
47
|
+
A client is not thread-safe: a re-login swaps the session and cookie jar underneath
|
|
48
|
+
concurrent calls. Use one client per thread.
|
|
49
|
+
|
|
50
|
+
### Finding a school
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
WebUntis::School.search("Heidelberg")
|
|
54
|
+
# => [#<WebUntis::School "Heidelberg College" login_name="heidelberg-college" ...>]
|
|
55
|
+
|
|
56
|
+
school = WebUntis::School.find("Heidelberg College")
|
|
57
|
+
school.login_name # => "heidelberg-college"
|
|
58
|
+
school.server # => "heidelberg-college.webuntis.com"
|
|
59
|
+
school.tenant_id # => "4239400"
|
|
60
|
+
school.base_url # => "https://heidelberg-college.webuntis.com/WebUntis"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`find` raises `WebUntis::DiscoveryError` when nothing or more than one tenant matches;
|
|
64
|
+
an exact case-insensitive hit on the display name, login name or server wins over
|
|
65
|
+
partial matches.
|
|
66
|
+
|
|
67
|
+
### Login flows
|
|
68
|
+
|
|
69
|
+
| `login:` | Credentials | Endpoint |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `:rpc` (default) | `username:` + `password:` | `jsonrpc.do` `authenticate` |
|
|
72
|
+
| `:form` | `username:` + `password:` | `j_spring_security_check`, verified by `api/token/new` |
|
|
73
|
+
| `:secret` | `username:` + `secret:` | TOTP via `jsonrpc_intern.do?m=getUserData2017` |
|
|
74
|
+
| `:anonymous` | `anonymous: true` | the same OTP flow with user `#anonymous#` |
|
|
75
|
+
|
|
76
|
+
The mode is derived from the credentials you pass, so `secret:` implies `:secret` and
|
|
77
|
+
`anonymous: true` implies `:anonymous`; pass `login:` to override. The constructor
|
|
78
|
+
raises `ArgumentError` when the mode is unknown or its credentials are missing.
|
|
79
|
+
|
|
80
|
+
## The modern REST layer
|
|
81
|
+
|
|
82
|
+
`client.rest` sends `Authorization: Bearer <jwt>` (minted lazily from `api/token/new`
|
|
83
|
+
and refreshed 30 s before the JWT's `exp`), `Tenant-Id`, the session cookies and
|
|
84
|
+
`Accept: application/json, text/plain, */*`. Results are returned exactly as the API
|
|
85
|
+
sends them (Hashes/Arrays with String keys).
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# 1. What the SPA loads first: tenant, permissions, current school year.
|
|
89
|
+
data = client.rest.app_data
|
|
90
|
+
data.dig("currentSchoolYear", "id") # => 17
|
|
91
|
+
|
|
92
|
+
# 2. One class's timetable for a week.
|
|
93
|
+
entries = client.rest.timetable_entries(
|
|
94
|
+
resource_type: :class, # "CLASS" / :class / WebUntis::ElementType::CLASS
|
|
95
|
+
resources: [470],
|
|
96
|
+
start: Date.new(2026, 3, 23),
|
|
97
|
+
end: Date.new(2026, 3, 27)
|
|
98
|
+
)
|
|
99
|
+
# GET api/rest/view/v1/timetable/entries?start=2026-03-23&end=2026-03-27&format=3
|
|
100
|
+
# &resourceType=CLASS&resources=470&periodTypes=&timetableType=STANDARD&layout=START_TIME
|
|
101
|
+
# `format` defaults to the tenant's format for that resource type (`classFormat` etc.),
|
|
102
|
+
# read once from api/rest/view/v1/timetable/grid; pass `format:` to override.
|
|
103
|
+
|
|
104
|
+
# 3. The inbox.
|
|
105
|
+
client.rest.messages["incomingMessages"]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Anything not wrapped is one line away:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
client.rest.get("api/rest/view/v1/timetable/entries/settings", { "format" => 2, "resourceType" => "CLASS" })
|
|
112
|
+
client.rest.post("api/rest/view/v1/classreg/homework/list", { "dateRange" => { "start" => "2026-03-23", "end" => "2026-03-27" } })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Query values are encoded for you: Arrays join with `,`, `Date`/`Time` become
|
|
116
|
+
`YYYY-MM-DD`, booleans become `true`/`false`, and `nil` values are dropped.
|
|
117
|
+
|
|
118
|
+
Some endpoints are scoped to a school year; wrap those calls to send
|
|
119
|
+
`X-Webuntis-Api-School-Year-Id`:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
client.with_school_year(16) { |scoped| scoped.rest.timetable_grid }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## The JSON-RPC layer
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
client.rpc.teachers
|
|
129
|
+
client.rpc.classes(school_year_id: 17)
|
|
130
|
+
client.rpc.substitutions(start_date: Date.new(2026, 3, 23), end_date: Date.new(2026, 3, 27))
|
|
131
|
+
|
|
132
|
+
client.rpc.timetable(
|
|
133
|
+
element_id: 470, element_type: :class,
|
|
134
|
+
start_date: Date.new(2026, 3, 23), end_date: Date.new(2026, 3, 27),
|
|
135
|
+
show_subst_text: true, teacher_fields: %w[id name longname]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
client.rpc.call("getAnythingElse", { "someParam" => 1 }) # generic escape hatch
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Parameters are written in snake_case and translated to the API's names
|
|
142
|
+
(`school_year_id` -> `schoolyearId`, `surname` -> `sn`); raw camelCase keys are accepted
|
|
143
|
+
too, and `timetable` raises `ArgumentError` for an option it does not know. Dates accept
|
|
144
|
+
`Date`/`Time`/`DateTime`, `"YYYYMMDD"` / `"YYYY-MM-DD"` Strings or Integers and are
|
|
145
|
+
sent as `YYYYMMDD` Integers; `nil` parameters are dropped. Element types accept
|
|
146
|
+
`:teacher`, `"TEACHER"` or `WebUntis::ElementType::TEACHER`.
|
|
147
|
+
|
|
148
|
+
Results are **not** transformed. Two helpers are there when you want to read them:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
WebUntis::Util.parse_date(20_260_916) # => #<Date 2026-09-16>
|
|
152
|
+
WebUntis::Util.parse_time(915) # => [9, 15]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Class teachers (Klassenlehrer)
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
client.class_teachers
|
|
159
|
+
# => [{ "class" => { "id" => 481, "name" => "10/1", "long_name" => "10/1" },
|
|
160
|
+
# "teachers" => [{ "id" => 187, "name" => "HOF", "long_name" => "Hoffmann", "fore_name" => nil }] }, ...]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Both sources return that same shape:
|
|
164
|
+
|
|
165
|
+
- `source: :rest` (default) reads `api/rest/view/v1/timetable/filter?resourceType=CLASS`,
|
|
166
|
+
whose class rows carry `classTeacher1`/`classTeacher2`. The filter is scoped to one day
|
|
167
|
+
(`date:`, default today). Forenames are not part of that payload, so `fore_name` is nil.
|
|
168
|
+
- `source: :rpc` joins `getKlassen`'s `teacher1`/`teacher2` against `getTeachers`
|
|
169
|
+
(teacher1 first, unknown and empty ids skipped, inactive classes filtered unless
|
|
170
|
+
`include_inactive: true`, `school_year_id:` forwarded) and raises `WebUntis::Error`
|
|
171
|
+
when the account cannot see those fields at all. Anonymous sessions get 401 from the
|
|
172
|
+
view API, so this is their only option, and it works only where the tenant grants
|
|
173
|
+
public access to `getTeachers`.
|
|
174
|
+
|
|
175
|
+
`examples/class_teachers.rb` prints them one class per line:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
WEBUNTIS_SCHOOL=heidelberg-college WEBUNTIS_USER=... WEBUNTIS_PASSWORD=... \
|
|
179
|
+
ruby examples/class_teachers.rb
|
|
180
|
+
# 10/1 Hoffmann (HOF)
|
|
181
|
+
|
|
182
|
+
ruby examples/class_teachers.rb --source rpc # Hoffmann Luca (HOF)
|
|
183
|
+
ruby examples/class_teachers.rb --settings config/settings.local.yml --json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`--settings` reads a YAML file with an `untis:` key holding `school_name`, `login` and
|
|
187
|
+
`password`.
|
|
188
|
+
|
|
189
|
+
## Errors
|
|
190
|
+
|
|
191
|
+
Everything descends from `WebUntis::Error`:
|
|
192
|
+
|
|
193
|
+
| Class | Raised when |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `DiscoveryError` | no school, or more than one, matched the query |
|
|
196
|
+
| `AuthError` | login failed, or a JWT could not be minted (`stage`, `status`) |
|
|
197
|
+
| `RpcError` | a JSON-RPC response carried an `error` member (`code`, `data`, `method`) |
|
|
198
|
+
| `HttpError` | an unhandled non-2xx response (`status`, `body`, `method`, `path`) |
|
|
199
|
+
| `ApiError` | a 2xx REST body carried `data.error.data.messageKey` or `errorMessage` |
|
|
200
|
+
|
|
201
|
+
Expired sessions are handled before they reach you: a JSON-RPC `-8520`
|
|
202
|
+
("not authenticated"), a REST 401/403, a REST redirect to `index.do`, or a failed REST
|
|
203
|
+
call for which `api/token/new` refused to mint a JWT makes the client log in again and
|
|
204
|
+
retry the call **once**.
|
|
205
|
+
|
|
206
|
+
## Testing against a fake
|
|
207
|
+
|
|
208
|
+
The HTTP layer is injectable. Any object answering
|
|
209
|
+
`call(method, url, headers:, body:) -> WebUntis::HTTP::Response` will do;
|
|
210
|
+
`WebUntis::HTTP::Fake` is included:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
fake = WebUntis::HTTP::Fake.new
|
|
214
|
+
fake.stub_json({ "jsonrpc" => "2.0", "result" => { "sessionId" => "SID" } })
|
|
215
|
+
fake.stub_json({ "jsonrpc" => "2.0", "result" => [{ "id" => 1, "name" => "5a" }] })
|
|
216
|
+
|
|
217
|
+
client = WebUntis::Client.new(school: "demo", server: "demo.webuntis.com",
|
|
218
|
+
username: "u", password: "p", http: fake)
|
|
219
|
+
client.rpc.classes
|
|
220
|
+
fake.requests.last.json # => {"id"=>"...", "method"=>"getKlassen", ...}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
A block form answers per request: `WebUntis::HTTP::Fake.new { |request| ... }`.
|
|
224
|
+
|
|
225
|
+
## Which API is this?
|
|
226
|
+
|
|
227
|
+
- The **JSON-RPC** API is the only officially documented one. Untis publishes it for
|
|
228
|
+
schools and it changes rarely.
|
|
229
|
+
- The `/WebUntis/api/rest/view/...` endpoints are the **web app's internal API**.
|
|
230
|
+
They are not documented, differ between tenants and roles, and may change without
|
|
231
|
+
notice - pin your gem version and expect to adapt.
|
|
232
|
+
- The official partner **Untis Platform API** (`api.webuntis.com`, OAuth client
|
|
233
|
+
credentials, requires an integration partnership) is a different product and is
|
|
234
|
+
**out of scope** for this gem.
|
|
235
|
+
|
|
236
|
+
Untis asks that applications log out as soon as they are done; server sessions are
|
|
237
|
+
otherwise dropped after roughly ten minutes of idling.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "yaml"
|
|
7
|
+
require_relative "../lib/webuntis"
|
|
8
|
+
|
|
9
|
+
options = { json: false, settings: nil, source: :rest }
|
|
10
|
+
OptionParser.new do |parser|
|
|
11
|
+
parser.banner = "Usage: class_teachers.rb [options]"
|
|
12
|
+
parser.on("--settings PATH", "YAML file with an `untis:` key (school_name, login, password)") do |path|
|
|
13
|
+
options[:settings] = path
|
|
14
|
+
end
|
|
15
|
+
parser.on("--source SOURCE", %w[rest rpc],
|
|
16
|
+
"rest (default): timetable/filter classTeacher1/2; rpc: getKlassen joined with getTeachers") do |s|
|
|
17
|
+
options[:source] = s.to_sym
|
|
18
|
+
end
|
|
19
|
+
parser.on("--json", "Print the raw class_teachers result as JSON") { options[:json] = true }
|
|
20
|
+
parser.on("-h", "--help") do
|
|
21
|
+
puts parser
|
|
22
|
+
exit 0
|
|
23
|
+
end
|
|
24
|
+
end.parse!
|
|
25
|
+
|
|
26
|
+
settings = options[:settings] ? (YAML.safe_load_file(options[:settings])["untis"] || {}) : {}
|
|
27
|
+
|
|
28
|
+
school = settings["school_name"] || ENV["WEBUNTIS_SCHOOL"] || "heidelberg-college"
|
|
29
|
+
username = settings["login"] || ENV.fetch("WEBUNTIS_USER", nil)
|
|
30
|
+
password = settings["password"] || ENV.fetch("WEBUNTIS_PASSWORD", nil)
|
|
31
|
+
server = ENV.fetch("WEBUNTIS_SERVER", nil)
|
|
32
|
+
|
|
33
|
+
abort("set WEBUNTIS_USER and WEBUNTIS_PASSWORD, or pass --settings PATH") if username.nil? || password.nil?
|
|
34
|
+
|
|
35
|
+
def teacher_label(teacher)
|
|
36
|
+
name = [teacher["long_name"], teacher["fore_name"]].compact.reject(&:empty?).join(" ")
|
|
37
|
+
short = teacher["name"]
|
|
38
|
+
short.to_s.empty? ? name : "#{name} (#{short})"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
# A settings file may hold the display name ("Heidelberg College"); the API wants the login name.
|
|
43
|
+
if school.match?(/\s/)
|
|
44
|
+
resolved = WebUntis::School.find(school)
|
|
45
|
+
warn("resolved #{school.inspect} to #{resolved.login_name} on #{resolved.server}")
|
|
46
|
+
school = resolved.login_name
|
|
47
|
+
server ||= resolved.server
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
client = WebUntis::Client.new(school: school, server: server, username: username, password: password)
|
|
51
|
+
rows = client.class_teachers(source: options[:source])
|
|
52
|
+
|
|
53
|
+
if options[:json]
|
|
54
|
+
puts JSON.pretty_generate(rows)
|
|
55
|
+
else
|
|
56
|
+
rows.sort_by { |row| row.dig("class", "name").to_s }.each do |row|
|
|
57
|
+
labels = row["teachers"].map { |teacher| teacher_label(teacher) }
|
|
58
|
+
puts "#{row.dig("class", "name")} #{labels.empty? ? "-" : labels.join(", ")}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
rescue WebUntis::Error => e
|
|
62
|
+
warn(e.message)
|
|
63
|
+
exit 1
|
|
64
|
+
ensure
|
|
65
|
+
client&.logout!
|
|
66
|
+
end
|