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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebUntis
|
|
4
|
+
module HTTP
|
|
5
|
+
# A test adapter: replays queued stubs (or a block) and records every request in {#requests}.
|
|
6
|
+
class Fake
|
|
7
|
+
attr_reader :requests, :stubs
|
|
8
|
+
|
|
9
|
+
def initialize(stubs = [], &block)
|
|
10
|
+
@stubs = Array(stubs)
|
|
11
|
+
@block = block
|
|
12
|
+
@requests = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Queues one more stub: a {Response}, or a callable taking the {Request}.
|
|
16
|
+
def stub(response = nil, status: 200, body: "", headers: {}, &block)
|
|
17
|
+
@stubs << (response || block || Response.new(status: status, body: body, headers: headers))
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Queues a JSON response stub.
|
|
22
|
+
def stub_json(payload, status: 200, headers: {})
|
|
23
|
+
stub(Response.new(status: status, body: JSON.generate(payload),
|
|
24
|
+
headers: { "content-type" => "application/json" }.merge(headers)))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Records the request and answers it from the block or the next queued stub.
|
|
28
|
+
def call(method, url, headers: {}, body: nil)
|
|
29
|
+
request = Request.new(method: method, url: url, headers: headers, body: body)
|
|
30
|
+
@requests << request
|
|
31
|
+
return @block.call(request) if @block
|
|
32
|
+
|
|
33
|
+
stub = @stubs.shift
|
|
34
|
+
raise Error, "WebUntis::HTTP::Fake ran out of stubs for #{method.to_s.upcase} #{url}" if stub.nil?
|
|
35
|
+
|
|
36
|
+
stub.respond_to?(:call) ? stub.call(request) : stub
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The last recorded request.
|
|
40
|
+
def last_request
|
|
41
|
+
@requests.last
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module WebUntis
|
|
7
|
+
module HTTP
|
|
8
|
+
# The default adapter: one persistent `Net::HTTP` connection per host, TLS on, no redirect following.
|
|
9
|
+
class NetHttp
|
|
10
|
+
REQUEST_CLASSES = {
|
|
11
|
+
get: Net::HTTP::Get,
|
|
12
|
+
post: Net::HTTP::Post,
|
|
13
|
+
put: Net::HTTP::Put,
|
|
14
|
+
delete: Net::HTTP::Delete
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(open_timeout: 10, read_timeout: 30)
|
|
18
|
+
@open_timeout = open_timeout
|
|
19
|
+
@read_timeout = read_timeout
|
|
20
|
+
@connections = {}
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Performs one request and returns a {Response}; 3xx responses are returned, never followed.
|
|
25
|
+
def call(method, url, headers: {}, body: nil)
|
|
26
|
+
uri = URI.parse(url)
|
|
27
|
+
request = build_request(method, uri, headers, body)
|
|
28
|
+
response = @mutex.synchronize { perform(uri, request) }
|
|
29
|
+
Response.new(status: response.code, body: response.body.to_s, headers: response_headers(response))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Closes every pooled connection.
|
|
33
|
+
def close
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
@connections.each_value { |connection| connection.finish if connection.started? }
|
|
36
|
+
@connections.clear
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# A pooled connection the server closed in the meantime fails once; reconnect and retry.
|
|
43
|
+
def perform(uri, request, retried: false)
|
|
44
|
+
connection_for(uri).request(request)
|
|
45
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
|
46
|
+
raise if retried
|
|
47
|
+
|
|
48
|
+
drop_connection(uri)
|
|
49
|
+
perform(uri, request, retried: true)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def drop_connection(uri)
|
|
53
|
+
connection = @connections.delete(connection_key(uri))
|
|
54
|
+
connection.finish if connection&.started?
|
|
55
|
+
rescue IOError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def connection_key(uri)
|
|
60
|
+
[uri.scheme, uri.host, uri.port]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_request(method, uri, headers, body)
|
|
64
|
+
klass = REQUEST_CLASSES.fetch(method.to_s.downcase.to_sym) { raise Error, "unsupported method #{method}" }
|
|
65
|
+
request = klass.new(uri.request_uri)
|
|
66
|
+
headers.each { |key, value| request[key.to_s] = value.to_s unless value.nil? }
|
|
67
|
+
request.body = body if body
|
|
68
|
+
request
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def connection_for(uri)
|
|
72
|
+
key = connection_key(uri)
|
|
73
|
+
connection = @connections[key]
|
|
74
|
+
return connection if connection&.started?
|
|
75
|
+
|
|
76
|
+
connection = Net::HTTP.new(uri.host, uri.port)
|
|
77
|
+
connection.use_ssl = uri.scheme == "https"
|
|
78
|
+
connection.open_timeout = @open_timeout
|
|
79
|
+
connection.read_timeout = @read_timeout
|
|
80
|
+
connection.start
|
|
81
|
+
@connections[key] = connection
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def response_headers(response)
|
|
85
|
+
response.each_header.with_object({}) do |(key, _), memo|
|
|
86
|
+
name = key.downcase
|
|
87
|
+
fields = response.get_fields(key) || []
|
|
88
|
+
memo[name] = name == "set-cookie" ? fields : fields.join(", ")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module WebUntis
|
|
7
|
+
# Transport plumbing: an adapter is anything answering
|
|
8
|
+
# `call(method, url, headers:, body:)` with a {Response}.
|
|
9
|
+
module HTTP
|
|
10
|
+
# One HTTP response, as every adapter must return it.
|
|
11
|
+
class Response
|
|
12
|
+
attr_reader :status, :headers, :body
|
|
13
|
+
|
|
14
|
+
def initialize(status:, body: "", headers: {})
|
|
15
|
+
@status = status.to_i
|
|
16
|
+
@body = body.to_s
|
|
17
|
+
@headers = headers.each_with_object({}) { |(key, value), memo| memo[key.to_s.downcase] = value }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# All `Set-Cookie` values of this response.
|
|
21
|
+
def set_cookies
|
|
22
|
+
Array(@headers["set-cookie"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The `Location` header, if any.
|
|
26
|
+
def location
|
|
27
|
+
value = @headers["location"]
|
|
28
|
+
value.is_a?(Array) ? value.first : value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# True for 2xx responses.
|
|
32
|
+
def success?
|
|
33
|
+
(200..299).cover?(@status)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# True for 3xx responses.
|
|
37
|
+
def redirect?
|
|
38
|
+
(300..399).cover?(@status)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The response body parsed as JSON, or the raw String when it is not JSON.
|
|
42
|
+
def json
|
|
43
|
+
return @body if @body.strip.empty?
|
|
44
|
+
|
|
45
|
+
JSON.parse(@body)
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
@body
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# One recorded request, as {Fake} exposes it.
|
|
52
|
+
class Request
|
|
53
|
+
attr_reader :method, :url, :headers, :body
|
|
54
|
+
|
|
55
|
+
def initialize(method:, url:, headers: {}, body: nil)
|
|
56
|
+
@method = method
|
|
57
|
+
@url = url
|
|
58
|
+
@headers = headers
|
|
59
|
+
@body = body
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The query string of the request URL as a Hash of String pairs.
|
|
63
|
+
def query
|
|
64
|
+
URI.decode_www_form(URI.parse(@url).query.to_s).to_h
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The request body parsed as JSON.
|
|
68
|
+
def json
|
|
69
|
+
JSON.parse(@body.to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The path of the request URL.
|
|
73
|
+
def path
|
|
74
|
+
URI.parse(@url).path
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
require_relative "http/net_http"
|
|
81
|
+
require_relative "http/fake"
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module WebUntis
|
|
8
|
+
# The modern REST API the WebUntis web app uses (`/WebUntis/api/rest/view/...`)
|
|
9
|
+
# plus the older `/WebUntis/api/...` endpoints that only need session cookies.
|
|
10
|
+
#
|
|
11
|
+
# Paths are relative to `<base>/`; every method returns parsed JSON with String keys.
|
|
12
|
+
class REST
|
|
13
|
+
VIEW = "api/rest/view"
|
|
14
|
+
RANGE_KEYS = %i[start end start_date end_date].freeze
|
|
15
|
+
FORMAT_KEYS = {
|
|
16
|
+
"CLASS" => "classFormat",
|
|
17
|
+
"TEACHER" => "teacherFormat",
|
|
18
|
+
"SUBJECT" => "subjectFormat",
|
|
19
|
+
"ROOM" => "roomFormat",
|
|
20
|
+
"STUDENT" => "studentFormat"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Builds a query string: arrays join with `,`, dates become `YYYY-MM-DD`, nils are dropped.
|
|
25
|
+
def encode_query(query)
|
|
26
|
+
pairs = (query || {}).filter_map do |key, value|
|
|
27
|
+
next if value.nil?
|
|
28
|
+
|
|
29
|
+
[key.to_s, format_value(value)]
|
|
30
|
+
end
|
|
31
|
+
URI.encode_www_form(pairs)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Formats one query value.
|
|
35
|
+
def format_value(value)
|
|
36
|
+
case value
|
|
37
|
+
when Array then value.map { |entry| format_value(entry) }.join(",")
|
|
38
|
+
when Date, Time then value.strftime("%Y-%m-%d")
|
|
39
|
+
else value.to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(client)
|
|
45
|
+
@client = client
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# GET any path relative to `<base>/`.
|
|
49
|
+
def get(path, query = {})
|
|
50
|
+
@client.rest_request(:get, path, query: query)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# POST any path relative to `<base>/`, with a JSON body.
|
|
54
|
+
def post(path, body = nil, query: {})
|
|
55
|
+
@client.rest_request(:post, path, query: query, body: body)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# PUT any path relative to `<base>/`, with a JSON body.
|
|
59
|
+
def put(path, body = nil, query: {})
|
|
60
|
+
@client.rest_request(:put, path, query: query, body: body)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# DELETE any path relative to `<base>/`.
|
|
64
|
+
def delete(path, query = {})
|
|
65
|
+
@client.rest_request(:delete, path, query: query)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# GET api/rest/view/v1/app/data
|
|
69
|
+
def app_data = get("#{VIEW}/v1/app/data")
|
|
70
|
+
|
|
71
|
+
# GET api/rest/view/v2/home
|
|
72
|
+
def home = get("#{VIEW}/v2/home")
|
|
73
|
+
|
|
74
|
+
# GET api/rest/view/v3/mobile/data
|
|
75
|
+
def mobile_data = get("#{VIEW}/v3/mobile/data")
|
|
76
|
+
|
|
77
|
+
# GET api/rest/view/v1/schoolyears
|
|
78
|
+
def school_years = get("#{VIEW}/v1/schoolyears")
|
|
79
|
+
|
|
80
|
+
# POST api/rest/view/v1/session/status
|
|
81
|
+
def session_status(client_time_zone: nil)
|
|
82
|
+
post("#{VIEW}/v1/session/status", { "clientTimeZone" => client_time_zone }.compact)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# GET api/rest/view/v1/today/meta
|
|
86
|
+
def today_meta = get("#{VIEW}/v1/today/meta")
|
|
87
|
+
|
|
88
|
+
# GET api/rest/view/v1/dashboard/cards
|
|
89
|
+
def dashboard_cards = get("#{VIEW}/v1/dashboard/cards")
|
|
90
|
+
|
|
91
|
+
# GET api/rest/view/v1/messages/status
|
|
92
|
+
def messages_status = get("#{VIEW}/v1/messages/status")
|
|
93
|
+
|
|
94
|
+
# GET api/rest/view/v1/timegrid
|
|
95
|
+
def timegrid = get("#{VIEW}/v1/timegrid")
|
|
96
|
+
|
|
97
|
+
# GET api/rest/view/v1/timetable/grid
|
|
98
|
+
def timetable_grid(timetable_type: "STANDARD")
|
|
99
|
+
get("#{VIEW}/v1/timetable/grid", { "timetableType" => timetable_type })
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# GET api/rest/view/v1/timetable/filter
|
|
103
|
+
def timetable_filter(resource_type:, timetable_type: "STANDARD", **range)
|
|
104
|
+
start_date, end_date = bounds(range)
|
|
105
|
+
get("#{VIEW}/v1/timetable/filter", {
|
|
106
|
+
"resourceType" => ElementType.label(resource_type),
|
|
107
|
+
"timetableType" => timetable_type,
|
|
108
|
+
"start" => start_date,
|
|
109
|
+
"end" => end_date
|
|
110
|
+
})
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# GET api/rest/view/v1/timetable/entries; `format` defaults to {#default_format}.
|
|
114
|
+
def timetable_entries(resource_type:, resources:, format: nil, period_types: "",
|
|
115
|
+
timetable_type: "STANDARD", layout: "START_TIME", **range)
|
|
116
|
+
start_date, end_date = bounds(range)
|
|
117
|
+
get("#{VIEW}/v1/timetable/entries", {
|
|
118
|
+
"start" => start_date,
|
|
119
|
+
"end" => end_date,
|
|
120
|
+
"format" => format || default_format(resource_type),
|
|
121
|
+
"resourceType" => ElementType.label(resource_type),
|
|
122
|
+
"resources" => Array(resources),
|
|
123
|
+
"periodTypes" => period_types,
|
|
124
|
+
"timetableType" => timetable_type,
|
|
125
|
+
"layout" => layout
|
|
126
|
+
})
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The tenant's timetable format id for a resource type, read once from `timetable/grid`.
|
|
130
|
+
def default_format(resource_type)
|
|
131
|
+
label = ElementType.label(resource_type)
|
|
132
|
+
@default_formats ||= {}
|
|
133
|
+
@default_formats[label] ||= timetable_grid[FORMAT_KEYS.fetch(label)] ||
|
|
134
|
+
raise(Error, "timetable/grid did not report a format for #{label}")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# GET api/rest/view/v1/timetable/entriesWeekOverview
|
|
138
|
+
def timetable_week_overview(resource_type:, resources:, timetable_type: nil, **range)
|
|
139
|
+
start_date, end_date = bounds(range)
|
|
140
|
+
get("#{VIEW}/v1/timetable/entriesWeekOverview", {
|
|
141
|
+
"start" => start_date,
|
|
142
|
+
"end" => end_date,
|
|
143
|
+
"resourceType" => ElementType.label(resource_type),
|
|
144
|
+
"resources" => Array(resources),
|
|
145
|
+
"timetableType" => timetable_type
|
|
146
|
+
})
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# GET api/rest/view/v1/timetable/search
|
|
150
|
+
def timetable_search(query:, school_year_id:)
|
|
151
|
+
get("#{VIEW}/v1/timetable/search", { "q" => query, "schoolyear" => school_year_id })
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# GET api/rest/view/v1/timetable/menu
|
|
155
|
+
def timetable_menu = get("#{VIEW}/v1/timetable/menu")
|
|
156
|
+
|
|
157
|
+
# GET api/rest/view/v1/timetable/calendar
|
|
158
|
+
def timetable_calendar(my_timetable: false, timetable_type: "STANDARD")
|
|
159
|
+
get("#{VIEW}/v1/timetable/calendar", { "myTimetable" => my_timetable, "timetableType" => timetable_type })
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# GET api/rest/view/v2/timetable/availableRooms
|
|
163
|
+
def available_rooms(start_date_time:, end_date_time:)
|
|
164
|
+
get("#{VIEW}/v2/timetable/availableRooms", {
|
|
165
|
+
"startDateTime" => Util.to_iso_datetime(start_date_time),
|
|
166
|
+
"endDateTime" => Util.to_iso_datetime(end_date_time)
|
|
167
|
+
})
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# GET api/rest/view/v2/calendar-entry/detail; this endpoint takes the numeric element type.
|
|
171
|
+
def calendar_entry_detail(element_id:, element_type:, start_date_time:, end_date_time:, homework_option: "DUE")
|
|
172
|
+
get("#{VIEW}/v2/calendar-entry/detail", {
|
|
173
|
+
"elementId" => element_id,
|
|
174
|
+
"elementType" => ElementType.resolve(element_type),
|
|
175
|
+
"startDateTime" => Util.to_iso_datetime(start_date_time),
|
|
176
|
+
"endDateTime" => Util.to_iso_datetime(end_date_time),
|
|
177
|
+
"homeworkOption" => homework_option
|
|
178
|
+
})
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# GET api/rest/view/v1/teachers
|
|
182
|
+
def teachers(query = {}) = get("#{VIEW}/v1/teachers", query)
|
|
183
|
+
|
|
184
|
+
# GET api/rest/view/v1/students
|
|
185
|
+
def students(query = {}) = get("#{VIEW}/v1/students", query)
|
|
186
|
+
|
|
187
|
+
# GET api/rest/view/v1/subjects
|
|
188
|
+
def subjects(query = {}) = get("#{VIEW}/v1/subjects", query)
|
|
189
|
+
|
|
190
|
+
# GET api/rest/view/v1/rooms
|
|
191
|
+
def rooms(query = {}) = get("#{VIEW}/v1/rooms", query)
|
|
192
|
+
|
|
193
|
+
# GET api/rest/view/v1/students/overview
|
|
194
|
+
def students_overview(query = {}) = get("#{VIEW}/v1/students/overview", query)
|
|
195
|
+
|
|
196
|
+
# GET api/rest/view/v1/teachers/{id}
|
|
197
|
+
def teacher(id) = get("#{VIEW}/v1/teachers/#{id}")
|
|
198
|
+
|
|
199
|
+
# GET api/rest/view/v1/students/{id}
|
|
200
|
+
def student(id) = get("#{VIEW}/v1/students/#{id}")
|
|
201
|
+
|
|
202
|
+
# GET api/rest/view/v1/students/{id}/lessons
|
|
203
|
+
def student_lessons(id) = get("#{VIEW}/v1/students/#{id}/lessons")
|
|
204
|
+
|
|
205
|
+
# GET api/rest/view/v1/students/{id}/class-history
|
|
206
|
+
def student_class_history(id) = get("#{VIEW}/v1/students/#{id}/class-history")
|
|
207
|
+
|
|
208
|
+
# GET api/rest/view/v1/messages
|
|
209
|
+
def messages = get("#{VIEW}/v1/messages")
|
|
210
|
+
|
|
211
|
+
# GET api/rest/view/v1/messages/{id}
|
|
212
|
+
def message(id) = get("#{VIEW}/v1/messages/#{id}")
|
|
213
|
+
|
|
214
|
+
# GET api/rest/view/v1/messages/sent
|
|
215
|
+
def sent_messages = get("#{VIEW}/v1/messages/sent")
|
|
216
|
+
|
|
217
|
+
# GET api/rest/view/v1/messages/drafts
|
|
218
|
+
def drafts = get("#{VIEW}/v1/messages/drafts")
|
|
219
|
+
|
|
220
|
+
# GET api/rest/view/v1/messages/permissions
|
|
221
|
+
def message_permissions = get("#{VIEW}/v1/messages/permissions")
|
|
222
|
+
|
|
223
|
+
# GET api/rest/view/v1/exams
|
|
224
|
+
def exams(with_deleted: false, **range)
|
|
225
|
+
start_date, end_date = bounds(range)
|
|
226
|
+
get("#{VIEW}/v1/exams", { "start" => start_date, "end" => end_date, "withDeleted" => with_deleted })
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# GET api/rest/view/v1/exams/filter
|
|
230
|
+
def exams_filter(**range)
|
|
231
|
+
start_date, end_date = bounds(range, required: false)
|
|
232
|
+
get("#{VIEW}/v1/exams/filter", { "start" => start_date, "end" => end_date })
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# GET api/rest/view/v1/exams/statistics
|
|
236
|
+
def exam_statistics(**range)
|
|
237
|
+
start_date, end_date = bounds(range)
|
|
238
|
+
get("#{VIEW}/v1/exams/statistics", { "start" => start_date, "end" => end_date })
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# GET api/rest/view/v1/exam-types
|
|
242
|
+
def exam_types_v1 = get("#{VIEW}/v1/exam-types")
|
|
243
|
+
|
|
244
|
+
# GET api/rest/view/v1/classreg/homework/meta
|
|
245
|
+
def homework_meta = get("#{VIEW}/v1/classreg/homework/meta")
|
|
246
|
+
|
|
247
|
+
# POST api/rest/view/v1/classreg/homework/list
|
|
248
|
+
def homework_list(body) = post("#{VIEW}/v1/classreg/homework/list", body)
|
|
249
|
+
|
|
250
|
+
# GET api/rest/view/v4/classreg/absences
|
|
251
|
+
def absences(query = {}) = get("#{VIEW}/v4/classreg/absences", query)
|
|
252
|
+
|
|
253
|
+
# GET api/rest/view/v1/classreg/open-periods
|
|
254
|
+
def open_periods(query = {}) = get("#{VIEW}/v1/classreg/open-periods", query)
|
|
255
|
+
|
|
256
|
+
# GET api/public/timetable/weekly/data (legacy; ISO date)
|
|
257
|
+
def weekly_timetable(element_type:, element_id:, date:, format_id: 1)
|
|
258
|
+
get("api/public/timetable/weekly/data", {
|
|
259
|
+
"elementType" => ElementType.resolve(element_type),
|
|
260
|
+
"elementId" => element_id,
|
|
261
|
+
"date" => Util.to_iso_date(date),
|
|
262
|
+
"formatId" => format_id
|
|
263
|
+
})
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# GET api/public/news/newsWidgetData (legacy)
|
|
267
|
+
def news_widget(date:)
|
|
268
|
+
get("api/public/news/newsWidgetData", { "date" => Util.to_untis_date(date) })
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# GET api/homeworks/lessons (legacy)
|
|
272
|
+
def homeworks(start_date:, end_date:)
|
|
273
|
+
get("api/homeworks/lessons", {
|
|
274
|
+
"startDate" => Util.to_untis_date(start_date),
|
|
275
|
+
"endDate" => Util.to_untis_date(end_date)
|
|
276
|
+
})
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# GET api/exams (legacy)
|
|
280
|
+
def legacy_exams(start_date:, end_date:, klasse_id: nil, with_grades: false)
|
|
281
|
+
get("api/exams", {
|
|
282
|
+
"startDate" => Util.to_untis_date(start_date),
|
|
283
|
+
"endDate" => Util.to_untis_date(end_date),
|
|
284
|
+
"klasseId" => klasse_id,
|
|
285
|
+
"withGrades" => with_grades
|
|
286
|
+
})
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# GET api/classreg/absences/students (legacy); `-1` means every student / every excuse status.
|
|
290
|
+
def student_absences(start_date:, end_date:, student_id: -1, excuse_status_id: -1)
|
|
291
|
+
get("api/classreg/absences/students", {
|
|
292
|
+
"startDate" => Util.to_untis_date(start_date),
|
|
293
|
+
"endDate" => Util.to_untis_date(end_date),
|
|
294
|
+
"studentId" => student_id,
|
|
295
|
+
"excuseStatusId" => excuse_status_id
|
|
296
|
+
})
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# GET api/app/config (legacy)
|
|
300
|
+
def app_config = get("api/app/config")
|
|
301
|
+
|
|
302
|
+
# GET api/daytimetable/config (legacy)
|
|
303
|
+
def daytimetable_config = get("api/daytimetable/config")
|
|
304
|
+
|
|
305
|
+
private
|
|
306
|
+
|
|
307
|
+
def bounds(range, required: true)
|
|
308
|
+
unknown = range.keys - RANGE_KEYS
|
|
309
|
+
raise ArgumentError, "unknown keywords: #{unknown.join(", ")}" unless unknown.empty?
|
|
310
|
+
|
|
311
|
+
start_value = range[:start] || range[:start_date]
|
|
312
|
+
end_value = range[:end] || range[:end_date]
|
|
313
|
+
missing = { start: start_value, end: end_value }.filter_map { |name, value| name if value.nil? }
|
|
314
|
+
raise ArgumentError, "missing keywords: #{missing.join(", ")}" if required && !missing.empty?
|
|
315
|
+
|
|
316
|
+
[Util.to_iso_date(start_value), Util.to_iso_date(end_value)]
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|