chuvsu-js 5.3.0 → 5.4.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.
- package/dist/common/types.d.ts +3 -1
- package/dist/common/types.js +2 -0
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/parsers.d.ts +1 -1
- package/dist/parsers.js +1 -1
- package/dist/tt/client.d.ts +1 -1
- package/dist/tt/client.js +17 -13
- package/dist/tt/domain/index.d.ts +1 -1
- package/dist/tt/domain/index.js +1 -1
- package/dist/tt/domain/repository.d.ts +1 -1
- package/dist/tt/domain/repository.js +6 -3
- package/dist/tt/domain/schedule-snapshot.d.ts +4 -3
- package/dist/tt/domain/schedule-snapshot.js +18 -9
- package/dist/tt/domain/schedule.js +1 -0
- package/dist/tt/domain/types.d.ts +7 -6
- package/dist/tt/observations.js +3 -2
- package/dist/tt/parse/entry-parts.d.ts +1 -0
- package/dist/tt/parse/entry-parts.js +5 -0
- package/dist/tt/parse/full-schedule.js +3 -1
- package/dist/tt/parse/index.d.ts +1 -1
- package/dist/tt/parse/index.js +1 -1
- package/dist/tt/parse/lists.d.ts +2 -0
- package/dist/tt/parse/lists.js +17 -2
- package/dist/tt/types.d.ts +2 -0
- package/dist/tt/utils/index.d.ts +1 -0
- package/dist/tt/utils/index.js +1 -0
- package/dist/tt/utils/lesson-type.d.ts +17 -0
- package/dist/tt/utils/lesson-type.js +33 -0
- package/dist/tt/webinars.js +2 -1
- package/docs/v5-migration.md +9 -2
- package/package.json +1 -1
package/dist/common/types.d.ts
CHANGED
package/dist/common/types.js
CHANGED
|
@@ -4,6 +4,8 @@ export var AcademicPeriod;
|
|
|
4
4
|
AcademicPeriod[AcademicPeriod["WinterSession"] = 2] = "WinterSession";
|
|
5
5
|
AcademicPeriod[AcademicPeriod["SpringSemester"] = 3] = "SpringSemester";
|
|
6
6
|
AcademicPeriod[AcademicPeriod["SummerSession"] = 4] = "SummerSession";
|
|
7
|
+
AcademicPeriod[AcademicPeriod["StateFinalAssessment"] = 9] = "StateFinalAssessment";
|
|
8
|
+
AcademicPeriod[AcademicPeriod["Retake"] = 11] = "Retake";
|
|
7
9
|
})(AcademicPeriod || (AcademicPeriod = {}));
|
|
8
10
|
export var EducationLevel;
|
|
9
11
|
(function (EducationLevel) {
|
package/dist/core.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./tt/domain/index.js";
|
|
2
2
|
export { attachWebinars, findWebinar } from "./tt/webinars.js";
|
|
3
3
|
export type { LessonWithWebinar } from "./tt/webinars.js";
|
|
4
|
-
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, formatLocalDate, getHolidayTransfers, getSemesterStart, getSemesterWeeks, getStandardScheduleBlocks, getWeekNumber, getWeekdayName, isHoliday, isLocalDate, isSessionPeriod, parseLocalDate, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
4
|
+
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, formatLocalDate, getHolidayTransfers, getSemesterStart, getSemesterWeeks, getStandardScheduleBlocks, getWeekNumber, getWeekdayName, isHoliday, isLocalDate, isSessionPeriod, LessonType, parseLessonType, parseLocalDate, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
5
5
|
export type { Holiday, HolidayTransfer } from "./tt/utils/index.js";
|
|
6
6
|
export { AuthError, EducationLevel, ParseError, AcademicPeriod, } from "./common/types.js";
|
|
7
7
|
export type { LocalDate, Teacher, Time, TimeRange, WeekRange, } from "./common/types.js";
|
package/dist/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Runtime-safe core for Node.js, browsers, Deno, and workers.
|
|
2
2
|
export * from "./tt/domain/index.js";
|
|
3
3
|
export { attachWebinars, findWebinar } from "./tt/webinars.js";
|
|
4
|
-
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, formatLocalDate, getHolidayTransfers, getSemesterStart, getSemesterWeeks, getStandardScheduleBlocks, getWeekNumber, getWeekdayName, isHoliday, isLocalDate, isSessionPeriod, parseLocalDate, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
4
|
+
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, formatLocalDate, getHolidayTransfers, getSemesterStart, getSemesterWeeks, getStandardScheduleBlocks, getWeekNumber, getWeekdayName, isHoliday, isLocalDate, isSessionPeriod, LessonType, parseLessonType, parseLocalDate, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
5
5
|
export { AuthError, EducationLevel, ParseError, AcademicPeriod, } from "./common/types.js";
|
package/dist/parsers.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { parseAcademicYearFromPage, parseRoomButtons, parseRoomSchedule, parseRoomInfo, parseRoomName, parseFacultyButtons, parseGroupSchedule, parseGroupName, parseGroupButtons, parseGroupsString, parsePeriodFromPage, parseTeacherButtons, parseTeacherSchedule, parseTeacherInfo, parseWebinars, } from "./tt/parse/index.js";
|
|
1
|
+
export { parseAcademicYearFromPage, parseAvailablePeriodsFromPage, parseRoomButtons, parseRoomSchedule, parseRoomInfo, parseRoomName, parseFacultyButtons, parseGroupSchedule, parseGroupName, parseGroupButtons, parseGroupsString, parsePeriodFromPage, parseTeacherButtons, parseTeacherSchedule, parseTeacherInfo, parseWebinars, } from "./tt/parse/index.js";
|
|
2
2
|
export { createScheduleSourceSnapshot } from "./tt/observations.js";
|
|
3
3
|
export type { ParsedScheduleDay, ParsedLesson, ParsedScheduleBlock, } from "./tt/types.js";
|
package/dist/parsers.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { parseAcademicYearFromPage, parseRoomButtons, parseRoomSchedule, parseRoomInfo, parseRoomName, parseFacultyButtons, parseGroupSchedule, parseGroupName, parseGroupButtons, parseGroupsString, parsePeriodFromPage, parseTeacherButtons, parseTeacherSchedule, parseTeacherInfo, parseWebinars, } from "./tt/parse/index.js";
|
|
1
|
+
export { parseAcademicYearFromPage, parseAvailablePeriodsFromPage, parseRoomButtons, parseRoomSchedule, parseRoomInfo, parseRoomName, parseFacultyButtons, parseGroupSchedule, parseGroupName, parseGroupButtons, parseGroupsString, parsePeriodFromPage, parseTeacherButtons, parseTeacherSchedule, parseTeacherInfo, parseWebinars, } from "./tt/parse/index.js";
|
|
2
2
|
export { createScheduleSourceSnapshot } from "./tt/observations.js";
|
package/dist/tt/client.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare class TimetableClient {
|
|
|
13
13
|
private repositoryReady;
|
|
14
14
|
private repositoryMutation;
|
|
15
15
|
private loginMode;
|
|
16
|
-
private
|
|
16
|
+
private timetableContexts;
|
|
17
17
|
constructor(opts?: TimetableClientOptions);
|
|
18
18
|
private get pertt();
|
|
19
19
|
get repository(): TimetableRepository;
|
package/dist/tt/client.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { HttpClient, } from "../common/http.js";
|
|
2
2
|
import { HybridCache } from "../common/cache.js";
|
|
3
3
|
import { AcademicPeriod, AuthError, EducationLevel, ParseError, } from "../common/types.js";
|
|
4
|
-
import { parseAcademicYearFromPage, parseRoomButtons, parseRoomSchedule, parseRoomInfo, parseRoomName, parseGroupButtons, parseGroupName, parseFacultyButtons, parseTeacherButtons, parseGroupSchedule, parseTeacherSchedule, parseTeacherInfo, parsePeriodFromPage, parseWebinars, } from "./parse/index.js";
|
|
4
|
+
import { parseAcademicYearFromPage, parseAvailablePeriodsFromPage, parseRoomButtons, parseRoomSchedule, parseRoomInfo, parseRoomName, parseGroupButtons, parseGroupName, parseFacultyButtons, parseTeacherButtons, parseGroupSchedule, parseTeacherSchedule, parseTeacherInfo, parsePeriodFromPage, parseWebinars, } from "./parse/index.js";
|
|
5
5
|
import { Schedule } from "./domain/schedule.js";
|
|
6
6
|
import { TimetableRepository } from "./domain/repository.js";
|
|
7
7
|
import { createScheduleSourceSnapshot } from "./observations.js";
|
|
8
8
|
const BASE = "https://tt.chuvsu.ru";
|
|
9
9
|
const AUTH_URL = `${BASE}/auth`;
|
|
10
10
|
const TIMETABLE_CONTEXT_TTL_MS = 15 * 60 * 1000;
|
|
11
|
-
const
|
|
11
|
+
const SUPPORTED_PERIODS = [
|
|
12
12
|
AcademicPeriod.FallSemester,
|
|
13
13
|
AcademicPeriod.WinterSession,
|
|
14
14
|
AcademicPeriod.SpringSemester,
|
|
15
15
|
AcademicPeriod.SummerSession,
|
|
16
|
+
AcademicPeriod.StateFinalAssessment,
|
|
17
|
+
AcademicPeriod.Retake,
|
|
16
18
|
];
|
|
17
19
|
function requirePositiveId(value, label) {
|
|
18
20
|
if (!Number.isInteger(value) || value == null || value < 1) {
|
|
@@ -28,12 +30,12 @@ function normalizeSearchQuery(value, label, minimumLength = 1) {
|
|
|
28
30
|
return normalized;
|
|
29
31
|
}
|
|
30
32
|
function normalizePeriods(periods) {
|
|
31
|
-
const values = [...
|
|
33
|
+
const values = [...periods];
|
|
32
34
|
if (values.length === 0)
|
|
33
35
|
throw new RangeError("At least one period is required");
|
|
34
36
|
const unique = new Set();
|
|
35
37
|
for (const period of values) {
|
|
36
|
-
if (!
|
|
38
|
+
if (!SUPPORTED_PERIODS.includes(period)) {
|
|
37
39
|
throw new RangeError(`Invalid academic period: ${period}`);
|
|
38
40
|
}
|
|
39
41
|
if (unique.has(period)) {
|
|
@@ -74,7 +76,7 @@ export class TimetableClient {
|
|
|
74
76
|
repositoryReady = null;
|
|
75
77
|
repositoryMutation = Promise.resolve();
|
|
76
78
|
loginMode = null;
|
|
77
|
-
|
|
79
|
+
timetableContexts = new Map();
|
|
78
80
|
constructor(opts) {
|
|
79
81
|
this.educationLevel = opts?.educationLevel ?? EducationLevel.HigherEducation;
|
|
80
82
|
if (this.educationLevel !== EducationLevel.HigherEducation &&
|
|
@@ -189,7 +191,7 @@ export class TimetableClient {
|
|
|
189
191
|
throw new AuthError("TT login failed");
|
|
190
192
|
}
|
|
191
193
|
this.loginMode = { type: "credentials", ...opts };
|
|
192
|
-
this.
|
|
194
|
+
this.timetableContexts.clear();
|
|
193
195
|
}
|
|
194
196
|
async loginAsGuest() {
|
|
195
197
|
const res = await this.http.post(AUTH_URL, { guest: "Войти гостем", hfac: "0", pertt: this.pertt }, false);
|
|
@@ -197,7 +199,7 @@ export class TimetableClient {
|
|
|
197
199
|
throw new AuthError("TT guest login failed");
|
|
198
200
|
}
|
|
199
201
|
this.loginMode = { type: "guest" };
|
|
200
|
-
this.
|
|
202
|
+
this.timetableContexts.clear();
|
|
201
203
|
}
|
|
202
204
|
isSessionExpired(body) {
|
|
203
205
|
return body.includes('name="wname"');
|
|
@@ -260,10 +262,9 @@ export class TimetableClient {
|
|
|
260
262
|
}
|
|
261
263
|
/** Resolve the academic year and active period from tt.chuvsu.ru itself. */
|
|
262
264
|
async getTimetableContext(pageUrl) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return
|
|
266
|
-
}
|
|
265
|
+
const cached = this.timetableContexts.get(pageUrl);
|
|
266
|
+
if (cached && Date.now() - cached.resolvedAt < TIMETABLE_CONTEXT_TTL_MS)
|
|
267
|
+
return cached;
|
|
267
268
|
const { body } = await this.authGet(pageUrl);
|
|
268
269
|
const academicYearStartYear = parseAcademicYearFromPage(body);
|
|
269
270
|
if (academicYearStartYear == null) {
|
|
@@ -273,12 +274,14 @@ export class TimetableClient {
|
|
|
273
274
|
if (period == null) {
|
|
274
275
|
throw new ParseError("TT page does not expose the active timetable period");
|
|
275
276
|
}
|
|
277
|
+
const availablePeriods = parseAvailablePeriodsFromPage(body);
|
|
276
278
|
const context = {
|
|
277
279
|
academicYearStartYear,
|
|
278
280
|
period,
|
|
281
|
+
availablePeriods: availablePeriods.length ? availablePeriods : [period],
|
|
279
282
|
resolvedAt: Date.now(),
|
|
280
283
|
};
|
|
281
|
-
this.
|
|
284
|
+
this.timetableContexts.set(pageUrl, context);
|
|
282
285
|
return context;
|
|
283
286
|
}
|
|
284
287
|
// --- Schedule ---
|
|
@@ -359,7 +362,7 @@ export class TimetableClient {
|
|
|
359
362
|
}
|
|
360
363
|
async getSchedule(owner, options) {
|
|
361
364
|
const ownerUrl = this.ownerUrl(owner);
|
|
362
|
-
const
|
|
365
|
+
const requestedPeriods = options?.periods ? normalizePeriods(options.periods) : null;
|
|
363
366
|
await this.ensureRepository();
|
|
364
367
|
if (owner.type === "group" && owner.group.name.trim()) {
|
|
365
368
|
await this.rememberGroups([owner.group]);
|
|
@@ -372,6 +375,7 @@ export class TimetableClient {
|
|
|
372
375
|
}
|
|
373
376
|
const resolvedOwner = this.resolveOwner(owner);
|
|
374
377
|
const context = await this.getTimetableContext(ownerUrl);
|
|
378
|
+
const periods = requestedPeriods ?? context.availablePeriods;
|
|
375
379
|
const results = await Promise.all(periods.map(async (period) => ({
|
|
376
380
|
period,
|
|
377
381
|
days: await this.fetchOwnerSchedule(resolvedOwner, period, context.academicYearStartYear),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { RandomLessonIdGenerator, occurrenceIdForSeries, migrateLegacyLessonId } from "./ids.js";
|
|
2
|
-
export { ScheduleSnapshot, createScheduleSnapshot } from "./schedule-snapshot.js";
|
|
2
|
+
export { ScheduleSnapshot, createScheduleSnapshot, SCHEDULE_SNAPSHOT_SCHEMA_VERSION } from "./schedule-snapshot.js";
|
|
3
3
|
export type { SerializedScheduleSnapshot } from "./schedule-snapshot.js";
|
|
4
4
|
export { TimetableDirectory } from "./directory.js";
|
|
5
5
|
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
package/dist/tt/domain/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { RandomLessonIdGenerator, occurrenceIdForSeries, migrateLegacyLessonId } from "./ids.js";
|
|
2
|
-
export { ScheduleSnapshot, createScheduleSnapshot } from "./schedule-snapshot.js";
|
|
2
|
+
export { ScheduleSnapshot, createScheduleSnapshot, SCHEDULE_SNAPSHOT_SCHEMA_VERSION } from "./schedule-snapshot.js";
|
|
3
3
|
export { TimetableDirectory } from "./directory.js";
|
|
4
4
|
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
|
5
5
|
export { MemoryTimetableRepositoryAdapter, TimetableRepository, } from "./repository.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
2
2
|
import { TimetableDirectory } from "./directory.js";
|
|
3
3
|
import type { IngestResult, LessonIdGenerator, LessonOccurrence, LessonSeries, ScheduleOwner, ScheduleSourceSnapshot, TimetableRepositoryAdapter, TimetableRepositoryPatch, TimetableRepositorySnapshot } from "./types.js";
|
|
4
4
|
export declare class TimetableRepository {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
1
2
|
import { isLocalDate } from "../utils/date.js";
|
|
2
3
|
import { TimetableDirectory } from "./directory.js";
|
|
3
4
|
import { RandomLessonIdGenerator } from "./ids.js";
|
|
@@ -9,7 +10,7 @@ function sourceObservationKey(sourceKey, observationKey) {
|
|
|
9
10
|
return claimKey(sourceKey, observationKey);
|
|
10
11
|
}
|
|
11
12
|
function canonicalBucketKey(observation, source) {
|
|
12
|
-
const prefix = `${source.academicYearStartYear}:${source.period}:${observation.kind}:${normalizeScheduleText(observation.subject)}`;
|
|
13
|
+
const prefix = `${source.academicYearStartYear}:${source.period}:${observation.kind}:${observation.retakeAttempt ?? 0}:${normalizeScheduleText(observation.subject)}`;
|
|
13
14
|
return observation.kind === "series"
|
|
14
15
|
? `${prefix}:${observation.recurrence.weekday}:${recurrenceWeeks(observation)}`
|
|
15
16
|
: `${prefix}:${observation.date}`;
|
|
@@ -88,7 +89,7 @@ function observationScore(left, right, sameSource, directory) {
|
|
|
88
89
|
return Number.NEGATIVE_INFINITY;
|
|
89
90
|
}
|
|
90
91
|
let score = 35;
|
|
91
|
-
if (
|
|
92
|
+
if (left.type === right.type) {
|
|
92
93
|
score += 10;
|
|
93
94
|
}
|
|
94
95
|
else if (left.type && right.type) {
|
|
@@ -570,7 +571,7 @@ export class TimetableRepository {
|
|
|
570
571
|
if (!Number.isInteger(source.academicYearStartYear)) {
|
|
571
572
|
throw new Error(`Invalid academic year for source ${source.sourceKey}`);
|
|
572
573
|
}
|
|
573
|
-
if (!Number.isInteger(source.period) ||
|
|
574
|
+
if (!Number.isInteger(source.period) || !Object.values(AcademicPeriod).includes(source.period)) {
|
|
574
575
|
throw new Error(`Invalid academic period for source ${source.sourceKey}`);
|
|
575
576
|
}
|
|
576
577
|
if (!(source.observedAt instanceof Date) || !Number.isFinite(source.observedAt.getTime())) {
|
|
@@ -939,6 +940,7 @@ export class TimetableRepository {
|
|
|
939
940
|
...relations,
|
|
940
941
|
isDistance: claims.some((value) => value.observation.isDistance === true),
|
|
941
942
|
possibleChanges: claims.some((value) => value.observation.possibleChanges === true),
|
|
943
|
+
...(preferred.observation.retakeAttempt ? { retakeAttempt: preferred.observation.retakeAttempt } : {}),
|
|
942
944
|
substitutions: this.aggregateSubstitutions(claims),
|
|
943
945
|
sources: claims.map(sourceRef),
|
|
944
946
|
};
|
|
@@ -980,6 +982,7 @@ export class TimetableRepository {
|
|
|
980
982
|
...relations,
|
|
981
983
|
isDistance: claims.some((value) => value.observation.isDistance === true),
|
|
982
984
|
possibleChanges: claims.some((value) => value.observation.possibleChanges === true),
|
|
985
|
+
...(preferred.observation.retakeAttempt ? { retakeAttempt: preferred.observation.retakeAttempt } : {}),
|
|
983
986
|
status: transfer ? "moved" : "scheduled",
|
|
984
987
|
movedFrom: transfer
|
|
985
988
|
? { date: transfer.fromDate, slotNumber: transfer.fromSlot }
|
|
@@ -11,8 +11,9 @@ type SerializedSeries = Omit<LessonSeries, "sources"> & {
|
|
|
11
11
|
type SerializedOccurrence = Omit<LessonOccurrence, "sources"> & {
|
|
12
12
|
sources: SerializedSource[];
|
|
13
13
|
};
|
|
14
|
+
export declare const SCHEDULE_SNAPSHOT_SCHEMA_VERSION = 3;
|
|
14
15
|
export interface SerializedScheduleSnapshot {
|
|
15
|
-
schemaVersion:
|
|
16
|
+
schemaVersion: typeof SCHEDULE_SNAPSHOT_SCHEMA_VERSION;
|
|
16
17
|
repositoryRevision: number;
|
|
17
18
|
owner: ScheduleOwner;
|
|
18
19
|
academicYearStartYear: number;
|
|
@@ -21,8 +22,8 @@ export interface SerializedScheduleSnapshot {
|
|
|
21
22
|
end: LocalDate;
|
|
22
23
|
series: SerializedSeries[];
|
|
23
24
|
direct: SerializedOccurrence[];
|
|
24
|
-
holidays
|
|
25
|
-
holidayTransfers
|
|
25
|
+
holidays?: Holiday[];
|
|
26
|
+
holidayTransfers?: {
|
|
26
27
|
dayOff: string;
|
|
27
28
|
workDay: string | null;
|
|
28
29
|
}[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { formatLocalDate, parseLocalDate } from "../utils/index.js";
|
|
1
|
+
import { formatLocalDate, parseLocalDate, RUSSIAN_HOLIDAYS } from "../utils/index.js";
|
|
2
2
|
import { Schedule } from "./schedule.js";
|
|
3
|
+
export const SCHEDULE_SNAPSHOT_SCHEMA_VERSION = 3;
|
|
3
4
|
function serializeSources(sources, include) {
|
|
4
5
|
return include
|
|
5
6
|
? sources.map((source) => ({ ...structuredClone(source), observedAt: source.observedAt.toISOString() }))
|
|
@@ -8,6 +9,12 @@ function serializeSources(sources, include) {
|
|
|
8
9
|
function deserializeSources(sources) {
|
|
9
10
|
return sources.map((source) => ({ ...structuredClone(source), observedAt: new Date(source.observedAt) }));
|
|
10
11
|
}
|
|
12
|
+
function usesDefaultHolidays(holidays) {
|
|
13
|
+
return holidays.length === RUSSIAN_HOLIDAYS.length && holidays.every((holiday, index) => {
|
|
14
|
+
const standard = RUSSIAN_HOLIDAYS[index];
|
|
15
|
+
return holiday.month === standard.month && holiday.day === standard.day && holiday.name === standard.name;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
11
18
|
/** Browser-ready schedule backed by recurring rules and dated exceptions. */
|
|
12
19
|
export class ScheduleSnapshot extends Schedule {
|
|
13
20
|
snapshot;
|
|
@@ -15,7 +22,7 @@ export class ScheduleSnapshot extends Schedule {
|
|
|
15
22
|
snapshotDirect;
|
|
16
23
|
dateSetCache = new Map();
|
|
17
24
|
constructor(snapshot) {
|
|
18
|
-
if (snapshot.schemaVersion !==
|
|
25
|
+
if (snapshot.schemaVersion !== SCHEDULE_SNAPSHOT_SCHEMA_VERSION) {
|
|
19
26
|
throw new Error(`Unsupported schedule snapshot schema: ${snapshot.schemaVersion}`);
|
|
20
27
|
}
|
|
21
28
|
const series = snapshot.series.map((value) => ({
|
|
@@ -27,7 +34,7 @@ export class ScheduleSnapshot extends Schedule {
|
|
|
27
34
|
const options = {
|
|
28
35
|
period: snapshot.period,
|
|
29
36
|
holidays: snapshot.holidays,
|
|
30
|
-
holidayTransfers: snapshot.holidayTransfers
|
|
37
|
+
holidayTransfers: snapshot.holidayTransfers?.map((value) => ({
|
|
31
38
|
dayOff: new Date(value.dayOff),
|
|
32
39
|
workDay: value.workDay ? new Date(value.workDay) : null,
|
|
33
40
|
})),
|
|
@@ -77,7 +84,7 @@ export function createScheduleSnapshot(schedule, options) {
|
|
|
77
84
|
...value, sources: serializeSources(value.sources, includeSources),
|
|
78
85
|
}));
|
|
79
86
|
return {
|
|
80
|
-
schemaVersion:
|
|
87
|
+
schemaVersion: SCHEDULE_SNAPSHOT_SCHEMA_VERSION,
|
|
81
88
|
repositoryRevision: schedule.revision,
|
|
82
89
|
owner: structuredClone(schedule.owner),
|
|
83
90
|
academicYearStartYear: schedule.academicYearStartYear,
|
|
@@ -86,10 +93,12 @@ export function createScheduleSnapshot(schedule, options) {
|
|
|
86
93
|
end: formatLocalDate(end),
|
|
87
94
|
series,
|
|
88
95
|
direct,
|
|
89
|
-
holidays: structuredClone(schedule.holidays),
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
...(!usesDefaultHolidays(schedule.holidays) ? { holidays: structuredClone(schedule.holidays) } : {}),
|
|
97
|
+
...(schedule.holidayTransfers.length ? {
|
|
98
|
+
holidayTransfers: schedule.holidayTransfers.map((value) => ({
|
|
99
|
+
dayOff: value.dayOff.toISOString(),
|
|
100
|
+
workDay: value.workDay?.toISOString() ?? null,
|
|
101
|
+
})),
|
|
102
|
+
} : {}),
|
|
94
103
|
};
|
|
95
104
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AcademicPeriod, LocalDate, TimeRange, WeekRange } from "../../common/types.js";
|
|
2
|
+
import type { LessonType } from "../utils/lesson-type.js";
|
|
2
3
|
export type LessonSeriesId = string;
|
|
3
4
|
export type LessonId = string;
|
|
4
5
|
export interface NamedEntityRef {
|
|
@@ -62,8 +63,7 @@ export interface LessonSeries {
|
|
|
62
63
|
academicYearStartYear: number;
|
|
63
64
|
period: AcademicPeriod;
|
|
64
65
|
subject: string;
|
|
65
|
-
|
|
66
|
-
type: string;
|
|
66
|
+
type: LessonType;
|
|
67
67
|
slotNumber?: number;
|
|
68
68
|
time?: TimeRange;
|
|
69
69
|
recurrence: LessonRecurrence;
|
|
@@ -72,6 +72,7 @@ export interface LessonSeries {
|
|
|
72
72
|
rooms: RelationSet<RoomRef>;
|
|
73
73
|
isDistance: boolean;
|
|
74
74
|
possibleChanges: boolean;
|
|
75
|
+
retakeAttempt?: 1 | 2;
|
|
75
76
|
substitutions: LessonSubstitution[];
|
|
76
77
|
sources: LessonSourceRef[];
|
|
77
78
|
}
|
|
@@ -85,8 +86,7 @@ export interface LessonOccurrence {
|
|
|
85
86
|
nominalDate: LocalDate;
|
|
86
87
|
scheduledDate: LocalDate;
|
|
87
88
|
subject: string;
|
|
88
|
-
|
|
89
|
-
type: string;
|
|
89
|
+
type: LessonType;
|
|
90
90
|
slotNumber?: number;
|
|
91
91
|
time?: TimeRange;
|
|
92
92
|
groups: RelationSet<GroupAttendance>;
|
|
@@ -94,6 +94,7 @@ export interface LessonOccurrence {
|
|
|
94
94
|
rooms: RelationSet<RoomRef>;
|
|
95
95
|
isDistance: boolean;
|
|
96
96
|
possibleChanges: boolean;
|
|
97
|
+
retakeAttempt?: 1 | 2;
|
|
97
98
|
status: LessonStatus;
|
|
98
99
|
movedFrom?: {
|
|
99
100
|
date: LocalDate;
|
|
@@ -107,8 +108,7 @@ interface ObservationBase {
|
|
|
107
108
|
/** Stable only inside one source page projection. */
|
|
108
109
|
key: string;
|
|
109
110
|
subject: string;
|
|
110
|
-
|
|
111
|
-
type: string;
|
|
111
|
+
type: LessonType;
|
|
112
112
|
slotNumber?: number;
|
|
113
113
|
time?: TimeRange;
|
|
114
114
|
groups: RelationSet<GroupAttendance>;
|
|
@@ -116,6 +116,7 @@ interface ObservationBase {
|
|
|
116
116
|
rooms: RelationSet<RoomRef>;
|
|
117
117
|
isDistance?: boolean;
|
|
118
118
|
possibleChanges?: boolean;
|
|
119
|
+
retakeAttempt?: 1 | 2;
|
|
119
120
|
substitutions?: LessonSubstitution[];
|
|
120
121
|
}
|
|
121
122
|
export interface SeriesObservation extends ObservationBase {
|
package/dist/tt/observations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseLocalDate } from "./utils/index.js";
|
|
1
|
+
import { parseLessonType, parseLocalDate } from "./utils/index.js";
|
|
2
2
|
const WEEKDAY_INDEX = new Map([
|
|
3
3
|
["воскресенье", 0],
|
|
4
4
|
["понедельник", 1],
|
|
@@ -67,7 +67,7 @@ function observationBase(key, entry, block, owner) {
|
|
|
67
67
|
return {
|
|
68
68
|
key,
|
|
69
69
|
subject: entry.subject,
|
|
70
|
-
type: entry.type,
|
|
70
|
+
type: parseLessonType(entry.type),
|
|
71
71
|
slotNumber: block.slotNumber,
|
|
72
72
|
time: block.time,
|
|
73
73
|
groups: groupsFor(entry, owner),
|
|
@@ -75,6 +75,7 @@ function observationBase(key, entry, block, owner) {
|
|
|
75
75
|
rooms: roomsFor(entry, owner),
|
|
76
76
|
isDistance: entry.isDistance,
|
|
77
77
|
possibleChanges: entry.possibleChanges,
|
|
78
|
+
...(entry.retakeAttempt ? { retakeAttempt: entry.retakeAttempt } : {}),
|
|
78
79
|
substitutions: substitutionsFor(entry.substitutions),
|
|
79
80
|
};
|
|
80
81
|
}
|
|
@@ -5,6 +5,11 @@ const GROUP_TOKEN_RE = /^[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+$/u;
|
|
|
5
5
|
const LEADING_PARENTHESIZED_RE = /^\s*\(([^()]*)\)/u;
|
|
6
6
|
const WEEK_METADATA_RE = /(?:^|\s)нед\.?(?:\s|$)/iu;
|
|
7
7
|
const SUBGROUP_METADATA_RE = /^\d+\s*подгруппа$/iu;
|
|
8
|
+
const RETAKE_RE = /(?:^|\s)([12])\s*ППА(?:\s|$)/iu;
|
|
9
|
+
export function parseRetakeAttempt(value) {
|
|
10
|
+
const match = value.match(RETAKE_RE);
|
|
11
|
+
return match ? Number(match[1]) : undefined;
|
|
12
|
+
}
|
|
8
13
|
export function entryHtmlLines(html) {
|
|
9
14
|
return html.split(BR_RE).map((line) => line.trim());
|
|
10
15
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseHtml, parseTeacher, parseTime, parseWeekParity, parseWeeks, text, } from "../../common/parse.js";
|
|
2
2
|
import { parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
3
3
|
import { SUBGROUP_RE, WEEKS_RE } from "./patterns.js";
|
|
4
|
-
import { hasDistanceMarker, containsGroupCode, linesAfterSubject, parseLessonTypeAfterSubject, parseEntryRoom, stripDistanceMarker, } from "./entry-parts.js";
|
|
4
|
+
import { hasDistanceMarker, containsGroupCode, linesAfterSubject, parseLessonTypeAfterSubject, parseEntryRoom, parseRetakeAttempt, stripDistanceMarker, stripHtml, } from "./entry-parts.js";
|
|
5
5
|
export function parseGroupSchedule(html) {
|
|
6
6
|
const doc = parseHtml(html);
|
|
7
7
|
// Session layout has date-based cells with ids like "trd20251224"
|
|
@@ -180,6 +180,7 @@ function parseSessionEntry(td) {
|
|
|
180
180
|
// Type: parenthesized text after </span>, case-insensitive
|
|
181
181
|
const type = parseLessonTypeAfterSubject(subjectEl).toLowerCase();
|
|
182
182
|
const subgroupMatch = plainText.match(SUBGROUP_RE);
|
|
183
|
+
const retakeAttempt = parseRetakeAttempt(stripHtml(fullHtml));
|
|
183
184
|
const parts = linesAfterSubject(fullHtml, subject);
|
|
184
185
|
const teacherPart = parts.find((part) => stripDistanceMarker(part).length > 0 &&
|
|
185
186
|
!/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/.test(part) &&
|
|
@@ -198,6 +199,7 @@ function parseSessionEntry(td) {
|
|
|
198
199
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
199
200
|
isDistance: hasDistanceMarker(plainText) || hasDistanceMarker(room ?? ""),
|
|
200
201
|
possibleChanges,
|
|
202
|
+
...(retakeAttempt ? { retakeAttempt } : {}),
|
|
201
203
|
},
|
|
202
204
|
time: { start: parseTime(timeMatch[1]), end: parseTime(timeMatch[2]) },
|
|
203
205
|
};
|
package/dist/tt/parse/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { parseGroupsString } from "./groups.js";
|
|
2
|
-
export { parseAcademicYearFromPage, parseRoomButtons, parseRoomName, parseFacultyButtons, parseGroupButtons, parseGroupName, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
|
|
2
|
+
export { parseAcademicYearFromPage, parseAvailablePeriodsFromPage, parseRoomButtons, parseRoomName, parseFacultyButtons, parseGroupButtons, parseGroupName, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
|
|
3
3
|
export { parseGroupSchedule } from "./full-schedule.js";
|
|
4
4
|
export { parseRoomSchedule, parseRoomInfo, } from "./audience.js";
|
|
5
5
|
export { parseTeacherSchedule, parseTeacherInfo, } from "./teacher.js";
|
package/dist/tt/parse/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { parseGroupsString } from "./groups.js";
|
|
2
|
-
export { parseAcademicYearFromPage, parseRoomButtons, parseRoomName, parseFacultyButtons, parseGroupButtons, parseGroupName, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
|
|
2
|
+
export { parseAcademicYearFromPage, parseAvailablePeriodsFromPage, parseRoomButtons, parseRoomName, parseFacultyButtons, parseGroupButtons, parseGroupName, parsePeriodFromPage, parseTeacherButtons, } from "./lists.js";
|
|
3
3
|
export { parseGroupSchedule } from "./full-schedule.js";
|
|
4
4
|
export { parseRoomSchedule, parseRoomInfo, } from "./audience.js";
|
|
5
5
|
export { parseTeacherSchedule, parseTeacherInfo, } from "./teacher.js";
|
package/dist/tt/parse/lists.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AcademicPeriod } from "../../common/types.js";
|
|
2
2
|
import type { Faculty, Group, Room } from "../types.js";
|
|
3
|
+
/** Period tabs exposed by this timetable page, in portal order. */
|
|
4
|
+
export declare function parseAvailablePeriodsFromPage(html: string): AcademicPeriod[];
|
|
3
5
|
export declare function parsePeriodFromPage(html: string): AcademicPeriod | null;
|
|
4
6
|
/** Parse the start year from labels like "2026/2027 учебный год". */
|
|
5
7
|
export declare function parseAcademicYearFromPage(html: string): number | null;
|
package/dist/tt/parse/lists.js
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import { parseHtml, text } from "../../common/parse.js";
|
|
2
|
+
import { AcademicPeriod } from "../../common/types.js";
|
|
2
3
|
const PERIOD_LABELS = {
|
|
3
4
|
"осенний семестр": 1,
|
|
4
5
|
"зимняя сессия": 2,
|
|
5
6
|
"весенний семестр": 3,
|
|
6
7
|
"летняя сессия": 4,
|
|
8
|
+
"гиа": AcademicPeriod.StateFinalAssessment,
|
|
9
|
+
"ппа": AcademicPeriod.Retake,
|
|
7
10
|
};
|
|
11
|
+
const KNOWN_PERIODS = new Set(Object.values(AcademicPeriod).filter((value) => typeof value === "number"));
|
|
12
|
+
/** Period tabs exposed by this timetable page, in portal order. */
|
|
13
|
+
export function parseAvailablePeriodsFromPage(html) {
|
|
14
|
+
const doc = parseHtml(html);
|
|
15
|
+
const periods = new Set();
|
|
16
|
+
for (const input of doc.querySelectorAll('input[name="pertype"]')) {
|
|
17
|
+
const value = Number(input.getAttribute("value"));
|
|
18
|
+
if (KNOWN_PERIODS.has(value))
|
|
19
|
+
periods.add(value);
|
|
20
|
+
}
|
|
21
|
+
return [...periods];
|
|
22
|
+
}
|
|
8
23
|
export function parsePeriodFromPage(html) {
|
|
9
24
|
const doc = parseHtml(html);
|
|
10
25
|
// Schedule pages expose the active period as the checked radio button.
|
|
11
26
|
const checked = doc.querySelector('input[name="pertype"][checked]');
|
|
12
27
|
const checkedValue = Number(checked?.getAttribute("value"));
|
|
13
|
-
if (checkedValue
|
|
28
|
+
if (KNOWN_PERIODS.has(checkedValue)) {
|
|
14
29
|
return checkedValue;
|
|
15
30
|
}
|
|
16
31
|
// Some pages keep the selected period only in the hidden form field.
|
|
17
32
|
const hiddenValue = Number(doc.querySelector('#htype')?.getAttribute("value"));
|
|
18
|
-
if (hiddenValue
|
|
33
|
+
if (KNOWN_PERIODS.has(hiddenValue)) {
|
|
19
34
|
return hiddenValue;
|
|
20
35
|
}
|
|
21
36
|
// Legacy markup used an italic textual marker.
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -96,6 +96,8 @@ export interface ParsedLesson {
|
|
|
96
96
|
substituteFor?: SubstituteForInfo;
|
|
97
97
|
/** Whether this entry is marked as potentially changing (class="want"). */
|
|
98
98
|
possibleChanges?: boolean;
|
|
99
|
+
/** Repeated interim assessment attempt (ППА), when marked by the portal. */
|
|
100
|
+
retakeAttempt?: 1 | 2;
|
|
99
101
|
}
|
|
100
102
|
export interface ParsedScheduleBlock {
|
|
101
103
|
/** Portal ordinal claim. It is independent from the time claim. */
|
package/dist/tt/utils/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { formatLocalDate, getMonday, getWeekdayName, isSameDay, isLocalDate, par
|
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
4
|
export { getStandardScheduleBlocks } from "./time-slots.js";
|
|
5
|
+
export { LessonType, parseLessonType } from "./lesson-type.js";
|
|
5
6
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
6
7
|
export type { Holiday, HolidayTransfer } from "./holidays.js";
|
package/dist/tt/utils/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { formatLocalDate, getMonday, getWeekdayName, isSameDay, isLocalDate, par
|
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
4
|
export { getStandardScheduleBlocks } from "./time-slots.js";
|
|
5
|
+
export { LessonType, parseLessonType } from "./lesson-type.js";
|
|
5
6
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Stable compact identifiers for portal lesson types. */
|
|
2
|
+
export declare enum LessonType {
|
|
3
|
+
Unknown = 0,
|
|
4
|
+
Lecture = 1,
|
|
5
|
+
Practical = 2,
|
|
6
|
+
Laboratory = 3,
|
|
7
|
+
Consultation = 4,
|
|
8
|
+
Credit = 5,
|
|
9
|
+
GradedCredit = 6,
|
|
10
|
+
Exam = 7,
|
|
11
|
+
Individual = 8,
|
|
12
|
+
Group = 9,
|
|
13
|
+
CourseProject = 10,
|
|
14
|
+
Krp = 11
|
|
15
|
+
}
|
|
16
|
+
/** Convert portal short code into compact domain identifier. */
|
|
17
|
+
export declare function parseLessonType(code: string): LessonType;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Stable compact identifiers for portal lesson types. */
|
|
2
|
+
export var LessonType;
|
|
3
|
+
(function (LessonType) {
|
|
4
|
+
LessonType[LessonType["Unknown"] = 0] = "Unknown";
|
|
5
|
+
LessonType[LessonType["Lecture"] = 1] = "Lecture";
|
|
6
|
+
LessonType[LessonType["Practical"] = 2] = "Practical";
|
|
7
|
+
LessonType[LessonType["Laboratory"] = 3] = "Laboratory";
|
|
8
|
+
LessonType[LessonType["Consultation"] = 4] = "Consultation";
|
|
9
|
+
LessonType[LessonType["Credit"] = 5] = "Credit";
|
|
10
|
+
LessonType[LessonType["GradedCredit"] = 6] = "GradedCredit";
|
|
11
|
+
LessonType[LessonType["Exam"] = 7] = "Exam";
|
|
12
|
+
LessonType[LessonType["Individual"] = 8] = "Individual";
|
|
13
|
+
LessonType[LessonType["Group"] = 9] = "Group";
|
|
14
|
+
LessonType[LessonType["CourseProject"] = 10] = "CourseProject";
|
|
15
|
+
LessonType[LessonType["Krp"] = 11] = "Krp";
|
|
16
|
+
})(LessonType || (LessonType = {}));
|
|
17
|
+
const TYPE_BY_CODE = {
|
|
18
|
+
лк: LessonType.Lecture,
|
|
19
|
+
пр: LessonType.Practical,
|
|
20
|
+
лб: LessonType.Laboratory,
|
|
21
|
+
конс: LessonType.Consultation,
|
|
22
|
+
зач: LessonType.Credit,
|
|
23
|
+
зачо: LessonType.GradedCredit,
|
|
24
|
+
экз: LessonType.Exam,
|
|
25
|
+
из: LessonType.Individual,
|
|
26
|
+
гз: LessonType.Group,
|
|
27
|
+
кп: LessonType.CourseProject,
|
|
28
|
+
крп: LessonType.Krp,
|
|
29
|
+
};
|
|
30
|
+
/** Convert portal short code into compact domain identifier. */
|
|
31
|
+
export function parseLessonType(code) {
|
|
32
|
+
return TYPE_BY_CODE[code.trim().toLocaleLowerCase("ru-RU").replace(/\.$/u, "")] ?? LessonType.Unknown;
|
|
33
|
+
}
|
package/dist/tt/webinars.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LessonType, parseLessonType } from "./utils/lesson-type.js";
|
|
1
2
|
function normalize(value) {
|
|
2
3
|
return value
|
|
3
4
|
.toLocaleLowerCase("ru-RU")
|
|
@@ -25,7 +26,7 @@ export function findWebinar(lesson, webinars) {
|
|
|
25
26
|
}
|
|
26
27
|
if (normalize(webinar.subject) !== normalize(lesson.subject))
|
|
27
28
|
return false;
|
|
28
|
-
if (webinar.type && lesson.type && webinar.type !== lesson.type)
|
|
29
|
+
if (webinar.type && lesson.type !== LessonType.Unknown && parseLessonType(webinar.type) !== lesson.type)
|
|
29
30
|
return false;
|
|
30
31
|
return true;
|
|
31
32
|
});
|
package/docs/v5-migration.md
CHANGED
|
@@ -104,7 +104,10 @@ new TimetableClient({ educationLevel: EducationLevel.HigherEducation });
|
|
|
104
104
|
| `getTeacherPhoto(id)` / `getTeacherPhotoLazy(id)` | `getTeacherPhoto(id)` |
|
|
105
105
|
|
|
106
106
|
`getGroupSchedule`, `getTeacherSchedule` и `getRoomSchedule` по умолчанию
|
|
107
|
-
загружают все
|
|
107
|
+
загружают все периоды, доступные на соответствующей странице портала, включая
|
|
108
|
+
ГИА (`AcademicPeriod.StateFinalAssessment`, 9) и ППА (`AcademicPeriod.Retake`, 11).
|
|
109
|
+
Уроки ППА содержат `retakeAttempt: 1 | 2`, если номер попытки указан на сайте.
|
|
110
|
+
Для ограничения запросов передайте `periods`:
|
|
108
111
|
|
|
109
112
|
```ts
|
|
110
113
|
import { AcademicPeriod } from "chuvsu-js";
|
|
@@ -117,6 +120,10 @@ const schedule = await client.getTeacherSchedule(teacherId, {
|
|
|
117
120
|
});
|
|
118
121
|
```
|
|
119
122
|
|
|
123
|
+
`LessonType` хранит тип занятия числом в наблюдениях, репозитории и снимке.
|
|
124
|
+
`parseLessonType(code)` переводит код портала в этот индекс. Для неизвестного
|
|
125
|
+
кода возвращается `LessonType.Unknown`; текстовые расшифровки принадлежат клиенту.
|
|
126
|
+
|
|
120
127
|
Для кода, который работает с разными владельцами одинаково, добавлен общий
|
|
121
128
|
метод:
|
|
122
129
|
|
|
@@ -314,7 +321,7 @@ schedule.dateKeys({ subgroup: 2 }); // даты для календаря
|
|
|
314
321
|
после дополнения пары из другого расписания. ` ScheduleSnapshot` использует ту же
|
|
315
322
|
логику дат, переносов, праздников и подгрупп, что и `Schedule`.
|
|
316
323
|
|
|
317
|
-
Формат `SerializedScheduleSnapshot` имеет `schemaVersion:
|
|
324
|
+
Формат `SerializedScheduleSnapshot` имеет `schemaVersion: 3`.
|
|
318
325
|
Новые UUID идентификаторы серий и пар кодируются короче; для перевода сохранённых
|
|
319
326
|
идентификаторов v5 используйте `migrateLegacyLessonId`.
|
|
320
327
|
|