chuvsu-js 4.0.0 → 4.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.
- package/dist/shared.d.ts +3 -3
- package/dist/shared.js +2 -2
- package/dist/tt/client.d.ts +11 -1
- package/dist/tt/client.js +51 -2
- package/dist/tt/parse/audience.js +2 -0
- package/dist/tt/parse/full-schedule.js +15 -1
- package/dist/tt/parse/index.d.ts +1 -0
- package/dist/tt/parse/index.js +1 -0
- package/dist/tt/parse/overlays.js +6 -3
- package/dist/tt/parse/patterns.d.ts +2 -2
- package/dist/tt/parse/patterns.js +2 -2
- package/dist/tt/parse/teacher.js +7 -0
- package/dist/tt/parse/webinars.d.ts +2 -0
- package/dist/tt/parse/webinars.js +98 -0
- package/dist/tt/schedule.d.ts +1 -0
- package/dist/tt/schedule.js +19 -2
- package/dist/tt/types.d.ts +30 -0
- package/dist/tt/utils/holidays.js +6 -6
- package/dist/tt/utils/index.d.ts +1 -1
- package/dist/tt/utils/index.js +1 -1
- package/dist/tt/utils/lessons.d.ts +3 -1
- package/dist/tt/utils/lessons.js +50 -0
- package/package.json +1 -1
package/dist/shared.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { Schedule } from "./tt/schedule.js";
|
|
2
|
-
export { parseGroupsString } from "./tt/parse/index.js";
|
|
3
|
-
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
2
|
+
export { parseGroupsString, parseWebinars } from "./tt/parse/index.js";
|
|
3
|
+
export { attachWebinarsToLessons, getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, matchWebinarToLesson, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
4
4
|
export type { Holiday, HolidayTransfer } from "./tt/utils/index.js";
|
|
5
5
|
export { AuthError, EducationType, ParseError, Period, } from "./common/types.js";
|
|
6
6
|
export type { Teacher, Time, WeekRange } from "./common/types.js";
|
|
7
7
|
export type { BlobAdapter, BlobPutOptions, CacheAdapter, CacheEntry, } from "./common/cache.js";
|
|
8
|
-
export type { Audience, AudienceInfo, CacheConfig, Faculty, FullScheduleDay, FullScheduleSlot, Group, Lesson, LessonTime, LessonTimeSlot, ScheduleEntry, SemesterWeek, SubstituteForInfo, Substitution, TeacherInfo, TransferInfo, TtClientOptions, } from "./tt/types.js";
|
|
8
|
+
export type { Audience, AudienceInfo, CacheConfig, Faculty, FullScheduleDay, FullScheduleSlot, Group, Lesson, LessonTime, LessonTimeSlot, ScheduleEntry, SemesterWeek, SubstituteForInfo, Substitution, TeacherInfo, TransferInfo, TtClientOptions, Webinar, } from "./tt/types.js";
|
|
9
9
|
export type { LkCacheConfig, LkClientOptions, PersonalData } from "./lk/types.js";
|
package/dist/shared.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
// Both ./index.ts and ./browser.ts re-export everything from here and only
|
|
3
3
|
// add their platform-specific extras on top.
|
|
4
4
|
export { Schedule } from "./tt/schedule.js";
|
|
5
|
-
export { parseGroupsString } from "./tt/parse/index.js";
|
|
6
|
-
export { getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
5
|
+
export { parseGroupsString, parseWebinars } from "./tt/parse/index.js";
|
|
6
|
+
export { attachWebinarsToLessons, getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, matchWebinarToLesson, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
7
7
|
export { AuthError, ParseError, } from "./common/types.js";
|
package/dist/tt/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CacheEntry } from "../common/cache.js";
|
|
2
2
|
import { Period } from "../common/types.js";
|
|
3
3
|
import { Schedule } from "./schedule.js";
|
|
4
|
-
import type { Audience, AudienceInfo, Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig } from "./types.js";
|
|
4
|
+
import type { Audience, AudienceInfo, Faculty, Group, TeacherInfo, TtClientOptions, CacheConfig, Webinar } from "./types.js";
|
|
5
5
|
export declare class TtClient {
|
|
6
6
|
private http;
|
|
7
7
|
private educationType;
|
|
@@ -36,6 +36,16 @@ export declare class TtClient {
|
|
|
36
36
|
groupId: number;
|
|
37
37
|
period: Period;
|
|
38
38
|
}): Promise<Schedule>;
|
|
39
|
+
getWebinars(opts?: {
|
|
40
|
+
date?: Date;
|
|
41
|
+
facultyId?: number;
|
|
42
|
+
}): Promise<Webinar[]>;
|
|
43
|
+
getWebinarJoinUrl(opts: {
|
|
44
|
+
webinarId: string | number;
|
|
45
|
+
idType?: number;
|
|
46
|
+
email: string;
|
|
47
|
+
password: string;
|
|
48
|
+
}): Promise<string>;
|
|
39
49
|
getFaculties(): Promise<Faculty[]>;
|
|
40
50
|
getGroupsForFaculty(opts: {
|
|
41
51
|
facultyId: number;
|
package/dist/tt/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpClient } from "../common/http.js";
|
|
2
2
|
import { HybridCache } from "../common/cache.js";
|
|
3
3
|
import { AuthError } from "../common/types.js";
|
|
4
|
-
import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, } from "./parse/index.js";
|
|
4
|
+
import { parseAudienceButtons, parseAudienceFullSchedule, parseAudienceInfo, parseAudienceName, parseGroupButtons, parseFacultyButtons, parseTeacherButtons, parseFullSchedule, parseTeacherFullSchedule, parseTeacherInfo, parseWebinars, } from "./parse/index.js";
|
|
5
5
|
import { Schedule } from "./schedule.js";
|
|
6
6
|
const BASE = "https://tt.chuvsu.ru";
|
|
7
7
|
const AUTH_URL = `${BASE}/auth`;
|
|
@@ -23,8 +23,20 @@ function makeUniformCacheConfig(ttl) {
|
|
|
23
23
|
teacherPhotos: ttl,
|
|
24
24
|
audienceInfo: ttl,
|
|
25
25
|
audienceImages: ttl,
|
|
26
|
+
webinars: ttl,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
function formatDate(date) {
|
|
30
|
+
const year = date.getFullYear();
|
|
31
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
32
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
33
|
+
return `${year}-${month}-${day}`;
|
|
34
|
+
}
|
|
35
|
+
function getAcademicYearKey(date = new Date()) {
|
|
36
|
+
const year = date.getFullYear();
|
|
37
|
+
const start = date.getMonth() >= 8 ? year : year - 1;
|
|
38
|
+
return `${start}-${start + 1}`;
|
|
39
|
+
}
|
|
28
40
|
export class TtClient {
|
|
29
41
|
http = new HttpClient();
|
|
30
42
|
educationType;
|
|
@@ -113,7 +125,7 @@ export class TtClient {
|
|
|
113
125
|
}
|
|
114
126
|
// --- Schedule ---
|
|
115
127
|
async fetchSchedule(groupId, period) {
|
|
116
|
-
const cacheKey = `${groupId}:${period}`;
|
|
128
|
+
const cacheKey = `${groupId}:${period}:${getAcademicYearKey()}`;
|
|
117
129
|
const cached = await this.cache?.get("schedule", cacheKey);
|
|
118
130
|
if (cached)
|
|
119
131
|
return cached;
|
|
@@ -147,6 +159,43 @@ export class TtClient {
|
|
|
147
159
|
schedules.set(opts.period, days);
|
|
148
160
|
return new Schedule(opts.groupId, schedules, opts.period, this.educationType);
|
|
149
161
|
}
|
|
162
|
+
async getWebinars(opts) {
|
|
163
|
+
const date = opts?.date ?? new Date();
|
|
164
|
+
const facultyId = opts?.facultyId ?? 0;
|
|
165
|
+
const cacheKey = `${formatDate(date)}:${facultyId}:${this.pertt}`;
|
|
166
|
+
const cached = await this.cache?.get("webinars", cacheKey);
|
|
167
|
+
if (cached)
|
|
168
|
+
return cached;
|
|
169
|
+
const { body } = await this.authPost(`${BASE}/webinar`, {
|
|
170
|
+
seldate: formatDate(date),
|
|
171
|
+
selfac: String(facultyId),
|
|
172
|
+
pertt: this.pertt,
|
|
173
|
+
});
|
|
174
|
+
const data = parseWebinars(body);
|
|
175
|
+
await this.cache?.set("webinars", cacheKey, data);
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
async getWebinarJoinUrl(opts) {
|
|
179
|
+
const { body } = await this.authPost(`${BASE}/webinar/getjoin`, {
|
|
180
|
+
idw: String(opts.webinarId),
|
|
181
|
+
idwt: String(opts.idType ?? 1),
|
|
182
|
+
name: opts.email,
|
|
183
|
+
pass: opts.password,
|
|
184
|
+
auto: "1",
|
|
185
|
+
});
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(body);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new AuthError("TT webinar join failed: invalid response");
|
|
192
|
+
}
|
|
193
|
+
const data = parsed;
|
|
194
|
+
if (data.mes !== "SUCCESS" || !data.url?.startsWith("http")) {
|
|
195
|
+
throw new AuthError(data.mes ?? "TT webinar join failed");
|
|
196
|
+
}
|
|
197
|
+
return data.url;
|
|
198
|
+
}
|
|
150
199
|
// --- Search / Discovery ---
|
|
151
200
|
async getFaculties() {
|
|
152
201
|
const cached = await this.cache?.get("faculties", "all");
|
|
@@ -3,6 +3,7 @@ import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
|
3
3
|
import { parseGroupsString } from "./groups.js";
|
|
4
4
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
5
5
|
import { LESSON_TYPE_GLOBAL_RE, LESSON_TYPE_RE, SUBGROUP_ANNOTATION_RE, SUBGROUP_RE, WEEKS_GLOBAL_RE, WEEKS_RE, } from "./patterns.js";
|
|
6
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
6
7
|
export function parseAudienceInfo(html) {
|
|
7
8
|
const doc = parseHtml(html);
|
|
8
9
|
// Name: <span class="htext"><nobr>Аудитория <span style="color: blue;">NAME</span></nobr></span>
|
|
@@ -141,6 +142,7 @@ function parseAudienceSemesterEntry(el) {
|
|
|
141
142
|
groups: parseGroupsString(groupsLine),
|
|
142
143
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
143
144
|
weekParity,
|
|
145
|
+
isDistance: DISTANCE_RE.test(cleanText),
|
|
144
146
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
145
147
|
possibleChanges,
|
|
146
148
|
};
|
|
@@ -2,6 +2,7 @@ import { parseHtml, parseTeacher, parseTime, parseWeekParity, parseWeeks, text,
|
|
|
2
2
|
import { getLessonNumber } from "../utils/index.js";
|
|
3
3
|
import { parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
4
4
|
import { FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
5
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
5
6
|
export function parseFullSchedule(html, educationType) {
|
|
6
7
|
const doc = parseHtml(html);
|
|
7
8
|
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
@@ -110,6 +111,7 @@ function parseSemesterEntry(el) {
|
|
|
110
111
|
groups: [],
|
|
111
112
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
112
113
|
weekParity,
|
|
114
|
+
isDistance: DISTANCE_RE.test(cleanText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
113
115
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
114
116
|
possibleChanges,
|
|
115
117
|
};
|
|
@@ -162,6 +164,7 @@ function parseSessionEntry(td) {
|
|
|
162
164
|
const plainText = text(td);
|
|
163
165
|
if (!plainText)
|
|
164
166
|
return null;
|
|
167
|
+
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
165
168
|
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
166
169
|
const subject = subjectEl ? text(subjectEl) : "";
|
|
167
170
|
if (!subject)
|
|
@@ -172,6 +175,14 @@ function parseSessionEntry(td) {
|
|
|
172
175
|
// Type: parenthesized text after </span>, case-insensitive
|
|
173
176
|
const typeMatch = plainText.match(FLEXIBLE_LESSON_TYPE_RE_I);
|
|
174
177
|
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
178
|
+
const subgroupMatch = plainText.match(SUBGROUP_RE);
|
|
179
|
+
const parts = fullHtml
|
|
180
|
+
.split(/<br\s*\/?>/i)
|
|
181
|
+
.map((part) => part.replace(/<[^>]*>/g, "").trim())
|
|
182
|
+
.filter((part) => part.length > 0);
|
|
183
|
+
const teacherPart = parts.find((part) => !part.includes(subject) &&
|
|
184
|
+
!/^\d{2}:\d{2}\s*-\s*\d{2}:\d{2}$/.test(part) &&
|
|
185
|
+
!SUBGROUP_RE.test(part)) ?? "";
|
|
175
186
|
// Time: after <br>, format HH:MM - HH:MM
|
|
176
187
|
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
177
188
|
if (!timeMatch)
|
|
@@ -182,8 +193,11 @@ function parseSessionEntry(td) {
|
|
|
182
193
|
subject,
|
|
183
194
|
type,
|
|
184
195
|
weeks: { from: 0, to: 0 },
|
|
185
|
-
teacher:
|
|
196
|
+
teacher: parseTeacher(teacherPart),
|
|
186
197
|
groups: [],
|
|
198
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
199
|
+
isDistance: DISTANCE_RE.test(plainText) || DISTANCE_RE.test(room),
|
|
200
|
+
possibleChanges,
|
|
187
201
|
},
|
|
188
202
|
timeStart: parseTime(timeMatch[1]),
|
|
189
203
|
timeEnd: parseTime(timeMatch[2]),
|
package/dist/tt/parse/index.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGrou
|
|
|
3
3
|
export { parseFullSchedule } from "./full-schedule.js";
|
|
4
4
|
export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
|
|
5
5
|
export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
|
|
6
|
+
export { parseWebinars } from "./webinars.js";
|
package/dist/tt/parse/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { parseAudienceButtons, parseAudienceName, parseFacultyButtons, parseGrou
|
|
|
3
3
|
export { parseFullSchedule } from "./full-schedule.js";
|
|
4
4
|
export { parseAudienceFullSchedule, parseAudienceInfo, } from "./audience.js";
|
|
5
5
|
export { parseTeacherFullSchedule, parseTeacherInfo, } from "./teacher.js";
|
|
6
|
+
export { parseWebinars } from "./webinars.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { parseTeacher, text } from "../../common/parse.js";
|
|
2
2
|
import { parseGroupsString } from "./groups.js";
|
|
3
3
|
import { LESSON_TYPE_PATTERN, LESSON_TYPE_RE, LESSON_TYPE_RE_I, SUBGROUP_RE, } from "./patterns.js";
|
|
4
|
+
const GROUP_CODE_RE = /[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+/u;
|
|
5
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
4
6
|
export function parseDate(dd, mm, yyyy) {
|
|
5
7
|
return new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
6
8
|
}
|
|
@@ -29,8 +31,7 @@ export function parseTransferDiv(div) {
|
|
|
29
31
|
const cleaned = part.trim();
|
|
30
32
|
if (!cleaned)
|
|
31
33
|
continue;
|
|
32
|
-
|
|
33
|
-
if (groups.length > 0) {
|
|
34
|
+
if (GROUP_CODE_RE.test(cleaned)) {
|
|
34
35
|
groupsPart = cleaned;
|
|
35
36
|
continue;
|
|
36
37
|
}
|
|
@@ -53,6 +54,7 @@ export function parseTransferDiv(div) {
|
|
|
53
54
|
teacher: parseTeacher(teacherPart),
|
|
54
55
|
groups: parseGroupsString(groupsPart),
|
|
55
56
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
57
|
+
isDistance: DISTANCE_RE.test(divText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
56
58
|
transfer,
|
|
57
59
|
},
|
|
58
60
|
};
|
|
@@ -72,7 +74,7 @@ export function parseSubstitutionDiv(div) {
|
|
|
72
74
|
const teacherMatch = divHtml.match(/Преподаватель:\s*<span[^>]*>([^<]+)<\/span>/);
|
|
73
75
|
if (teacherMatch)
|
|
74
76
|
teacher = parseTeacher(teacherMatch[1].trim());
|
|
75
|
-
return { date, room, teacher };
|
|
77
|
+
return { date, room, teacher, isDistance: DISTANCE_RE.test(room ?? divText) };
|
|
76
78
|
}
|
|
77
79
|
export function parseSubstituteForDiv(div) {
|
|
78
80
|
const divText = text(div);
|
|
@@ -111,6 +113,7 @@ export function parseSubstituteForDiv(div) {
|
|
|
111
113
|
teacher: { name: "" },
|
|
112
114
|
groups: parseGroupsString(groupsMatch?.[1]),
|
|
113
115
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
116
|
+
isDistance: DISTANCE_RE.test(divText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
114
117
|
substituteFor: { date, originalTeacher },
|
|
115
118
|
},
|
|
116
119
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare const LESSON_TYPE_PATTERN = "\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441";
|
|
2
|
-
export declare const FLEXIBLE_LESSON_TYPE_PATTERN = "
|
|
1
|
+
export declare const LESSON_TYPE_PATTERN = "\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447\u043E|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441|\u043A\u043F";
|
|
2
|
+
export declare const FLEXIBLE_LESSON_TYPE_PATTERN = "(?:\u043B\u043A|\u043F\u0440|\u043B\u0431|\u0437\u0430\u0447\u043E|\u0437\u0430\u0447|\u044D\u043A\u0437|\u043A\u043E\u043D\u0441|\u043A\u043F)\\.?|\u042D\u043A\u0437";
|
|
3
3
|
export declare const LESSON_TYPE_RE: RegExp;
|
|
4
4
|
export declare const LESSON_TYPE_RE_I: RegExp;
|
|
5
5
|
export declare const FLEXIBLE_LESSON_TYPE_RE_I: RegExp;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const LESSON_TYPE_PATTERN = "
|
|
2
|
-
export const FLEXIBLE_LESSON_TYPE_PATTERN =
|
|
1
|
+
export const LESSON_TYPE_PATTERN = "лк|пр|лб|зачо|зач|экз|конс|кп";
|
|
2
|
+
export const FLEXIBLE_LESSON_TYPE_PATTERN = `(?:${LESSON_TYPE_PATTERN})\\.?|Экз`;
|
|
3
3
|
export const LESSON_TYPE_RE = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`);
|
|
4
4
|
export const LESSON_TYPE_RE_I = new RegExp(`\\((${LESSON_TYPE_PATTERN})\\)`, "i");
|
|
5
5
|
export const FLEXIBLE_LESSON_TYPE_RE_I = new RegExp(`\\((${FLEXIBLE_LESSON_TYPE_PATTERN})\\)`, "i");
|
package/dist/tt/parse/teacher.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseSemesterScheduleWith } from "./full-schedule.js";
|
|
|
4
4
|
import { parseGroupsString } from "./groups.js";
|
|
5
5
|
import { parseSubstituteForDiv, parseSubstitutionDiv, parseTransferDiv, } from "./overlays.js";
|
|
6
6
|
import { FLEXIBLE_LESSON_TYPE_PATTERN, FLEXIBLE_LESSON_TYPE_RE_I, LESSON_TYPE_RE, SUBGROUP_RE, WEEKS_RE, } from "./patterns.js";
|
|
7
|
+
const DISTANCE_RE = /дистанционно|ДОТ/i;
|
|
7
8
|
export function parseTeacherFullSchedule(html, educationType) {
|
|
8
9
|
const doc = parseHtml(html);
|
|
9
10
|
const edType = educationType ?? 1 /* EducationType.HigherEducation */;
|
|
@@ -68,6 +69,7 @@ function parseTeacherSemesterEntry(el) {
|
|
|
68
69
|
groups: parseGroupsString(groupsMatch?.[1]),
|
|
69
70
|
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
70
71
|
weekParity,
|
|
72
|
+
isDistance: DISTANCE_RE.test(cleanText) || DISTANCE_RE.test(roomMatch?.[1] ?? ""),
|
|
71
73
|
substitutions: substitutions.length > 0 ? substitutions : undefined,
|
|
72
74
|
possibleChanges,
|
|
73
75
|
};
|
|
@@ -116,6 +118,7 @@ function parseTeacherSessionEntry(td) {
|
|
|
116
118
|
const plainText = text(td);
|
|
117
119
|
if (!plainText)
|
|
118
120
|
return null;
|
|
121
|
+
const possibleChanges = (td.getAttribute("class") ?? "").includes("want") || undefined;
|
|
119
122
|
const subjectEl = td.querySelector('span[style*="color: blue"]');
|
|
120
123
|
const subject = subjectEl ? text(subjectEl) : "";
|
|
121
124
|
if (!subject)
|
|
@@ -124,6 +127,7 @@ function parseTeacherSessionEntry(td) {
|
|
|
124
127
|
const room = roomMatch ? roomMatch[1].trim() : "";
|
|
125
128
|
const typeMatch = plainText.match(FLEXIBLE_LESSON_TYPE_RE_I);
|
|
126
129
|
const type = typeMatch ? typeMatch[1].replace(/\.$/, "").toLowerCase() : "";
|
|
130
|
+
const subgroupMatch = plainText.match(SUBGROUP_RE);
|
|
127
131
|
// Groups: text between </span> type and <br>time
|
|
128
132
|
const groupsMatch = fullHtml.match(new RegExp(`\\((?:${FLEXIBLE_LESSON_TYPE_PATTERN})\\)\\s*([^<]+?)\\s*<br`, "i"));
|
|
129
133
|
const timeMatch = fullHtml.match(/<br\s*\/?>\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
@@ -137,6 +141,9 @@ function parseTeacherSessionEntry(td) {
|
|
|
137
141
|
weeks: { from: 0, to: 0 },
|
|
138
142
|
teacher: { name: "" },
|
|
139
143
|
groups: parseGroupsString(groupsMatch?.[1]),
|
|
144
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
145
|
+
isDistance: DISTANCE_RE.test(plainText) || DISTANCE_RE.test(room),
|
|
146
|
+
possibleChanges,
|
|
140
147
|
},
|
|
141
148
|
timeStart: parseTime(timeMatch[1]),
|
|
142
149
|
timeEnd: parseTime(timeMatch[2]),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { parseHtml, parseTeacher, parseTime, text, } from "../../common/parse.js";
|
|
2
|
+
import { parseGroupsString } from "./groups.js";
|
|
3
|
+
import { FLEXIBLE_LESSON_TYPE_RE_I, SUBGROUP_RE } from "./patterns.js";
|
|
4
|
+
const GROUP_CODE_RE = /[A-ZА-ЯЁ]{1,}(?:-[A-ZА-ЯЁa-zа-яё0-9]+)+(?:\s*ин)?/u;
|
|
5
|
+
function parseDateValue(value) {
|
|
6
|
+
const match = value?.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
7
|
+
if (!match)
|
|
8
|
+
return undefined;
|
|
9
|
+
return new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]));
|
|
10
|
+
}
|
|
11
|
+
function parseTimeRange(raw) {
|
|
12
|
+
const match = raw.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
return { start: parseTime(match[1]), end: parseTime(match[2]) };
|
|
16
|
+
}
|
|
17
|
+
function splitLesson(raw) {
|
|
18
|
+
const typeMatches = [...raw.matchAll(new RegExp(FLEXIBLE_LESSON_TYPE_RE_I, "gi"))];
|
|
19
|
+
const typeMatch = typeMatches.at(-1);
|
|
20
|
+
if (!typeMatch || typeMatch.index == null) {
|
|
21
|
+
return {
|
|
22
|
+
subject: raw,
|
|
23
|
+
type: "",
|
|
24
|
+
teacherRaw: "",
|
|
25
|
+
groupsRaw: "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const subject = raw.slice(0, typeMatch.index).trim();
|
|
29
|
+
const type = typeMatch[1].replace(/\.$/, "").toLowerCase();
|
|
30
|
+
const rest = raw.slice(typeMatch.index + typeMatch[0].length).trim();
|
|
31
|
+
const groupMatch = rest.match(GROUP_CODE_RE);
|
|
32
|
+
const teacherRaw = groupMatch?.index == null ? rest : rest.slice(0, groupMatch.index).trim();
|
|
33
|
+
const groupsRaw = groupMatch?.index == null ? "" : rest.slice(groupMatch.index).trim();
|
|
34
|
+
const subgroupMatch = raw.match(SUBGROUP_RE);
|
|
35
|
+
return {
|
|
36
|
+
subject,
|
|
37
|
+
type,
|
|
38
|
+
teacherRaw,
|
|
39
|
+
groupsRaw,
|
|
40
|
+
subgroup: subgroupMatch ? parseInt(subgroupMatch[1]) : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function parseWebinarRows(doc, tableSelector, scheduled, fallbackDate) {
|
|
44
|
+
const webinars = [];
|
|
45
|
+
for (const slotRow of doc.querySelectorAll(`${tableSelector} > tbody > tr`)) {
|
|
46
|
+
const slotCells = [...slotRow.children].filter((child) => child.tagName.toLowerCase() === "td");
|
|
47
|
+
const timeCell = slotCells[0];
|
|
48
|
+
const dataCell = slotCells[1];
|
|
49
|
+
if (!timeCell || !dataCell)
|
|
50
|
+
continue;
|
|
51
|
+
const timeText = text(timeCell);
|
|
52
|
+
const timeRange = parseTimeRange(timeText);
|
|
53
|
+
if (!timeRange)
|
|
54
|
+
continue;
|
|
55
|
+
const id = timeCell.querySelector("div")?.getAttribute("id") ?? "";
|
|
56
|
+
const idMatch = id.match(/trd(\d{4}-\d{2}-\d{2})t(\d+)/);
|
|
57
|
+
const date = parseDateValue(idMatch?.[1]) ?? fallbackDate;
|
|
58
|
+
const slotNumber = idMatch ? parseInt(idMatch[2]) : undefined;
|
|
59
|
+
for (const row of dataCell.querySelectorAll("table tr")) {
|
|
60
|
+
const cells = [...row.children].filter((child) => child.tagName.toLowerCase() === "td");
|
|
61
|
+
const lessonCell = cells[0];
|
|
62
|
+
const titleCell = cells[1];
|
|
63
|
+
if (!lessonCell || !titleCell)
|
|
64
|
+
continue;
|
|
65
|
+
const button = row.querySelector("button[onclick*=\"jointo\"]");
|
|
66
|
+
const onclick = button?.getAttribute("onclick") ?? "";
|
|
67
|
+
const joinMatch = onclick.match(/jointo(?:sub)?\('([^']+)',\s*(\d+)\)/);
|
|
68
|
+
const raw = text(lessonCell);
|
|
69
|
+
const parsed = splitLesson(raw);
|
|
70
|
+
webinars.push({
|
|
71
|
+
id: joinMatch?.[1] ?? "",
|
|
72
|
+
idType: joinMatch ? parseInt(joinMatch[2]) : 1,
|
|
73
|
+
scheduled,
|
|
74
|
+
date,
|
|
75
|
+
slotNumber,
|
|
76
|
+
timeStart: timeRange.start,
|
|
77
|
+
timeEnd: timeRange.end,
|
|
78
|
+
subject: parsed.subject,
|
|
79
|
+
type: parsed.type,
|
|
80
|
+
teacher: parseTeacher(parsed.teacherRaw),
|
|
81
|
+
groups: parseGroupsString(parsed.groupsRaw),
|
|
82
|
+
subgroup: parsed.subgroup,
|
|
83
|
+
title: text(titleCell),
|
|
84
|
+
raw,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return webinars;
|
|
89
|
+
}
|
|
90
|
+
export function parseWebinars(html) {
|
|
91
|
+
const doc = parseHtml(html);
|
|
92
|
+
const selectedDate = doc.querySelector('select[name="seldate"] option[selected]')?.getAttribute("value");
|
|
93
|
+
const fallbackDate = parseDateValue(selectedDate);
|
|
94
|
+
return [
|
|
95
|
+
...parseWebinarRows(doc, "#webstt", true, fallbackDate),
|
|
96
|
+
...parseWebinarRows(doc, "#websttext", false, fallbackDate),
|
|
97
|
+
];
|
|
98
|
+
}
|
package/dist/tt/schedule.d.ts
CHANGED
package/dist/tt/schedule.js
CHANGED
|
@@ -60,6 +60,15 @@ export class Schedule {
|
|
|
60
60
|
date.setHours(0, 0, 0, 0);
|
|
61
61
|
return date;
|
|
62
62
|
}
|
|
63
|
+
isInCurrentAcademicYear(date) {
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const startYear = now.getMonth() >= 8
|
|
66
|
+
? now.getFullYear()
|
|
67
|
+
: now.getFullYear() - 1;
|
|
68
|
+
const start = new Date(startYear, 8, 1);
|
|
69
|
+
const end = new Date(startYear + 1, 7, 31, 23, 59, 59, 999);
|
|
70
|
+
return date >= start && date <= end;
|
|
71
|
+
}
|
|
63
72
|
// --- Public query methods ---
|
|
64
73
|
forDay(weekday, opts) {
|
|
65
74
|
const period = this.period;
|
|
@@ -70,7 +79,10 @@ export class Schedule {
|
|
|
70
79
|
const dayName = getWeekdayName(weekday);
|
|
71
80
|
for (const d of days) {
|
|
72
81
|
if (d.weekday.toLowerCase() === dayName.toLowerCase() && d.date) {
|
|
73
|
-
|
|
82
|
+
const slots = filterSlots(d.slots, { subgroup: opts?.subgroup });
|
|
83
|
+
lessons.push(...slotsToLessons(slots, d.date, {
|
|
84
|
+
isTeacherSchedule: this.isTeacherSchedule,
|
|
85
|
+
}));
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
88
|
return lessons.sort(sortLessons);
|
|
@@ -80,6 +92,8 @@ export class Schedule {
|
|
|
80
92
|
return slotsToLessons(slots, date, { isTeacherSchedule: this.isTeacherSchedule });
|
|
81
93
|
}
|
|
82
94
|
forDate(date, opts) {
|
|
95
|
+
if (!this.isInCurrentAcademicYear(date))
|
|
96
|
+
return [];
|
|
83
97
|
if (isHoliday(date, this.holidays, this.holidayTransfers))
|
|
84
98
|
return [];
|
|
85
99
|
const period = getCurrentPeriod({ date });
|
|
@@ -88,7 +102,10 @@ export class Schedule {
|
|
|
88
102
|
for (const [, d] of this.scheduleMap) {
|
|
89
103
|
for (const day of d) {
|
|
90
104
|
if (day.date && isSameDay(day.date, date)) {
|
|
91
|
-
|
|
105
|
+
const slots = filterSlots(day.slots, { subgroup: opts?.subgroup });
|
|
106
|
+
lessons.push(...slotsToLessons(slots, date, {
|
|
107
|
+
isTeacherSchedule: this.isTeacherSchedule,
|
|
108
|
+
}));
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
}
|
package/dist/tt/types.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface Substitution {
|
|
|
42
42
|
date: Date;
|
|
43
43
|
/** New room, if changed. */
|
|
44
44
|
room?: string;
|
|
45
|
+
/** Whether the substitution moves the lesson online. */
|
|
46
|
+
isDistance?: boolean;
|
|
45
47
|
/** New teacher, if changed. */
|
|
46
48
|
teacher?: Teacher;
|
|
47
49
|
}
|
|
@@ -78,6 +80,8 @@ export interface ScheduleEntry {
|
|
|
78
80
|
groups: string[];
|
|
79
81
|
subgroup?: number;
|
|
80
82
|
weekParity?: "even" | "odd";
|
|
83
|
+
/** True when the lesson is explicitly marked as дистанционно / ДОТ. */
|
|
84
|
+
isDistance?: boolean;
|
|
81
85
|
/** Date-specific substitutions (замена на). */
|
|
82
86
|
substitutions?: Substitution[];
|
|
83
87
|
/** If this entry is a transferred lesson (перенос). */
|
|
@@ -121,6 +125,10 @@ export interface Lesson {
|
|
|
121
125
|
weeks: WeekRange;
|
|
122
126
|
subgroup?: number;
|
|
123
127
|
weekParity?: "even" | "odd";
|
|
128
|
+
/** True when the lesson is explicitly marked as дистанционно / ДОТ. */
|
|
129
|
+
isDistance?: boolean;
|
|
130
|
+
/** Matched webinar metadata, if attached by `attachWebinarsToLessons`. */
|
|
131
|
+
webinar?: Webinar;
|
|
124
132
|
/** If a substitution was applied, the original room. */
|
|
125
133
|
originalRoom?: string;
|
|
126
134
|
/** If a substitution was applied, the original teacher. */
|
|
@@ -145,6 +153,27 @@ export interface SemesterWeek {
|
|
|
145
153
|
start: Date;
|
|
146
154
|
end: Date;
|
|
147
155
|
}
|
|
156
|
+
export interface Webinar {
|
|
157
|
+
/** Internal tt.chuvsu.ru webinar id used by `/webinar/getjoin`. */
|
|
158
|
+
id: string;
|
|
159
|
+
/** Webinar table type argument (`idwt`) used by `/webinar/getjoin`. */
|
|
160
|
+
idType: number;
|
|
161
|
+
/** True for "Вебинары по расписанию"; false for external webinars. */
|
|
162
|
+
scheduled: boolean;
|
|
163
|
+
date?: Date;
|
|
164
|
+
slotNumber?: number;
|
|
165
|
+
timeStart: Time;
|
|
166
|
+
timeEnd: Time;
|
|
167
|
+
subject: string;
|
|
168
|
+
type: string;
|
|
169
|
+
teacher: Teacher;
|
|
170
|
+
groups: string[];
|
|
171
|
+
subgroup?: number;
|
|
172
|
+
/** Free-form title/topic from the second table column. */
|
|
173
|
+
title: string;
|
|
174
|
+
/** Raw first-column text as rendered by tt.chuvsu.ru. */
|
|
175
|
+
raw: string;
|
|
176
|
+
}
|
|
148
177
|
export interface CacheConfig {
|
|
149
178
|
schedule?: number;
|
|
150
179
|
faculties?: number;
|
|
@@ -156,6 +185,7 @@ export interface CacheConfig {
|
|
|
156
185
|
teacherPhotos?: number;
|
|
157
186
|
audienceInfo?: number;
|
|
158
187
|
audienceImages?: number;
|
|
188
|
+
webinars?: number;
|
|
159
189
|
}
|
|
160
190
|
export interface TtClientOptions {
|
|
161
191
|
educationType?: EducationType;
|
|
@@ -92,18 +92,18 @@ export function getEffectiveHolidays(year, holidays = RUSSIAN_HOLIDAYS, transfer
|
|
|
92
92
|
const nonJanuaryOnWeekend = holidays
|
|
93
93
|
.filter((h) => !isJanuaryHoliday(h))
|
|
94
94
|
.map((h) => new Date(year, h.month - 1, h.day))
|
|
95
|
-
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
95
|
+
.filter((d) => d.getDay() === 0 || (!sixDayWeek && d.getDay() === 6))
|
|
96
96
|
.sort((a, b) => a.getTime() - b.getTime());
|
|
97
97
|
for (const holiday of nonJanuaryOnWeekend) {
|
|
98
98
|
let candidate = new Date(holiday);
|
|
99
|
-
if (candidate.getDay() === 6) {
|
|
99
|
+
if (!sixDayWeek && candidate.getDay() === 6) {
|
|
100
100
|
candidate.setDate(candidate.getDate() + 2);
|
|
101
101
|
}
|
|
102
102
|
else {
|
|
103
103
|
candidate.setDate(candidate.getDate() + 1);
|
|
104
104
|
}
|
|
105
105
|
while (candidate.getDay() === 0 ||
|
|
106
|
-
candidate.getDay() === 6 ||
|
|
106
|
+
(!sixDayWeek && candidate.getDay() === 6) ||
|
|
107
107
|
isOriginalHoliday(candidate) ||
|
|
108
108
|
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
109
109
|
candidate.setDate(candidate.getDate() + 1);
|
|
@@ -141,19 +141,19 @@ export function getHolidayTransfers(year, holidays = RUSSIAN_HOLIDAYS, transfers
|
|
|
141
141
|
const nonJanuaryOnWeekend = holidays
|
|
142
142
|
.filter((h) => !isJanuaryHoliday(h))
|
|
143
143
|
.map((h) => new Date(year, h.month - 1, h.day))
|
|
144
|
-
.filter((d) => d.getDay() === 0 || d.getDay() === 6)
|
|
144
|
+
.filter((d) => d.getDay() === 0 || (!sixDayWeek && d.getDay() === 6))
|
|
145
145
|
.sort((a, b) => a.getTime() - b.getTime());
|
|
146
146
|
const isOriginalHoliday = (d) => originalDates.some((od) => isSameDay(od, d));
|
|
147
147
|
for (const holiday of nonJanuaryOnWeekend) {
|
|
148
148
|
let candidate = new Date(holiday);
|
|
149
|
-
if (candidate.getDay() === 6) {
|
|
149
|
+
if (!sixDayWeek && candidate.getDay() === 6) {
|
|
150
150
|
candidate.setDate(candidate.getDate() + 2);
|
|
151
151
|
}
|
|
152
152
|
else {
|
|
153
153
|
candidate.setDate(candidate.getDate() + 1);
|
|
154
154
|
}
|
|
155
155
|
while (candidate.getDay() === 0 ||
|
|
156
|
-
candidate.getDay() === 6 ||
|
|
156
|
+
(!sixDayWeek && candidate.getDay() === 6) ||
|
|
157
157
|
isOriginalHoliday(candidate) ||
|
|
158
158
|
effectiveDays.some((ed) => isSameDay(ed, candidate))) {
|
|
159
159
|
candidate.setDate(candidate.getDate() + 1);
|
package/dist/tt/utils/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { getMonday, getWeekdayName, isSameDay } from "./date.js";
|
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
4
|
export { getLessonNumber, getTimeSlots } from "./time-slots.js";
|
|
5
|
-
export { collectTransfers, filterSlots, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
5
|
+
export { attachWebinarsToLessons, collectTransfers, filterSlots, matchWebinarToLesson, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
6
6
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
7
7
|
export type { Holiday, HolidayTransfer } from "./holidays.js";
|
package/dist/tt/utils/index.js
CHANGED
|
@@ -2,5 +2,5 @@ export { getMonday, getWeekdayName, isSameDay } from "./date.js";
|
|
|
2
2
|
export { getAdjacentSemester, getCurrentPeriod, isSessionPeriod, } from "./period.js";
|
|
3
3
|
export { getSemesterStart, getSemesterWeeks, getWeekNumber, } from "./semester.js";
|
|
4
4
|
export { getLessonNumber, getTimeSlots } from "./time-slots.js";
|
|
5
|
-
export { collectTransfers, filterSlots, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
5
|
+
export { attachWebinarsToLessons, collectTransfers, filterSlots, matchWebinarToLesson, slotsToLessons, sortLessons, suppressTransferredLessons, } from "./lessons.js";
|
|
6
6
|
export { getCompensatingWorkDays, getEffectiveHolidays, getHolidayTransfers, isHoliday, RUSSIAN_HOLIDAYS, } from "./holidays.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FullScheduleDay, FullScheduleSlot, Lesson, ScheduleEntry } from "../types.js";
|
|
1
|
+
import type { FullScheduleDay, FullScheduleSlot, Lesson, ScheduleEntry, Webinar } from "../types.js";
|
|
2
2
|
export declare function sortLessons(a: Lesson, b: Lesson): number;
|
|
3
3
|
export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
|
|
4
4
|
subgroup?: number;
|
|
@@ -8,6 +8,8 @@ export declare function filterSlots(slots: FullScheduleSlot[], opts?: {
|
|
|
8
8
|
export declare function slotsToLessons(slots: FullScheduleSlot[], date: Date, opts?: {
|
|
9
9
|
isTeacherSchedule?: boolean;
|
|
10
10
|
}): Lesson[];
|
|
11
|
+
export declare function matchWebinarToLesson(lesson: Lesson, webinars: Webinar[]): Webinar | undefined;
|
|
12
|
+
export declare function attachWebinarsToLessons(lessons: Lesson[], webinars: Webinar[]): Lesson[];
|
|
11
13
|
/** All transfer entries from the given schedule days. */
|
|
12
14
|
export declare function collectTransfers(days: FullScheduleDay[]): ScheduleEntry[];
|
|
13
15
|
/** Remove lessons whose source date/slot match a transfer. */
|
package/dist/tt/utils/lessons.js
CHANGED
|
@@ -51,12 +51,16 @@ function makeLessonTime(date, time) {
|
|
|
51
51
|
d.setHours(time.hours, time.minutes, 0, 0);
|
|
52
52
|
return { date: d, hours: time.hours, minutes: time.minutes };
|
|
53
53
|
}
|
|
54
|
+
function isDistanceRoom(room) {
|
|
55
|
+
return /дистанционно|ДОТ/i.test(room ?? "");
|
|
56
|
+
}
|
|
54
57
|
export function slotsToLessons(slots, date, opts) {
|
|
55
58
|
const lessons = [];
|
|
56
59
|
for (const slot of slots) {
|
|
57
60
|
for (const entry of slot.entries) {
|
|
58
61
|
let room = entry.room;
|
|
59
62
|
let teacher = entry.teacher;
|
|
63
|
+
let isDistance = entry.isDistance || isDistanceRoom(room);
|
|
60
64
|
let originalRoom;
|
|
61
65
|
let originalTeacher;
|
|
62
66
|
// Apply date-specific substitutions
|
|
@@ -66,6 +70,7 @@ export function slotsToLessons(slots, date, opts) {
|
|
|
66
70
|
if (sub.room) {
|
|
67
71
|
originalRoom = room;
|
|
68
72
|
room = sub.room;
|
|
73
|
+
isDistance = sub.isDistance || isDistanceRoom(room);
|
|
69
74
|
}
|
|
70
75
|
if (sub.teacher) {
|
|
71
76
|
// On teacher schedules, a teacher substitution means another teacher
|
|
@@ -89,6 +94,7 @@ export function slotsToLessons(slots, date, opts) {
|
|
|
89
94
|
weeks: entry.weeks,
|
|
90
95
|
subgroup: entry.subgroup,
|
|
91
96
|
weekParity: entry.weekParity,
|
|
97
|
+
isDistance,
|
|
92
98
|
originalRoom,
|
|
93
99
|
originalTeacher,
|
|
94
100
|
transfer: entry.transfer,
|
|
@@ -99,6 +105,50 @@ export function slotsToLessons(slots, date, opts) {
|
|
|
99
105
|
}
|
|
100
106
|
return lessons;
|
|
101
107
|
}
|
|
108
|
+
function normalizeText(value) {
|
|
109
|
+
return value
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
.replace(/ё/g, "е")
|
|
112
|
+
.replace(/\s+/g, " ")
|
|
113
|
+
.trim();
|
|
114
|
+
}
|
|
115
|
+
function sameTime(lesson, webinar) {
|
|
116
|
+
return (lesson.start.hours === webinar.timeStart.hours &&
|
|
117
|
+
lesson.start.minutes === webinar.timeStart.minutes &&
|
|
118
|
+
lesson.end.hours === webinar.timeEnd.hours &&
|
|
119
|
+
lesson.end.minutes === webinar.timeEnd.minutes);
|
|
120
|
+
}
|
|
121
|
+
export function matchWebinarToLesson(lesson, webinars) {
|
|
122
|
+
return webinars.find((webinar) => {
|
|
123
|
+
if (!webinar.scheduled)
|
|
124
|
+
return false;
|
|
125
|
+
if (webinar.date && !isSameDay(webinar.date, lesson.start.date))
|
|
126
|
+
return false;
|
|
127
|
+
if (webinar.slotNumber != null && webinar.slotNumber !== lesson.number) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (!sameTime(lesson, webinar))
|
|
131
|
+
return false;
|
|
132
|
+
if (normalizeText(webinar.subject) !== normalizeText(lesson.subject)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (webinar.type && lesson.type && webinar.type !== lesson.type) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (webinar.subgroup != null &&
|
|
139
|
+
lesson.subgroup != null &&
|
|
140
|
+
webinar.subgroup !== lesson.subgroup) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export function attachWebinarsToLessons(lessons, webinars) {
|
|
147
|
+
return lessons.map((lesson) => {
|
|
148
|
+
const webinar = matchWebinarToLesson(lesson, webinars);
|
|
149
|
+
return webinar ? { ...lesson, webinar } : lesson;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
102
152
|
/** All transfer entries from the given schedule days. */
|
|
103
153
|
export function collectTransfers(days) {
|
|
104
154
|
const transfers = [];
|