mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 09:47:46 +03:00
Прекращение поддержки расписания v1.
Новые типы пар. Фикс парсинга информации о паре, если в названии присутствуют дополнительные данные.
This commit is contained in:
11475
package-lock.json
generated
11475
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cookie": ">=0.7.0",
|
||||
"create-map-transform-fn": "gist:f65ddd8f17f8c388659aab76890f194b",
|
||||
"dotenv": "^16.4.5",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"jsdom": "^25.0.0",
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { AuthService } from "./auth.service";
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { V1ScheduleService } from "../schedule/v1-schedule.service";
|
||||
import { UserToken } from "./auth.decorator";
|
||||
import { ResponseVersion } from "../version/response-version.decorator";
|
||||
import { SignInDto } from "./dto/sign-in.dto";
|
||||
@@ -19,13 +18,14 @@ import { SignUpDto } from "./dto/sign-up.dto";
|
||||
import { UpdateTokenDto } from "./dto/update-token.dto";
|
||||
import { UpdateTokenResponseDto } from "./dto/update-token-response.dto";
|
||||
import { ChangePasswordDto } from "./dto/change-password.dto";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
|
||||
@ApiTags("v1/auth")
|
||||
@Controller({ path: "auth", version: "1" })
|
||||
export class V1AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly scheduleService: V1ScheduleService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: "Авторизация по логину и паролю" })
|
||||
|
||||
@@ -9,18 +9,18 @@ import {
|
||||
import { AuthService } from "./auth.service";
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { V1ScheduleService } from "../schedule/v1-schedule.service";
|
||||
import { SignInDto } from "./dto/sign-in.dto";
|
||||
import { SignUpDto } from "./dto/sign-up.dto";
|
||||
import { UpdateTokenDto } from "./dto/update-token.dto";
|
||||
import { V2ClientUserDto } from "../users/dto/v2/v2-client-user.dto";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
|
||||
@ApiTags("v2/auth")
|
||||
@Controller({ path: "auth", version: "2" })
|
||||
export class V2AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly scheduleService: V1ScheduleService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: "Авторизация по логину и паролю" })
|
||||
|
||||
31
src/schedule/dto/cache-status.dto.ts
Normal file
31
src/schedule/dto/cache-status.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { IsBoolean, IsHash, IsNumber } from "class-validator";
|
||||
|
||||
export class CacheStatusDto {
|
||||
/**
|
||||
* Хеш данных парсера
|
||||
* @example "40bd001563085fc35165329ea1ff5c5ecbdbbeef"
|
||||
*/
|
||||
@IsHash("sha1")
|
||||
cacheHash: string;
|
||||
|
||||
/**
|
||||
* Требуется ли обновление кеша?
|
||||
* @example true
|
||||
*/
|
||||
@IsBoolean()
|
||||
cacheUpdateRequired: boolean;
|
||||
|
||||
/**
|
||||
* Дата обновления кеша
|
||||
* @example 1729288173002
|
||||
*/
|
||||
@IsNumber()
|
||||
lastCacheUpdate: number;
|
||||
|
||||
/**
|
||||
* Дата обновления расписания
|
||||
* @example 1729288173002
|
||||
*/
|
||||
@IsNumber()
|
||||
lastScheduleUpdate: number;
|
||||
}
|
||||
52
src/schedule/dto/day.dto.ts
Normal file
52
src/schedule/dto/day.dto.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { LessonDto } from "./lesson.dto";
|
||||
|
||||
export class DayDto {
|
||||
/**
|
||||
* День недели
|
||||
* @example "Понедельник"
|
||||
*/
|
||||
@IsString()
|
||||
@Transform(({ value, obj, options }) => {
|
||||
if ((obj as DayDto).street && options?.groups?.includes("v1"))
|
||||
return `${value} | ${(obj as DayDto).street}`;
|
||||
|
||||
return value;
|
||||
})
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Улица (v2)
|
||||
* @example "Железнодорожная, 13"
|
||||
*/
|
||||
@IsString()
|
||||
@Transform(({ value, options }) => {
|
||||
if (value && options?.groups?.includes("v1")) return undefined;
|
||||
|
||||
return value;
|
||||
})
|
||||
@IsOptional()
|
||||
street?: string;
|
||||
|
||||
/**
|
||||
* Дата
|
||||
* @example "2024-10-06T20:00:00.000Z"
|
||||
*/
|
||||
@IsDateString()
|
||||
date: Date;
|
||||
|
||||
/**
|
||||
* Занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LessonDto)
|
||||
lessons: Array<LessonDto>;
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { IsArray, IsObject, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V2ScheduleDto } from "./v2-schedule.dto";
|
||||
import { V2GroupDto } from "./v2-group.dto";
|
||||
import { ScheduleDto } from "./schedule.dto";
|
||||
import { GroupDto } from "./group.dto";
|
||||
|
||||
export class V2GroupScheduleDto extends PickType(V2ScheduleDto, ["updatedAt"]) {
|
||||
export class GroupScheduleDto extends PickType(ScheduleDto, ["updatedAt"]) {
|
||||
/**
|
||||
* Расписание группы
|
||||
*/
|
||||
@IsObject()
|
||||
group: V2GroupDto;
|
||||
@Type(() => GroupDto)
|
||||
group: GroupDto;
|
||||
|
||||
/**
|
||||
* Обновлённые дни с последнего изменения расписания
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IsArray, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V2DayDto } from "./v2-day.dto";
|
||||
import { DayDto } from "./day.dto";
|
||||
|
||||
export class V2GroupDto {
|
||||
export class GroupDto {
|
||||
/**
|
||||
* Название группы
|
||||
* @example "ИС-214/23"
|
||||
@@ -15,6 +15,6 @@ export class V2GroupDto {
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => V2DayDto)
|
||||
days: Array<V2DayDto>;
|
||||
@Type(() => DayDto)
|
||||
days: Array<DayDto>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class V2LessonSubGroupDto {
|
||||
export class LessonSubGroupDto {
|
||||
/**
|
||||
* Номер подгруппы
|
||||
* @example 1
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsDateString } from "class-validator";
|
||||
|
||||
export class V2LessonTimeDto {
|
||||
export class LessonTimeDto {
|
||||
/**
|
||||
* Начало занятия
|
||||
* @example "2024-10-07T04:30:00.000Z"
|
||||
102
src/schedule/dto/lesson.dto.ts
Normal file
102
src/schedule/dto/lesson.dto.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import "reflect-metadata";
|
||||
|
||||
import { V2LessonType } from "../enum/v2-lesson-type.enum";
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { NullIf } from "../../utility/class-validators/conditional-field";
|
||||
import { LessonTimeDto } from "./lesson-time.dto";
|
||||
import { LessonSubGroupDto } from "./lesson-sub-group.dto";
|
||||
|
||||
export class LessonDto {
|
||||
/**
|
||||
* Тип занятия
|
||||
* @example DEFAULT
|
||||
*/
|
||||
@IsEnum(V2LessonType)
|
||||
@Transform(({ value, options }) => {
|
||||
if (options?.groups?.includes("v1")) {
|
||||
switch (value as V2LessonType) {
|
||||
case V2LessonType.CONSULTATION:
|
||||
case V2LessonType.INDEPENDENT_WORK:
|
||||
case V2LessonType.EXAM:
|
||||
case V2LessonType.EXAM_WITH_GRADE:
|
||||
return V2LessonType.DEFAULT;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
type: V2LessonType;
|
||||
|
||||
/**
|
||||
* Индексы пар, если присутствуют
|
||||
* @example [1, 3]
|
||||
* @optional
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
@IsOptional()
|
||||
@NullIf((self: LessonDto) => {
|
||||
return self.type !== V2LessonType.DEFAULT;
|
||||
})
|
||||
defaultRange: Array<number> | null;
|
||||
|
||||
/**
|
||||
* Название занятия
|
||||
* @example "Элементы высшей математики"
|
||||
* @optional
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@NullIf((self: LessonDto) => {
|
||||
return self.type === V2LessonType.BREAK;
|
||||
})
|
||||
@Transform(({ value, obj, options }) => {
|
||||
if (!value) return value;
|
||||
|
||||
if (options?.groups?.includes("v1")) {
|
||||
switch (obj.type as V2LessonType) {
|
||||
case V2LessonType.INDEPENDENT_WORK:
|
||||
return `Самостоятельная | ${value}`;
|
||||
case V2LessonType.CONSULTATION:
|
||||
return `Консультация | ${value}`;
|
||||
case V2LessonType.EXAM:
|
||||
return `ЗАЧЕТ | ${value}`;
|
||||
case V2LessonType.EXAM_WITH_GRADE:
|
||||
return `ЗАЧЕТ С ОЦЕНКОЙ | ${value}`;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
name: string | null;
|
||||
|
||||
/**
|
||||
* Начало и конец занятия
|
||||
*/
|
||||
@Type(() => LessonTimeDto)
|
||||
time: LessonTimeDto;
|
||||
|
||||
/**
|
||||
* Тип занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LessonSubGroupDto)
|
||||
@IsOptional()
|
||||
@NullIf((self: LessonDto) => {
|
||||
return self.type !== V2LessonType.DEFAULT;
|
||||
})
|
||||
subGroups: Array<LessonSubGroupDto> | null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsArray } from "class-validator";
|
||||
|
||||
export class V2ScheduleGroupNamesDto {
|
||||
export class ScheduleGroupNamesDto {
|
||||
/**
|
||||
* Группы
|
||||
* @example ["ИС-214/23", "ИС-213/23"]
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsArray } from "class-validator";
|
||||
|
||||
export class V2ScheduleTeacherNamesDto {
|
||||
export class ScheduleTeacherNamesDto {
|
||||
/**
|
||||
* Группы
|
||||
* @example ["Хомченко Н.Е."]
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IsArray, IsDate, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V2GroupDto } from "./v2-group.dto";
|
||||
import { GroupDto } from "./group.dto";
|
||||
import { ToMap } from "create-map-transform-fn";
|
||||
|
||||
export class V2ScheduleDto {
|
||||
export class ScheduleDto {
|
||||
/**
|
||||
* Дата когда последний раз расписание было скачано с сервера политехникума
|
||||
* @example "2024-10-18T21:50:06.680Z"
|
||||
@@ -13,10 +14,8 @@ export class V2ScheduleDto {
|
||||
/**
|
||||
* Расписание групп
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => V2GroupDto)
|
||||
groups: Array<V2GroupDto>;
|
||||
@ToMap({ mapValueClass: GroupDto })
|
||||
groups: Map<string, GroupDto>;
|
||||
|
||||
/**
|
||||
* Обновлённые дни с последнего изменения расписания
|
||||
15
src/schedule/dto/teacher-day.dto.ts
Normal file
15
src/schedule/dto/teacher-day.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { DayDto } from "./day.dto";
|
||||
import { IsArray, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
import { TeacherLessonDto } from "./teacher-lesson.dto";
|
||||
|
||||
export class TeacherDayDto extends OmitType(DayDto, ["lessons"]) {
|
||||
/**
|
||||
* Занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TeacherLessonDto)
|
||||
lessons: Array<TeacherLessonDto>;
|
||||
}
|
||||
18
src/schedule/dto/teacher-lesson.dto.ts
Normal file
18
src/schedule/dto/teacher-lesson.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { LessonDto } from "./lesson.dto";
|
||||
import { IsOptional, IsString } from "class-validator";
|
||||
import { NullIf } from "../../utility/class-validators/conditional-field";
|
||||
import { V2LessonType } from "../enum/v2-lesson-type.enum";
|
||||
|
||||
export class TeacherLessonDto extends LessonDto {
|
||||
/**
|
||||
* Название группы
|
||||
* @example "ИС-214/23"
|
||||
* @optional
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@NullIf((self: TeacherLessonDto) => {
|
||||
return self.type === V2LessonType.BREAK;
|
||||
})
|
||||
group: string | null;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { V2ScheduleDto } from "./v2-schedule.dto";
|
||||
import { ScheduleDto } from "./schedule.dto";
|
||||
import { IsArray, IsObject, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V2TeacherDto } from "./v2-teacher.dto";
|
||||
import { TeacherDto } from "./teacher.dto";
|
||||
|
||||
export class V2TeacherScheduleDto extends PickType(V2ScheduleDto, [
|
||||
export class TeacherScheduleDto extends PickType(ScheduleDto, [
|
||||
"updatedAt",
|
||||
]) {
|
||||
/**
|
||||
* Расписание преподавателя
|
||||
*/
|
||||
@IsObject()
|
||||
teacher: V2TeacherDto;
|
||||
teacher: TeacherDto;
|
||||
|
||||
/**
|
||||
* Обновлённые дни с последнего изменения расписания
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IsArray, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V2TeacherDayDto } from "./v2-teacher-day.dto";
|
||||
import { TeacherDayDto } from "./teacher-day.dto";
|
||||
|
||||
export class V2TeacherDto {
|
||||
export class TeacherDto {
|
||||
/**
|
||||
* ФИО преподавателя
|
||||
* @example "Хомченко Н.Е."
|
||||
@@ -15,6 +15,6 @@ export class V2TeacherDto {
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => V2TeacherDayDto)
|
||||
days: Array<V2TeacherDayDto>;
|
||||
@Type(() => TeacherDayDto)
|
||||
days: Array<TeacherDayDto>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsUrl } from "class-validator";
|
||||
|
||||
export class V2UpdateDownloadUrlDto {
|
||||
export class UpdateDownloadUrlDto {
|
||||
/**
|
||||
* Прямая ссылка на скачивание расписания
|
||||
* @example "https://politehnikum-eng.ru/2024/poltavskaja_07_s_14_po_20_10-5-.xls"
|
||||
@@ -1,19 +0,0 @@
|
||||
import { V2CacheStatusDto } from "../v2/v2-cache-status.dto";
|
||||
import { instanceToPlain, plainToClass } from "class-transformer";
|
||||
import { V1CacheStatusDto } from "./v1-cache-status.dto";
|
||||
|
||||
export class CacheStatusDto extends V2CacheStatusDto {
|
||||
public static stripVersion(instance: CacheStatusDto, version: number) {
|
||||
switch (version) {
|
||||
default:
|
||||
return instance;
|
||||
case 0: {
|
||||
return plainToClass(
|
||||
V1CacheStatusDto,
|
||||
instanceToPlain(instance),
|
||||
{ excludeExtraneousValues: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsHash } from "class-validator";
|
||||
import { Expose } from "class-transformer";
|
||||
|
||||
export class V1CacheStatusDto {
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
description: "Нужно ли обновить ссылку для скачивания xls?",
|
||||
})
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
cacheUpdateRequired: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: "e6ff169b01608addf998dbf8f40b019a7f514239",
|
||||
description: "Хеш последних полученных данных",
|
||||
})
|
||||
@IsHash("sha1")
|
||||
@Expose()
|
||||
cacheHash: string;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V1LessonDto } from "./v1-lesson.dto";
|
||||
import { V1LessonType } from "../../enum/v1-lesson-type.enum";
|
||||
|
||||
export class V1DayDto {
|
||||
@ApiProperty({
|
||||
example: "Понедельник",
|
||||
description: "День недели",
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ example: [0, 1, 3], description: "Индексы занятий" })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
nonNullIndices: Array<number>;
|
||||
|
||||
@ApiProperty({ example: [1, 3], description: "Индексы полных пар" })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
defaultIndices: Array<number>;
|
||||
|
||||
@ApiProperty({ example: [0], description: "Индексы доп. занятий" })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
customIndices: Array<number>;
|
||||
|
||||
@ApiProperty({ example: [], description: "Занятия" })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@IsOptional()
|
||||
@Type(() => V1LessonDto)
|
||||
lessons: Array<V1LessonDto | null>;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
|
||||
this.nonNullIndices = [];
|
||||
this.defaultIndices = [];
|
||||
this.customIndices = [];
|
||||
|
||||
this.lessons = [];
|
||||
}
|
||||
|
||||
public fillIndices(): void {
|
||||
this.nonNullIndices = [];
|
||||
this.defaultIndices = [];
|
||||
this.customIndices = [];
|
||||
|
||||
for (const lessonRawIdx in this.lessons) {
|
||||
const lessonIdx = Number.parseInt(lessonRawIdx);
|
||||
|
||||
const lesson = this.lessons[lessonIdx];
|
||||
if (lesson === null) continue;
|
||||
|
||||
this.nonNullIndices.push(lessonIdx);
|
||||
|
||||
(lesson.type === V1LessonType.DEFAULT
|
||||
? this.defaultIndices
|
||||
: this.customIndices
|
||||
).push(lessonIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { PartialType, PickType } from "@nestjs/swagger";
|
||||
import { V1GroupDto } from "./v1-group.dto";
|
||||
|
||||
export class V1GroupScheduleNameDto extends PartialType(
|
||||
PickType(V1GroupDto, ["name"]),
|
||||
) {}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ApiProperty, OmitType } from "@nestjs/swagger";
|
||||
import { IsArray, IsObject, ValidateNested } from "class-validator";
|
||||
import { V1GroupDto } from "./v1-group.dto";
|
||||
import { Type } from "class-transformer";
|
||||
import { V1ScheduleDto } from "./v1-schedule.dto";
|
||||
|
||||
export class V1GroupScheduleDto extends OmitType(V1ScheduleDto, [
|
||||
"groups",
|
||||
"lastChangedDays",
|
||||
]) {
|
||||
@ApiProperty({ description: "Расписание группы" })
|
||||
@IsObject()
|
||||
group: V1GroupDto;
|
||||
|
||||
@ApiProperty({
|
||||
example: [5, 6],
|
||||
description: "Обновлённые дни с последнего изменения расписания",
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
lastChangedDays: Array<number>;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V1DayDto } from "./v1-day.dto";
|
||||
|
||||
export class V1GroupDto {
|
||||
@ApiProperty({
|
||||
example: "ИС-214/23",
|
||||
description: "Название группы",
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ example: [], description: "Дни недели" })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@IsOptional()
|
||||
@Type(() => V1DayDto)
|
||||
days: Array<V1DayDto | null>;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.days = [];
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNumber } from "class-validator";
|
||||
|
||||
export class V1LessonTimeDto {
|
||||
@ApiProperty({
|
||||
example: 0,
|
||||
description: "Начало занятия в минутах относительно начала суток",
|
||||
})
|
||||
@IsNumber()
|
||||
start: number;
|
||||
@ApiProperty({
|
||||
example: 60,
|
||||
description: "Конец занятия в минутах относительно начала суток",
|
||||
})
|
||||
@IsNumber()
|
||||
end: number;
|
||||
|
||||
constructor(start: number, end: number) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
static fromString(time: string): V1LessonTimeDto {
|
||||
time = time.trim().replaceAll(".", ":");
|
||||
|
||||
const regex = /(\d+:\d+)-(\d+:\d+)/g;
|
||||
|
||||
const parseResult = regex.exec(time);
|
||||
if (!parseResult) return new V1LessonTimeDto(0, 0);
|
||||
|
||||
const start = parseResult[1].split(":");
|
||||
const end = parseResult[2].split(":");
|
||||
|
||||
return new V1LessonTimeDto(
|
||||
Number.parseInt(start[0]) * 60 + Number.parseInt(start[1]),
|
||||
Number.parseInt(end[0]) * 60 + Number.parseInt(end[1]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { V1LessonType } from "../../enum/v1-lesson-type.enum";
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { V1LessonTimeDto } from "./v1-lesson-time.dto";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class V1LessonDto {
|
||||
@ApiProperty({
|
||||
example: V1LessonType.DEFAULT,
|
||||
description: "Тип занятия",
|
||||
})
|
||||
@IsEnum(V1LessonType)
|
||||
type: V1LessonType;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: "Индекс пары, если присутствует",
|
||||
})
|
||||
@IsNumber()
|
||||
defaultIndex: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: "Элементы высшей математики",
|
||||
description: "Название занятия",
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: new V1LessonTimeDto(0, 60),
|
||||
description:
|
||||
"Начало и конец занятия в минутах относительно начала суток",
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => V1LessonTimeDto)
|
||||
time: V1LessonTimeDto | null;
|
||||
|
||||
@ApiProperty({ example: ["42", "с\\з"], description: "Кабинеты" })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => String)
|
||||
cabinets: Array<string>;
|
||||
|
||||
@ApiProperty({
|
||||
example: ["Хомченко Н.Е."],
|
||||
description: "ФИО преподавателей",
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => String)
|
||||
teacherNames: Array<string>;
|
||||
|
||||
constructor(
|
||||
type: V1LessonType,
|
||||
defaultIndex: number,
|
||||
time: V1LessonTimeDto,
|
||||
name: string,
|
||||
cabinets: Array<string>,
|
||||
teacherNames: Array<string>,
|
||||
) {
|
||||
this.type = type;
|
||||
this.defaultIndex = defaultIndex;
|
||||
this.time = time;
|
||||
this.name = name;
|
||||
this.cabinets = cabinets;
|
||||
this.teacherNames = teacherNames;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsArray } from "class-validator";
|
||||
|
||||
export class V1ScheduleGroupNamesDto {
|
||||
@ApiProperty({
|
||||
example: ["ИС-214/23", "ИС-213/23"],
|
||||
description: "Список названий всех групп в текущем расписании",
|
||||
})
|
||||
@IsArray()
|
||||
names: Array<string>;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { IsDate, IsObject, IsOptional } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
|
||||
export class V1ScheduleDto {
|
||||
@ApiProperty({
|
||||
example: new Date(),
|
||||
description:
|
||||
"Дата когда последний раз расписание было скачано с сервера политехникума",
|
||||
})
|
||||
@IsDate()
|
||||
updatedAt: Date;
|
||||
|
||||
@ApiProperty({ description: "Расписание групп" })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
groups: any;
|
||||
|
||||
@ApiProperty({
|
||||
example: { "ИС-214/23": [5, 6] },
|
||||
description: "Обновлённые дни с последнего изменения расписания",
|
||||
})
|
||||
@IsObject()
|
||||
@Type(() => Object)
|
||||
@Transform(({ value }) => {
|
||||
const object = {};
|
||||
|
||||
for (const key in value) {
|
||||
object[key] = value[key];
|
||||
}
|
||||
|
||||
return object;
|
||||
})
|
||||
@Type(() => Object)
|
||||
lastChangedDays: Array<Array<number>>;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsBase64 } from "class-validator";
|
||||
|
||||
export class V1SiteMainPageDto {
|
||||
@ApiProperty({
|
||||
example: "MHz=",
|
||||
description: "Страница политехникума",
|
||||
})
|
||||
@IsBase64()
|
||||
mainPage: string;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { V1CacheStatusDto } from "../v1/v1-cache-status.dto";
|
||||
import { IsNumber } from "class-validator";
|
||||
|
||||
export class V2CacheStatusDto extends V1CacheStatusDto {
|
||||
/**
|
||||
* Дата обновления кеша
|
||||
* @example 1729288173002
|
||||
*/
|
||||
@IsNumber()
|
||||
lastCacheUpdate: number;
|
||||
|
||||
/**
|
||||
* Дата обновления расписания
|
||||
* @example 1729288173002
|
||||
*/
|
||||
@IsNumber()
|
||||
lastScheduleUpdate: number;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { V2LessonDto } from "./v2-lesson.dto";
|
||||
|
||||
export class V2DayDto {
|
||||
/**
|
||||
* День недели
|
||||
* @example "Понедельник"
|
||||
*/
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Дата
|
||||
* @example "2024-10-06T20:00:00.000Z"
|
||||
*/
|
||||
@IsDateString()
|
||||
date: Date;
|
||||
|
||||
/**
|
||||
* Занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => V2LessonDto)
|
||||
lessons: Array<V2LessonDto>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { PartialType, PickType } from "@nestjs/swagger";
|
||||
import { V1GroupDto } from "../v1/v1-group.dto";
|
||||
|
||||
export class V2GroupScheduleByNameDto extends PartialType(
|
||||
PickType(V1GroupDto, ["name"]),
|
||||
) {}
|
||||
@@ -1,67 +0,0 @@
|
||||
import "reflect-metadata";
|
||||
|
||||
import { V2LessonType } from "../../enum/v2-lesson-type.enum";
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { NullIf } from "../../../utility/class-validators/conditional-field";
|
||||
import { V2LessonTimeDto } from "./v2-lesson-time.dto";
|
||||
import { V2LessonSubGroupDto } from "./v2-lesson-sub-group.dto";
|
||||
|
||||
export class V2LessonDto {
|
||||
/**
|
||||
* Тип занятия
|
||||
* @example DEFAULT
|
||||
*/
|
||||
@IsEnum(V2LessonType)
|
||||
type: V2LessonType;
|
||||
|
||||
/**
|
||||
* Индексы пар, если присутствуют
|
||||
* @example [1, 3]
|
||||
* @optional
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
@IsOptional()
|
||||
@NullIf((self: V2LessonDto) => {
|
||||
return self.type !== V2LessonType.DEFAULT;
|
||||
})
|
||||
defaultRange: Array<number> | null;
|
||||
|
||||
/**
|
||||
* Название занятия
|
||||
* @example "Элементы высшей математики"
|
||||
* @optional
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@NullIf((self: V2LessonDto) => {
|
||||
return self.type === V2LessonType.BREAK;
|
||||
})
|
||||
name: string | null;
|
||||
|
||||
/**
|
||||
* Начало и конец занятия
|
||||
*/
|
||||
@Type(() => V2LessonTimeDto)
|
||||
time: V2LessonTimeDto;
|
||||
|
||||
/**
|
||||
* Тип занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => V2LessonSubGroupDto)
|
||||
@IsOptional()
|
||||
@NullIf((self: V2LessonDto) => {
|
||||
return self.type !== V2LessonType.DEFAULT;
|
||||
})
|
||||
subGroups: Array<V2LessonSubGroupDto> | null;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { V2DayDto } from "./v2-day.dto";
|
||||
import { IsArray, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
import { V2TeacherLessonDto } from "./v2-teacher-lesson.dto";
|
||||
|
||||
export class V2TeacherDayDto extends OmitType(V2DayDto, ["lessons"]) {
|
||||
/**
|
||||
* Занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => V2TeacherLessonDto)
|
||||
lessons: Array<V2TeacherLessonDto>;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { V2LessonDto } from "./v2-lesson.dto";
|
||||
import { IsOptional, IsString } from "class-validator";
|
||||
import { NullIf } from "../../../utility/class-validators/conditional-field";
|
||||
import { V2LessonType } from "../../enum/v2-lesson-type.enum";
|
||||
|
||||
export class V2TeacherLessonDto extends V2LessonDto {
|
||||
/**
|
||||
* Название группы
|
||||
* @example "ИС-214/23"
|
||||
* @optional
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@NullIf((self: V2TeacherLessonDto) => {
|
||||
return self.type === V2LessonType.BREAK;
|
||||
})
|
||||
group: string | null;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum V1LessonType {
|
||||
DEFAULT = 0,
|
||||
CUSTOM,
|
||||
}
|
||||
@@ -2,4 +2,8 @@ export enum V2LessonType {
|
||||
DEFAULT = 0,
|
||||
ADDITIONAL,
|
||||
BREAK,
|
||||
CONSULTATION,
|
||||
INDEPENDENT_WORK,
|
||||
EXAM,
|
||||
EXAM_WITH_GRADE,
|
||||
}
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
import { XlsDownloaderInterface } from "../xls-downloader/xls-downloader.interface";
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
import { toNormalString, trimAll } from "../../../utility/string.util";
|
||||
import { V1LessonTimeDto } from "../../dto/v1/v1-lesson-time.dto";
|
||||
import { V1LessonType } from "../../enum/v1-lesson-type.enum";
|
||||
import { V1LessonDto } from "../../dto/v1/v1-lesson.dto";
|
||||
import { V1DayDto } from "../../dto/v1/v1-day.dto";
|
||||
import { V1GroupDto } from "../../dto/v1/v1-group.dto";
|
||||
import { ScheduleReplacerService } from "../../schedule-replacer.service";
|
||||
import * as assert from "node:assert";
|
||||
|
||||
type InternalId = { row: number; column: number; name: string };
|
||||
type InternalDay = InternalId;
|
||||
|
||||
export class ScheduleParseResult {
|
||||
etag: string;
|
||||
replacerId?: string;
|
||||
groups: Array<V1GroupDto>;
|
||||
affectedDays: Array<Array<number>>;
|
||||
}
|
||||
|
||||
type CellData = XLSX.CellObject["v"];
|
||||
|
||||
export class V1ScheduleParser {
|
||||
private lastResult: ScheduleParseResult | null = null;
|
||||
|
||||
public constructor(
|
||||
private readonly xlsDownloader: XlsDownloaderInterface,
|
||||
private readonly scheduleReplacerService: ScheduleReplacerService,
|
||||
) {}
|
||||
|
||||
private static getCellData(
|
||||
worksheet: XLSX.Sheet,
|
||||
row: number,
|
||||
column: number,
|
||||
): string | null {
|
||||
const cell: XLSX.CellObject | null =
|
||||
worksheet[XLSX.utils.encode_cell({ r: row, c: column })];
|
||||
|
||||
return toNormalString(cell?.w);
|
||||
}
|
||||
|
||||
private parseTeacherFullNames(lessonName: string): {
|
||||
name: string;
|
||||
teacherFullNames: Array<string>;
|
||||
} {
|
||||
const firstRegex =
|
||||
/(?:[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.[А-ЯЁ]\.(?:\s?\([0-9] подгруппа\))?(?:,\s)?)+$/gm;
|
||||
const secondRegex =
|
||||
/(?:[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.[А-ЯЁ]\.(?:\s?\([0-9] подгруппа\))?)+/gm;
|
||||
|
||||
const fm = firstRegex.exec(lessonName);
|
||||
if (fm === null) return { name: lessonName, teacherFullNames: [] };
|
||||
|
||||
const teacherFullNames: Array<string> = [];
|
||||
|
||||
let teacherFullNameMatch: RegExpExecArray;
|
||||
while ((teacherFullNameMatch = secondRegex.exec(fm[0])) !== null) {
|
||||
if (teacherFullNameMatch.index === secondRegex.lastIndex)
|
||||
secondRegex.lastIndex++;
|
||||
|
||||
teacherFullNames.push(teacherFullNameMatch[0].trim());
|
||||
}
|
||||
|
||||
if (teacherFullNames.length === 0)
|
||||
return { name: lessonName, teacherFullNames: [] };
|
||||
|
||||
return {
|
||||
name: lessonName.substring(0, fm.index).trim(),
|
||||
teacherFullNames: teacherFullNames,
|
||||
};
|
||||
}
|
||||
|
||||
parseSkeleton(worksheet: XLSX.Sheet): {
|
||||
groupSkeletons: Array<InternalId>;
|
||||
daySkeletons: Array<InternalDay>;
|
||||
} {
|
||||
const range = XLSX.utils.decode_range(worksheet["!ref"] || "");
|
||||
let isHeaderParsed: boolean = false;
|
||||
|
||||
const groups: Array<InternalId> = [];
|
||||
const days: Array<InternalDay> = [];
|
||||
|
||||
for (let row = range.s.r + 1; row <= range.e.r; ++row) {
|
||||
const dayName = V1ScheduleParser.getCellData(worksheet, row, 0);
|
||||
if (!dayName) continue;
|
||||
|
||||
if (!isHeaderParsed) {
|
||||
isHeaderParsed = true;
|
||||
|
||||
--row;
|
||||
for (
|
||||
let column = range.s.c + 2;
|
||||
column <= range.e.c;
|
||||
++column
|
||||
) {
|
||||
const groupName = V1ScheduleParser.getCellData(
|
||||
worksheet,
|
||||
row,
|
||||
column,
|
||||
);
|
||||
if (!groupName) continue;
|
||||
|
||||
groups.push({ row: row, column: column, name: groupName });
|
||||
}
|
||||
++row;
|
||||
}
|
||||
|
||||
if (
|
||||
days.length == 0 ||
|
||||
!days[days.length - 1].name.startsWith("Суббота")
|
||||
) {
|
||||
const dayMonthIdx = /[А-Яа-я]+\s(\d+)\.\d+\.\d+/.exec(
|
||||
trimAll(dayName),
|
||||
);
|
||||
|
||||
if (dayMonthIdx === null) continue;
|
||||
}
|
||||
|
||||
days.push({
|
||||
row: row,
|
||||
column: 0,
|
||||
name: dayName,
|
||||
});
|
||||
|
||||
if (
|
||||
days.length > 2 &&
|
||||
days[days.length - 2].name.startsWith("Суббота")
|
||||
)
|
||||
break;
|
||||
}
|
||||
|
||||
return { daySkeletons: days, groupSkeletons: groups };
|
||||
}
|
||||
|
||||
getXlsDownloader(): XlsDownloaderInterface {
|
||||
return this.xlsDownloader;
|
||||
}
|
||||
|
||||
async getSchedule(): Promise<ScheduleParseResult> {
|
||||
const headData = await this.xlsDownloader.fetch(true);
|
||||
this.xlsDownloader.verifyFetchResult(headData);
|
||||
|
||||
assert(headData.type === "success");
|
||||
|
||||
const replacer = await this.scheduleReplacerService.getByEtag(
|
||||
headData.etag,
|
||||
);
|
||||
|
||||
if (this.lastResult && this.lastResult.etag === headData.etag) {
|
||||
if (!replacer) return this.lastResult;
|
||||
|
||||
if (this.lastResult.replacerId === replacer.id)
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
const buffer = async () => {
|
||||
if (replacer) return replacer.data;
|
||||
|
||||
const downloadData = await this.xlsDownloader.fetch(false);
|
||||
this.xlsDownloader.verifyFetchResult(downloadData);
|
||||
|
||||
assert(downloadData.type === "success");
|
||||
|
||||
return downloadData.data;
|
||||
};
|
||||
|
||||
const workBook = XLSX.read(await buffer());
|
||||
const workSheet = workBook.Sheets[workBook.SheetNames[0]];
|
||||
|
||||
const { groupSkeletons, daySkeletons } = this.parseSkeleton(workSheet);
|
||||
|
||||
const groups: Array<V1GroupDto> = [];
|
||||
|
||||
for (const groupSkeleton of groupSkeletons) {
|
||||
const group = new V1GroupDto(groupSkeleton.name);
|
||||
|
||||
for (let dayIdx = 0; dayIdx < daySkeletons.length - 1; ++dayIdx) {
|
||||
const daySkeleton = daySkeletons[dayIdx];
|
||||
const day = new V1DayDto(daySkeleton.name);
|
||||
|
||||
const lessonTimeColumn = daySkeletons[0].column + 1;
|
||||
const rowDistance =
|
||||
daySkeletons[dayIdx + 1].row - daySkeleton.row;
|
||||
|
||||
for (
|
||||
let row = daySkeleton.row;
|
||||
row < daySkeleton.row + rowDistance;
|
||||
++row
|
||||
) {
|
||||
// time
|
||||
const time = V1ScheduleParser.getCellData(
|
||||
workSheet,
|
||||
row,
|
||||
lessonTimeColumn,
|
||||
)?.replaceAll(" ", "");
|
||||
|
||||
if (!time) continue;
|
||||
|
||||
// name
|
||||
const rawName: CellData = trimAll(
|
||||
V1ScheduleParser.getCellData(
|
||||
workSheet,
|
||||
row,
|
||||
groupSkeleton.column,
|
||||
)?.replaceAll(/[\n\r]/g, "") ?? "",
|
||||
);
|
||||
|
||||
if (rawName.length === 0) {
|
||||
day.lessons.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
// cabinets
|
||||
const cabinets: Array<string> = [];
|
||||
|
||||
const rawCabinets = V1ScheduleParser.getCellData(
|
||||
workSheet,
|
||||
row,
|
||||
groupSkeleton.column + 1,
|
||||
);
|
||||
|
||||
if (rawCabinets) {
|
||||
const parts = rawCabinets.split(/(\n|\s)/g);
|
||||
|
||||
for (const cabinet of parts) {
|
||||
if (!toNormalString(cabinet)) continue;
|
||||
|
||||
cabinets.push(cabinet.replaceAll(/[\n\s\r]/g, " "));
|
||||
}
|
||||
}
|
||||
|
||||
// type
|
||||
const lessonType = time?.includes("пара")
|
||||
? V1LessonType.DEFAULT
|
||||
: V1LessonType.CUSTOM;
|
||||
|
||||
// full names
|
||||
const { name, teacherFullNames } =
|
||||
this.parseTeacherFullNames(
|
||||
trimAll(rawName?.replaceAll(/[\n\r]/g, "") ?? ""),
|
||||
);
|
||||
|
||||
day.lessons.push(
|
||||
new V1LessonDto(
|
||||
lessonType,
|
||||
lessonType === V1LessonType.DEFAULT
|
||||
? Number.parseInt(time[0])
|
||||
: -1,
|
||||
V1LessonTimeDto.fromString(
|
||||
lessonType === V1LessonType.DEFAULT
|
||||
? time.substring(5)
|
||||
: time,
|
||||
),
|
||||
name,
|
||||
cabinets,
|
||||
teacherFullNames,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
day.fillIndices();
|
||||
|
||||
if (day.nonNullIndices.length == 0) group.days.push(null);
|
||||
else group.days.push(day);
|
||||
}
|
||||
|
||||
groups[group.name] = group;
|
||||
}
|
||||
|
||||
return (this.lastResult = {
|
||||
etag: headData.etag,
|
||||
replacerId: replacer?.id,
|
||||
groups: groups,
|
||||
affectedDays: this.getAffectedDays(this.lastResult?.groups, groups),
|
||||
});
|
||||
}
|
||||
|
||||
private getAffectedDays(
|
||||
cachedGroups: Array<V1GroupDto> | null,
|
||||
groups: Array<V1GroupDto>,
|
||||
): Array<Array<number>> {
|
||||
const affectedDays: Array<Array<number>> = [];
|
||||
|
||||
if (!cachedGroups) return affectedDays;
|
||||
|
||||
// noinspection SpellCheckingInspection
|
||||
const dayEquals = (
|
||||
lday: V1DayDto | null,
|
||||
rday: V1DayDto | undefined,
|
||||
): boolean => {
|
||||
if (!lday || !rday || rday.lessons.length != lday.lessons.length)
|
||||
return false;
|
||||
|
||||
for (const lessonIdx in lday.lessons) {
|
||||
// noinspection SpellCheckingInspection
|
||||
const llesson = lday.lessons[lessonIdx];
|
||||
// noinspection SpellCheckingInspection
|
||||
const rlesson = rday.lessons[lessonIdx];
|
||||
|
||||
if (llesson === null && rlesson === null) continue;
|
||||
if (!llesson || !rlesson) return false;
|
||||
|
||||
if (
|
||||
llesson.name.length > 0 &&
|
||||
(llesson.name !== rlesson.name ||
|
||||
llesson.time.start !== rlesson.time.start ||
|
||||
llesson.time.end !== rlesson.time.end ||
|
||||
llesson.cabinets.toString() !==
|
||||
rlesson.cabinets.toString() ||
|
||||
llesson.teacherNames.toString() !==
|
||||
rlesson.teacherNames.toString())
|
||||
)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const groupName in cachedGroups) {
|
||||
const cachedGroup = cachedGroups[groupName];
|
||||
const group = groups[groupName];
|
||||
|
||||
const affectedGroupDays: Array<number> = [];
|
||||
|
||||
for (const dayIdx in group.days) {
|
||||
// noinspection SpellCheckingInspection
|
||||
const lday = group.days[dayIdx];
|
||||
// noinspection SpellCheckingInspection
|
||||
const rday = cachedGroup.days[dayIdx];
|
||||
|
||||
if (!dayEquals(lday, rday))
|
||||
affectedGroupDays.push(Number.parseInt(dayIdx));
|
||||
}
|
||||
|
||||
affectedDays[groupName] = affectedGroupDays;
|
||||
}
|
||||
|
||||
return affectedDays;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { V2ScheduleParser } from "./v2-schedule-parser";
|
||||
import { V2ScheduleParser, V2ScheduleParseResult } from "./v2-schedule-parser";
|
||||
import { BasicXlsDownloader } from "../xls-downloader/basic-xls-downloader";
|
||||
import { V2DayDto } from "../../dto/v2/v2-day.dto";
|
||||
import { V2GroupDto } from "../../dto/v2/v2-group.dto";
|
||||
import { DayDto } from "../../dto/day.dto";
|
||||
import { GroupDto } from "../../dto/group.dto";
|
||||
import { V2LessonType } from "../../enum/v2-lesson-type.enum";
|
||||
import instanceToInstance2 from "../../../utility/class-trasformer/instance-to-instance-2";
|
||||
|
||||
describe("V2ScheduleParser", () => {
|
||||
let parser: V2ScheduleParser;
|
||||
@@ -31,85 +33,87 @@ describe("V2ScheduleParser", () => {
|
||||
const schedule = await parser.getSchedule();
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: V2GroupDto | undefined = schedule.groups["ИС-214/23"];
|
||||
const group: GroupDto | undefined = schedule.groups.get("ИС-214/23");
|
||||
expect(group).toBeDefined();
|
||||
|
||||
const saturday: V2DayDto = group.days[5];
|
||||
expect(saturday).toBeDefined();
|
||||
const monday: DayDto = group.days[0];
|
||||
expect(monday).toBeDefined();
|
||||
|
||||
const name = saturday.name;
|
||||
const name = monday.name;
|
||||
expect(name).toBeDefined();
|
||||
expect(name.length).toBeGreaterThan(0);
|
||||
};
|
||||
//
|
||||
// function mapReplacer(key: any, value: any) {
|
||||
// if (value instanceof Map) {
|
||||
// return Array.from(value.entries());
|
||||
// } else {
|
||||
// return value;
|
||||
// }
|
||||
// }
|
||||
|
||||
describe("Расписание", () => {
|
||||
beforeEach(async () => {
|
||||
await setLink(
|
||||
"https://politehnikum-eng.ru/2024/poltavskaja_11_s_11_11_po_17_11-5-.xls",
|
||||
"https://politehnikum-eng.ru/2024/poltavskaja_12_s_18_po_24_11.xls",
|
||||
);
|
||||
});
|
||||
|
||||
it("Должен вернуть расписание", defaultTest);
|
||||
it("Название дня не должно быть пустым или null", nameTest);
|
||||
|
||||
it("Парсер должен вернуть корректное время если она на нескольких линиях", async () => {
|
||||
const schedule = await parser.getSchedule();
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: V2GroupDto | undefined = schedule.groups["ИС-214/23"];
|
||||
expect(group).toBeDefined();
|
||||
|
||||
const saturday: V2DayDto = group.days[5];
|
||||
expect(saturday).toBeDefined();
|
||||
|
||||
const firstLesson = saturday.lessons[0];
|
||||
expect(firstLesson).toBeDefined();
|
||||
|
||||
expect(firstLesson.time).toBeDefined();
|
||||
|
||||
expect(firstLesson.time.start).toBeDefined();
|
||||
expect(firstLesson.time.end).toBeDefined();
|
||||
|
||||
const startMinutes =
|
||||
firstLesson.time.start.getHours() * 60 +
|
||||
firstLesson.time.start.getMinutes();
|
||||
const endMinutes =
|
||||
firstLesson.time.end.getHours() * 60 +
|
||||
firstLesson.time.end.getMinutes();
|
||||
|
||||
const differenceMinutes = endMinutes - startMinutes;
|
||||
|
||||
expect(differenceMinutes).toBe(190);
|
||||
|
||||
expect(firstLesson.defaultRange).toStrictEqual([1, 3]);
|
||||
});
|
||||
|
||||
it("Ошибка парсинга?", async () => {
|
||||
const schedule = await parser.getSchedule();
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: V2GroupDto | undefined = schedule.groups["ИС-214/23"];
|
||||
expect(group).toBeDefined();
|
||||
|
||||
const thursday: V2DayDto = group.days[3];
|
||||
expect(thursday).toBeDefined();
|
||||
|
||||
expect(thursday.lessons.length).toBe(5);
|
||||
|
||||
const lastLessonName = thursday.lessons[4].name;
|
||||
expect(lastLessonName).toBe(
|
||||
"МДК.05.01 Проектирование и дизайн информационных систем",
|
||||
it("Зачёт с оценкой v1", async () => {
|
||||
const schedule = await parser.getSchedule().then((v) =>
|
||||
instanceToInstance2(V2ScheduleParseResult, v, {
|
||||
groups: ["v1"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("Суббота не должна отсутствовать", async () => {
|
||||
const schedule = await parser.getSchedule();
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: V2GroupDto | undefined = schedule.groups["ИС-214/23"];
|
||||
const group: GroupDto | undefined =
|
||||
schedule.groups.get("ИС-214/23");
|
||||
expect(group).toBeDefined();
|
||||
|
||||
expect(group.days.length).toBe(6);
|
||||
const tuesday = group.days[1];
|
||||
expect(tuesday).toBeDefined();
|
||||
|
||||
const oseLesson = tuesday.lessons[6];
|
||||
expect(oseLesson).toBeDefined();
|
||||
|
||||
expect(oseLesson.name.startsWith("ЗАЧЕТ С ОЦЕНКОЙ | ")).toBe(true);
|
||||
expect(oseLesson.type).toBe(V2LessonType.DEFAULT);
|
||||
});
|
||||
|
||||
it("Зачёт с оценкой v2", async () => {
|
||||
const schedule = await parser.getSchedule().then((v) =>
|
||||
instanceToInstance2(V2ScheduleParseResult, v, {
|
||||
groups: ["v2"],
|
||||
}),
|
||||
);
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: GroupDto | undefined =
|
||||
schedule.groups.get("ИС-214/23");
|
||||
expect(group).toBeDefined();
|
||||
|
||||
const tuesday = group.days[1];
|
||||
expect(tuesday).toBeDefined();
|
||||
|
||||
const oseLesson = tuesday.lessons[6];
|
||||
expect(oseLesson).toBeDefined();
|
||||
|
||||
expect(oseLesson.name.startsWith("Операционные")).toBe(true);
|
||||
expect(oseLesson.type).toBe(V2LessonType.EXAM_WITH_GRADE);
|
||||
});
|
||||
|
||||
// it("Суббота не должна отсутствовать", async () => {
|
||||
// const schedule = await parser.getSchedule();
|
||||
// expect(schedule).toBeDefined();
|
||||
//
|
||||
// const group: V2GroupDto | undefined = schedule.groups["ИС-214/23"];
|
||||
// expect(group).toBeDefined();
|
||||
//
|
||||
// expect(group.days.length).toBe(6);
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,27 @@ import { XlsDownloaderInterface } from "../xls-downloader/xls-downloader.interfa
|
||||
import * as XLSX from "xlsx";
|
||||
import { Range, WorkSheet } from "xlsx";
|
||||
import { toNormalString, trimAll } from "../../../utility/string.util";
|
||||
import { plainToClass, plainToInstance } from "class-transformer";
|
||||
import { plainToClass, plainToInstance, Type } from "class-transformer";
|
||||
import * as objectHash from "object-hash";
|
||||
import { V2LessonTimeDto } from "../../dto/v2/v2-lesson-time.dto";
|
||||
import { LessonTimeDto } from "../../dto/lesson-time.dto";
|
||||
import { V2LessonType } from "../../enum/v2-lesson-type.enum";
|
||||
import { V2LessonSubGroupDto } from "../../dto/v2/v2-lesson-sub-group.dto";
|
||||
import { V2LessonDto } from "../../dto/v2/v2-lesson.dto";
|
||||
import { V2DayDto } from "../../dto/v2/v2-day.dto";
|
||||
import { V2GroupDto } from "../../dto/v2/v2-group.dto";
|
||||
import { LessonSubGroupDto } from "../../dto/lesson-sub-group.dto";
|
||||
import { LessonDto } from "../../dto/lesson.dto";
|
||||
import { DayDto } from "../../dto/day.dto";
|
||||
import { GroupDto } from "../../dto/group.dto";
|
||||
import * as assert from "node:assert";
|
||||
import { ScheduleReplacerService } from "../../schedule-replacer.service";
|
||||
import { V2TeacherDto } from "../../dto/v2/v2-teacher.dto";
|
||||
import { V2TeacherDayDto } from "../../dto/v2/v2-teacher-day.dto";
|
||||
import { V2TeacherLessonDto } from "../../dto/v2/v2-teacher-lesson.dto";
|
||||
import { TeacherDto } from "../../dto/teacher.dto";
|
||||
import { TeacherDayDto } from "../../dto/teacher-day.dto";
|
||||
import { TeacherLessonDto } from "../../dto/teacher-lesson.dto";
|
||||
import {
|
||||
IsArray,
|
||||
IsDate,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { ToMap } from "create-map-transform-fn";
|
||||
|
||||
type InternalId = {
|
||||
/**
|
||||
@@ -38,7 +46,7 @@ type InternalTime = {
|
||||
/**
|
||||
* Временной отрезок
|
||||
*/
|
||||
timeRange: V2LessonTimeDto;
|
||||
timeRange: LessonTimeDto;
|
||||
|
||||
/**
|
||||
* Тип пары на этой строке
|
||||
@@ -60,45 +68,61 @@ export class V2ScheduleParseResult {
|
||||
/**
|
||||
* ETag расписания
|
||||
*/
|
||||
@IsString()
|
||||
etag: string;
|
||||
|
||||
/**
|
||||
* Идентификатор заменённого расписания (ObjectId)
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
replacerId?: string;
|
||||
|
||||
/**
|
||||
* Дата загрузки расписания на сайт политехникума
|
||||
*/
|
||||
@IsDate()
|
||||
uploadedAt: Date;
|
||||
|
||||
/**
|
||||
* Дата загрузки расписания с сайта политехникума
|
||||
*/
|
||||
@IsDate()
|
||||
downloadedAt: Date;
|
||||
|
||||
/**
|
||||
* Расписание групп в виде списка.
|
||||
* Ключ - название группы.
|
||||
*/
|
||||
groups: Array<V2GroupDto>;
|
||||
@ToMap({ mapValueClass: GroupDto })
|
||||
groups: Map<string, GroupDto>;
|
||||
|
||||
/**
|
||||
* Расписание преподавателей в виде списка.
|
||||
* Ключ - ФИО преподавателя
|
||||
*/
|
||||
teachers: Array<V2TeacherDto>;
|
||||
@ToMap({ mapValueClass: TeacherDto })
|
||||
teachers: Map<string, TeacherDto>;
|
||||
|
||||
/**
|
||||
* Список групп у которых было обновлено расписание с момента последнего обновления файла.
|
||||
* Ключ - название группы.
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Array<number>)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
updatedGroups: Array<Array<number>>;
|
||||
|
||||
/**
|
||||
* Список преподавателей у которых было обновлено расписание с момента последнего обновления файла.
|
||||
* Ключ - ФИО преподавателя.
|
||||
*/
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Array<number>)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
updatedTeachers: Array<Array<number>>;
|
||||
}
|
||||
|
||||
@@ -163,14 +187,14 @@ export class V2ScheduleParser {
|
||||
* @param lessonName - текст в записи
|
||||
* @returns {{
|
||||
* name: string;
|
||||
* subGroups: Array<V2LessonSubGroupDto>;
|
||||
* subGroups: Array<LessonSubGroupDto>;
|
||||
* }} - название пары и список подгрупп
|
||||
* @private
|
||||
* @static
|
||||
*/
|
||||
private static parseNameAndSubGroups(lessonName: string): {
|
||||
name: string;
|
||||
subGroups: Array<V2LessonSubGroupDto>;
|
||||
subGroups: Array<LessonSubGroupDto>;
|
||||
} {
|
||||
// хд
|
||||
|
||||
@@ -201,7 +225,7 @@ export class V2ScheduleParser {
|
||||
throw new Error("Парадокс");
|
||||
}
|
||||
|
||||
const subGroups: Array<V2LessonSubGroupDto> = [];
|
||||
const subGroups: Array<LessonSubGroupDto> = [];
|
||||
|
||||
for (const teacherAndSubGroup of all) {
|
||||
const teacherRegex = /[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.\s?[А-ЯЁ]\./g;
|
||||
@@ -224,7 +248,7 @@ export class V2ScheduleParser {
|
||||
: 1;
|
||||
|
||||
subGroups.push(
|
||||
plainToClass(V2LessonSubGroupDto, {
|
||||
plainToClass(LessonSubGroupDto, {
|
||||
teacher: teacherFIO,
|
||||
number: subGroup,
|
||||
cabinet: "",
|
||||
@@ -330,34 +354,36 @@ export class V2ScheduleParser {
|
||||
}
|
||||
|
||||
private static convertGroupsToTeachers(
|
||||
groups: Array<V2GroupDto>,
|
||||
): Array<V2TeacherDto> {
|
||||
const result: Array<V2TeacherDto> = [];
|
||||
groups: Map<string, GroupDto>,
|
||||
): Map<string, TeacherDto> {
|
||||
const result = new Map<string, TeacherDto>();
|
||||
|
||||
for (const groupName in groups) {
|
||||
const group = groups[groupName];
|
||||
for (const groupName of groups.keys()) {
|
||||
const group = groups.get(groupName);
|
||||
|
||||
for (const day of group.days) {
|
||||
for (const lesson of day.lessons) {
|
||||
if (lesson.type !== V2LessonType.DEFAULT) continue;
|
||||
|
||||
for (const subGroup of lesson.subGroups) {
|
||||
let teacherDto: V2TeacherDto = result[subGroup.teacher];
|
||||
let teacherDto: TeacherDto = result.get(
|
||||
subGroup.teacher,
|
||||
);
|
||||
|
||||
if (!teacherDto) {
|
||||
teacherDto = result[subGroup.teacher] =
|
||||
new V2TeacherDto();
|
||||
teacherDto = new TeacherDto();
|
||||
result.set(subGroup.teacher, teacherDto);
|
||||
|
||||
teacherDto.name = subGroup.teacher;
|
||||
teacherDto.days = [];
|
||||
}
|
||||
|
||||
let teacherDay: V2TeacherDayDto =
|
||||
let teacherDay: TeacherDayDto =
|
||||
teacherDto.days[day.name];
|
||||
|
||||
if (!teacherDay) {
|
||||
teacherDay = teacherDto.days[day.name] =
|
||||
new V2TeacherDayDto();
|
||||
new TeacherDayDto();
|
||||
|
||||
// TODO: Что это блять такое?
|
||||
// noinspection JSConstantReassignment
|
||||
@@ -368,7 +394,7 @@ export class V2ScheduleParser {
|
||||
|
||||
const teacherLesson = structuredClone(
|
||||
lesson,
|
||||
) as V2TeacherLessonDto;
|
||||
) as TeacherLessonDto;
|
||||
teacherLesson.group = groupName;
|
||||
|
||||
teacherDay.lessons.push(teacherLesson);
|
||||
@@ -377,8 +403,8 @@ export class V2ScheduleParser {
|
||||
}
|
||||
}
|
||||
|
||||
for (const teacherName in result) {
|
||||
const teacher = result[teacherName];
|
||||
for (const teacherName of result.keys()) {
|
||||
const teacher = result.get(teacherName);
|
||||
|
||||
const days = teacher.days;
|
||||
|
||||
@@ -438,7 +464,7 @@ export class V2ScheduleParser {
|
||||
const { groupSkeletons, daySkeletons } =
|
||||
V2ScheduleParser.parseSkeleton(workSheet);
|
||||
|
||||
const groups: Array<V2GroupDto> = [];
|
||||
const groups = new Map<string, GroupDto>();
|
||||
|
||||
const daysTimes: Array<Array<InternalTime>> = [];
|
||||
let daysTimesFilled = false;
|
||||
@@ -447,13 +473,13 @@ export class V2ScheduleParser {
|
||||
.e.r;
|
||||
|
||||
for (const groupSkeleton of groupSkeletons) {
|
||||
const group = new V2GroupDto();
|
||||
const group = new GroupDto();
|
||||
group.name = groupSkeleton.name;
|
||||
group.days = [];
|
||||
|
||||
for (let dayIdx = 0; dayIdx < daySkeletons.length; ++dayIdx) {
|
||||
const daySkeleton = daySkeletons[dayIdx];
|
||||
const day = new V2DayDto();
|
||||
const day = new DayDto();
|
||||
{
|
||||
const daySpaceIndex = daySkeleton.name.indexOf(" ");
|
||||
day.name = daySkeleton.name.substring(0, daySpaceIndex);
|
||||
@@ -502,7 +528,7 @@ export class V2ScheduleParser {
|
||||
: null;
|
||||
|
||||
// time
|
||||
const timeRange = new V2LessonTimeDto();
|
||||
const timeRange = new LessonTimeDto();
|
||||
|
||||
timeRange.start = new Date(day.date);
|
||||
timeRange.end = new Date(day.date);
|
||||
@@ -543,7 +569,7 @@ export class V2ScheduleParser {
|
||||
}
|
||||
|
||||
for (const time of dayTimes) {
|
||||
const lessons = V2ScheduleParser.parseLesson(
|
||||
const lessonsOrStreet = V2ScheduleParser.parseLesson(
|
||||
workSheet,
|
||||
day,
|
||||
dayTimes,
|
||||
@@ -551,7 +577,13 @@ export class V2ScheduleParser {
|
||||
groupSkeleton.column,
|
||||
);
|
||||
|
||||
for (const lesson of lessons) day.lessons.push(lesson);
|
||||
if (typeof lessonsOrStreet === "string") {
|
||||
day.street = lessonsOrStreet as string;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const lesson of lessonsOrStreet as Array<LessonDto>)
|
||||
day.lessons.push(lesson);
|
||||
}
|
||||
|
||||
group.days.push(day);
|
||||
@@ -559,7 +591,7 @@ export class V2ScheduleParser {
|
||||
|
||||
if (!daysTimesFilled) daysTimesFilled = true;
|
||||
|
||||
groups[group.name] = group;
|
||||
groups.set(group.name, group);
|
||||
}
|
||||
|
||||
const updatedGroups = V2ScheduleParser.getUpdatedGroups(
|
||||
@@ -588,34 +620,54 @@ export class V2ScheduleParser {
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly consultationRegExp = /\(?[кК]онсультация\)?/;
|
||||
private static readonly otherStreetRegExp = /^[А-Я][а-я]+,\s?[0-9]+$/;
|
||||
|
||||
private static parseLesson(
|
||||
workSheet: XLSX.Sheet,
|
||||
day: V2DayDto,
|
||||
day: DayDto,
|
||||
dayTimes: Array<InternalTime>,
|
||||
time: InternalTime,
|
||||
column: number,
|
||||
): Array<V2LessonDto> {
|
||||
): Array<LessonDto> | string {
|
||||
const row = time.xlsxRange.s.r;
|
||||
|
||||
// name
|
||||
const rawName = trimAll(
|
||||
let rawName = trimAll(
|
||||
V2ScheduleParser.getCellData(workSheet, row, column)?.replaceAll(
|
||||
/[\n\r]/g,
|
||||
"",
|
||||
" ",
|
||||
) ?? "",
|
||||
);
|
||||
|
||||
if (rawName.length === 0) return [];
|
||||
|
||||
const lesson = new V2LessonDto();
|
||||
const lesson = new LessonDto();
|
||||
|
||||
if (this.otherStreetRegExp.test(rawName)) return rawName;
|
||||
else if (rawName.includes("ЗАЧЕТ С ОЦЕНКОЙ")) {
|
||||
lesson.type = V2LessonType.EXAM_WITH_GRADE;
|
||||
rawName = trimAll(rawName.replace("ЗАЧЕТ С ОЦЕНКОЙ", ""));
|
||||
} else if (rawName.includes("ЗАЧЕТ")) {
|
||||
lesson.type = V2LessonType.EXAM;
|
||||
rawName = trimAll(rawName.replace("ЗАЧЕТ", ""));
|
||||
} else if (rawName.includes("(консультация)")) {
|
||||
lesson.type = V2LessonType.CONSULTATION;
|
||||
rawName = trimAll(rawName.replace("(консультация)", ""));
|
||||
} else if (this.consultationRegExp.test(rawName)) {
|
||||
lesson.type = V2LessonType.CONSULTATION;
|
||||
rawName = trimAll(rawName.replace(this.consultationRegExp, ""));
|
||||
} else if (rawName.includes("САМОСТОЯТЕЛЬНАЯ РАБОТА")) {
|
||||
lesson.type = V2LessonType.INDEPENDENT_WORK;
|
||||
rawName = trimAll(rawName.replace("САМОСТОЯТЕЛЬНАЯ РАБОТА", ""));
|
||||
} else lesson.type = time.lessonType;
|
||||
|
||||
lesson.type = time.lessonType;
|
||||
lesson.defaultRange =
|
||||
time.defaultIndex !== null
|
||||
? [time.defaultIndex, time.defaultIndex]
|
||||
: null;
|
||||
|
||||
lesson.time = new V2LessonTimeDto();
|
||||
lesson.time = new LessonTimeDto();
|
||||
lesson.time.start = time.timeRange.start;
|
||||
|
||||
// check if multi-lesson
|
||||
@@ -657,11 +709,11 @@ export class V2ScheduleParser {
|
||||
for (const index in cabinets) {
|
||||
if (lesson.subGroups[index] === undefined) {
|
||||
lesson.subGroups.push(
|
||||
plainToInstance(V2LessonSubGroupDto, {
|
||||
plainToInstance(LessonSubGroupDto, {
|
||||
number: +index + 1,
|
||||
teacher: "Ошибка в расписании",
|
||||
cabinet: cabinets[index],
|
||||
} as V2LessonSubGroupDto),
|
||||
} as LessonSubGroupDto),
|
||||
);
|
||||
|
||||
continue;
|
||||
@@ -681,16 +733,16 @@ export class V2ScheduleParser {
|
||||
if (!prevLesson) return [lesson];
|
||||
|
||||
return [
|
||||
plainToInstance(V2LessonDto, {
|
||||
plainToInstance(LessonDto, {
|
||||
type: V2LessonType.BREAK,
|
||||
defaultRange: null,
|
||||
name: null,
|
||||
time: plainToInstance(V2LessonTimeDto, {
|
||||
time: plainToInstance(LessonTimeDto, {
|
||||
start: prevLesson.time.end,
|
||||
end: lesson.time.start,
|
||||
} as V2LessonTimeDto),
|
||||
} as LessonTimeDto),
|
||||
subGroups: [],
|
||||
} as V2LessonDto),
|
||||
} as LessonDto),
|
||||
lesson,
|
||||
];
|
||||
}
|
||||
@@ -722,16 +774,16 @@ export class V2ScheduleParser {
|
||||
}
|
||||
|
||||
private static getUpdatedGroups(
|
||||
cachedGroups: Array<V2GroupDto> | null,
|
||||
currentGroups: Array<V2GroupDto>,
|
||||
cachedGroups: Map<string, GroupDto> | null,
|
||||
currentGroups: Map<string, GroupDto>,
|
||||
): Array<Array<number>> {
|
||||
if (!cachedGroups) return [];
|
||||
|
||||
const updatedGroups = [];
|
||||
|
||||
for (const name in cachedGroups) {
|
||||
const cachedGroup = cachedGroups[name];
|
||||
const currentGroup = currentGroups[name];
|
||||
for (const name of cachedGroups.keys()) {
|
||||
const cachedGroup = cachedGroups.get(name);
|
||||
const currentGroup = currentGroups.get(name);
|
||||
|
||||
const affectedGroupDays: Array<number> = [];
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import { AuthGuard } from "src/auth/auth.guard";
|
||||
import { AuthRoles } from "../auth/auth-role.decorator";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { V1ScheduleService } from "./v1-schedule.service";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
@@ -25,6 +24,7 @@ import { UserRole } from "../users/user-role.enum";
|
||||
import { ScheduleReplacerDto } from "./dto/schedule-replacer.dto";
|
||||
import { ClearScheduleReplacerDto } from "./dto/clear-schedule-replacer.dto";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
|
||||
@ApiTags("v1/schedule-replacer")
|
||||
@ApiBearerAuth()
|
||||
@@ -32,7 +32,7 @@ import { plainToInstance } from "class-transformer";
|
||||
@UseGuards(AuthGuard)
|
||||
export class ScheduleReplacerController {
|
||||
constructor(
|
||||
private readonly scheduleService: V1ScheduleService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
private readonly scheduleReplaceService: ScheduleReplacerService,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { V1ScheduleService } from "./v1-schedule.service";
|
||||
import { V1ScheduleController } from "./v1-schedule.controller";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { FirebaseAdminModule } from "../firebase-admin/firebase-admin.module";
|
||||
import { UsersModule } from "src/users/users.module";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { ScheduleReplacerController } from "./schedule-replacer.controller";
|
||||
import { V2ScheduleService } from "./v2-schedule.service";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { V2ScheduleController } from "./v2-schedule.controller";
|
||||
import { V3ScheduleController } from "./v3-schedule.controller";
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => UsersModule), FirebaseAdminModule],
|
||||
providers: [
|
||||
PrismaService,
|
||||
V1ScheduleService,
|
||||
V2ScheduleService,
|
||||
ScheduleReplacerService,
|
||||
],
|
||||
providers: [PrismaService, ScheduleService, ScheduleReplacerService],
|
||||
controllers: [
|
||||
V1ScheduleController,
|
||||
V2ScheduleController,
|
||||
V3ScheduleController,
|
||||
ScheduleReplacerController,
|
||||
],
|
||||
exports: [V1ScheduleService, V2ScheduleService],
|
||||
exports: [ScheduleService],
|
||||
})
|
||||
export class ScheduleModule {}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
@@ -10,21 +9,20 @@ import { plainToInstance } from "class-transformer";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service";
|
||||
import { scheduleConstants } from "../contants";
|
||||
import { V2ScheduleDto } from "./dto/v2/v2-schedule.dto";
|
||||
import { V1ScheduleService } from "./v1-schedule.service";
|
||||
import { ScheduleDto } from "./dto/schedule.dto";
|
||||
import {
|
||||
V2ScheduleParser,
|
||||
V2ScheduleParseResult,
|
||||
} from "./internal/schedule-parser/v2-schedule-parser";
|
||||
import * as objectHash from "object-hash";
|
||||
import { V2CacheStatusDto } from "./dto/v2/v2-cache-status.dto";
|
||||
import { V2GroupScheduleDto } from "./dto/v2/v2-group-schedule.dto";
|
||||
import { V2ScheduleGroupNamesDto } from "./dto/v2/v2-schedule-group-names.dto";
|
||||
import { V2TeacherScheduleDto } from "./dto/v2/v2-teacher-schedule.dto";
|
||||
import { V2ScheduleTeacherNamesDto } from "./dto/v2/v2-schedule-teacher-names.dto";
|
||||
import { CacheStatusDto } from "./dto/cache-status.dto";
|
||||
import { GroupScheduleDto } from "./dto/group-schedule.dto";
|
||||
import { ScheduleGroupNamesDto } from "./dto/schedule-group-names.dto";
|
||||
import { TeacherScheduleDto } from "./dto/teacher-schedule.dto";
|
||||
import { ScheduleTeacherNamesDto } from "./dto/schedule-teacher-names.dto";
|
||||
|
||||
@Injectable()
|
||||
export class V2ScheduleService {
|
||||
export class ScheduleService {
|
||||
readonly scheduleParser: V2ScheduleParser;
|
||||
|
||||
private cacheUpdatedAt: Date = new Date(0);
|
||||
@@ -36,8 +34,6 @@ export class V2ScheduleService {
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
private readonly scheduleReplacerService: ScheduleReplacerService,
|
||||
private readonly firebaseAdminService: FirebaseAdminService,
|
||||
@Inject(forwardRef(() => V1ScheduleService))
|
||||
private readonly v1ScheduleService: V1ScheduleService,
|
||||
) {
|
||||
setInterval(async () => {
|
||||
const now = new Date();
|
||||
@@ -60,8 +56,8 @@ export class V2ScheduleService {
|
||||
);
|
||||
}
|
||||
|
||||
getCacheStatus(): V2CacheStatusDto {
|
||||
return plainToInstance(V2CacheStatusDto, {
|
||||
getCacheStatus(): CacheStatusDto {
|
||||
return plainToInstance(CacheStatusDto, {
|
||||
cacheHash: this.cacheHash,
|
||||
cacheUpdateRequired:
|
||||
(Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >=
|
||||
@@ -71,9 +67,7 @@ export class V2ScheduleService {
|
||||
});
|
||||
}
|
||||
|
||||
async getSourceSchedule(
|
||||
silent: boolean = false,
|
||||
): Promise<V2ScheduleParseResult> {
|
||||
async getSourceSchedule(): Promise<V2ScheduleParseResult> {
|
||||
const schedule = await this.scheduleParser.getSchedule();
|
||||
|
||||
this.cacheUpdatedAt = new Date();
|
||||
@@ -83,8 +77,6 @@ export class V2ScheduleService {
|
||||
|
||||
if (this.cacheHash !== oldHash) {
|
||||
if (this.scheduleUpdatedAt.valueOf() !== 0) {
|
||||
if (!silent) await this.v1ScheduleService.refreshCache(true);
|
||||
|
||||
const isReplaced = await this.scheduleReplacerService.hasByEtag(
|
||||
schedule.etag,
|
||||
);
|
||||
@@ -103,7 +95,7 @@ export class V2ScheduleService {
|
||||
return schedule;
|
||||
}
|
||||
|
||||
async getSchedule(): Promise<V2ScheduleDto> {
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
const sourceSchedule = await this.getSourceSchedule();
|
||||
|
||||
return {
|
||||
@@ -113,10 +105,11 @@ export class V2ScheduleService {
|
||||
};
|
||||
}
|
||||
|
||||
async getGroup(name: string): Promise<V2GroupScheduleDto> {
|
||||
async getGroup(name: string): Promise<GroupScheduleDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
|
||||
if (schedule.groups[name] === undefined) {
|
||||
const group = schedule.groups.get(name);
|
||||
if (group === undefined) {
|
||||
throw new NotFoundException(
|
||||
"Группы с таким названием не существует!",
|
||||
);
|
||||
@@ -124,26 +117,27 @@ export class V2ScheduleService {
|
||||
|
||||
return {
|
||||
updatedAt: this.cacheUpdatedAt,
|
||||
group: schedule.groups[name],
|
||||
group: group,
|
||||
updated: schedule.updatedGroups[name] ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getGroupNames(): Promise<V2ScheduleGroupNamesDto> {
|
||||
async getGroupNames(): Promise<ScheduleGroupNamesDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
|
||||
for (const name in schedule.groups) names.push(name);
|
||||
for (const name of schedule.groups.keys()) names.push(name);
|
||||
|
||||
return plainToInstance(V2ScheduleGroupNamesDto, {
|
||||
return plainToInstance(ScheduleGroupNamesDto, {
|
||||
names: names,
|
||||
});
|
||||
}
|
||||
|
||||
async getTeacher(name: string): Promise<V2TeacherScheduleDto> {
|
||||
async getTeacher(name: string): Promise<TeacherScheduleDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
|
||||
if (schedule.teachers[name] === undefined) {
|
||||
const teacher = schedule.teachers.get(name);
|
||||
if (teacher === undefined) {
|
||||
throw new NotFoundException(
|
||||
"Преподавателя с таким ФИО не существует!",
|
||||
);
|
||||
@@ -151,45 +145,33 @@ export class V2ScheduleService {
|
||||
|
||||
return {
|
||||
updatedAt: this.cacheUpdatedAt,
|
||||
teacher: schedule.teachers[name],
|
||||
teacher: teacher,
|
||||
updated: schedule.updatedGroups[name] ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getTeacherNames(): Promise<V2ScheduleTeacherNamesDto> {
|
||||
async getTeacherNames(): Promise<ScheduleTeacherNamesDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
|
||||
for (const name in schedule.teachers) names.push(name);
|
||||
for (const name of schedule.teachers.keys()) names.push(name);
|
||||
|
||||
return plainToInstance(V2ScheduleTeacherNamesDto, {
|
||||
return plainToInstance(ScheduleTeacherNamesDto, {
|
||||
names: names,
|
||||
});
|
||||
}
|
||||
|
||||
async updateDownloadUrl(
|
||||
url: string,
|
||||
silent: boolean = false,
|
||||
): Promise<V2CacheStatusDto> {
|
||||
async updateDownloadUrl(url: string): Promise<CacheStatusDto> {
|
||||
await this.scheduleParser.getXlsDownloader().setDownloadUrl(url);
|
||||
await this.v1ScheduleService.scheduleParser
|
||||
.getXlsDownloader()
|
||||
.setDownloadUrl(url);
|
||||
|
||||
if (!silent) {
|
||||
await this.refreshCache(false);
|
||||
await this.v1ScheduleService.refreshCache(true);
|
||||
}
|
||||
await this.refreshCache();
|
||||
|
||||
return this.getCacheStatus();
|
||||
}
|
||||
|
||||
async refreshCache(silent: boolean = false) {
|
||||
if (!silent) {
|
||||
async refreshCache() {
|
||||
await this.cacheManager.reset();
|
||||
await this.v1ScheduleService.refreshCache(true);
|
||||
}
|
||||
|
||||
await this.getSourceSchedule(silent);
|
||||
await this.getSourceSchedule();
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { V1ScheduleService } from "./v1-schedule.service";
|
||||
import { V1ScheduleDto } from "./dto/v1/v1-schedule.dto";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiExtraModels,
|
||||
ApiNotAcceptableResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
refs,
|
||||
} from "@nestjs/swagger";
|
||||
import { ResponseVersion } from "../version/response-version.decorator";
|
||||
import { AuthRoles, AuthUnauthorized } from "../auth/auth-role.decorator";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { UserPipe } from "../auth/auth.pipe";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import { V1CacheStatusDto } from "./dto/v1/v1-cache-status.dto";
|
||||
import { V2CacheStatusDto } from "./dto/v2/v2-cache-status.dto";
|
||||
import { CacheStatusDto } from "./dto/v1/cache-status.dto";
|
||||
import { V1GroupScheduleNameDto } from "./dto/v1/v1-group-schedule-name.dto";
|
||||
import { V1ScheduleGroupNamesDto } from "./dto/v1/v1-schedule-group-names.dto";
|
||||
import { V1GroupScheduleDto } from "./dto/v1/v1-group-schedule.dto";
|
||||
import { V1SiteMainPageDto } from "./dto/v1/v1-site-main-page.dto";
|
||||
|
||||
@ApiTags("v1/schedule")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "schedule", version: "1" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V1ScheduleController {
|
||||
constructor(private readonly scheduleService: V1ScheduleService) {}
|
||||
|
||||
@ApiExtraModels(V1ScheduleDto)
|
||||
@ApiOperation({
|
||||
summary: "Получение расписания",
|
||||
tags: ["admin"],
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Расписание получено успешно",
|
||||
schema: refs(V1ScheduleDto)[0],
|
||||
})
|
||||
@ResultDto(V1ScheduleDto)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("get")
|
||||
async getSchedule(): Promise<V1ScheduleDto> {
|
||||
return await this.scheduleService.getSchedule();
|
||||
}
|
||||
|
||||
@ApiExtraModels(V1GroupScheduleDto)
|
||||
@ApiOperation({ summary: "Получение расписания группы" })
|
||||
@ApiOkResponse({
|
||||
description: "Расписание получено успешно",
|
||||
schema: refs(V1GroupScheduleDto)[0],
|
||||
})
|
||||
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
|
||||
@ResultDto(V1GroupScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("get-group")
|
||||
async getGroupSchedule(
|
||||
@Body() groupDto: V1GroupScheduleNameDto,
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<V1GroupScheduleDto> {
|
||||
return await this.scheduleService.getGroup(groupDto.name ?? user.group);
|
||||
}
|
||||
|
||||
@ApiExtraModels(V1ScheduleGroupNamesDto)
|
||||
@ApiOperation({
|
||||
summary: "Получение списка названий всех групп в расписании",
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Список получен успешно",
|
||||
schema: refs(V1ScheduleGroupNamesDto)[0],
|
||||
})
|
||||
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
|
||||
@ResultDto(V1ScheduleGroupNamesDto)
|
||||
@AuthUnauthorized()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("get-group-names")
|
||||
async getGroupNames(): Promise<V1ScheduleGroupNamesDto> {
|
||||
return await this.scheduleService.getGroupNames();
|
||||
}
|
||||
|
||||
@ApiExtraModels(V1SiteMainPageDto)
|
||||
@ApiExtraModels(V1CacheStatusDto)
|
||||
@ApiExtraModels(V2CacheStatusDto)
|
||||
@ApiOperation({
|
||||
summary: "Обновление данных основной страницы политехникума",
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Данные обновлены успешно",
|
||||
schema: refs(V1CacheStatusDto)[0],
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Данные обновлены успешно",
|
||||
schema: refs(V1CacheStatusDto)[1],
|
||||
})
|
||||
@ApiNotAcceptableResponse({
|
||||
description: "Передан некорректный код страницы",
|
||||
})
|
||||
@ResultDto([V1CacheStatusDto, V2CacheStatusDto])
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("update-site-main-page")
|
||||
async updateSiteMainPage(
|
||||
@Body() siteMainPageDto: V1SiteMainPageDto,
|
||||
@ResponseVersion() version: number,
|
||||
): Promise<V1CacheStatusDto> {
|
||||
return CacheStatusDto.stripVersion(
|
||||
await this.scheduleService.updateSiteMainPage(siteMainPageDto),
|
||||
version,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiExtraModels(V1CacheStatusDto)
|
||||
@ApiExtraModels(V2CacheStatusDto)
|
||||
@ApiOperation({
|
||||
summary: "Получение информации о кеше",
|
||||
tags: ["cache"],
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Получение данных прошло успешно",
|
||||
schema: refs(V1CacheStatusDto)[0],
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: "Получение данных прошло успешно",
|
||||
schema: refs(V2CacheStatusDto)[0],
|
||||
})
|
||||
@ResultDto([V1CacheStatusDto, V2CacheStatusDto])
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("cache-status")
|
||||
getCacheStatus(
|
||||
@ResponseVersion() version: number,
|
||||
): V1CacheStatusDto | V2CacheStatusDto {
|
||||
return CacheStatusDto.stripVersion(
|
||||
this.scheduleService.getCacheStatus(),
|
||||
version,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import {
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotAcceptableException,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
V1ScheduleParser,
|
||||
ScheduleParseResult,
|
||||
} from "./internal/schedule-parser/v1-schedule-parser";
|
||||
import { BasicXlsDownloader } from "./internal/xls-downloader/basic-xls-downloader";
|
||||
import { V1ScheduleDto } from "./dto/v1/v1-schedule.dto";
|
||||
import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { instanceToPlain } from "class-transformer";
|
||||
import { cacheGetOrFill } from "../utility/cache.util";
|
||||
import * as crypto from "crypto";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { scheduleConstants } from "../contants";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { V2ScheduleService } from "./v2-schedule.service";
|
||||
import { V1GroupDto } from "./dto/v1/v1-group.dto";
|
||||
import { CacheStatusDto } from "./dto/v1/cache-status.dto";
|
||||
import { V1ScheduleGroupNamesDto } from "./dto/v1/v1-schedule-group-names.dto";
|
||||
import { V1GroupScheduleDto } from "./dto/v1/v1-group-schedule.dto";
|
||||
import { V1SiteMainPageDto } from "./dto/v1/v1-site-main-page.dto";
|
||||
|
||||
@Injectable()
|
||||
export class V1ScheduleService {
|
||||
readonly scheduleParser: V1ScheduleParser;
|
||||
|
||||
private cacheUpdatedAt: Date = new Date(0);
|
||||
private cacheHash: string = "0000000000000000000000000000000000000000";
|
||||
|
||||
private lastChangedDays: Array<Array<number>> = [];
|
||||
private scheduleUpdatedAt: Date = new Date(0);
|
||||
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
private readonly scheduleReplacerService: ScheduleReplacerService,
|
||||
@Inject(forwardRef(() => V2ScheduleService))
|
||||
private readonly v2ScheduleService: V2ScheduleService,
|
||||
) {
|
||||
this.scheduleParser = new V1ScheduleParser(
|
||||
new BasicXlsDownloader(),
|
||||
this.scheduleReplacerService,
|
||||
);
|
||||
}
|
||||
|
||||
getCacheStatus(): CacheStatusDto {
|
||||
return {
|
||||
cacheHash: this.cacheHash,
|
||||
cacheUpdateRequired:
|
||||
(Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >=
|
||||
scheduleConstants.cacheInvalidateDelay,
|
||||
lastCacheUpdate: this.cacheUpdatedAt.valueOf(),
|
||||
lastScheduleUpdate: this.scheduleUpdatedAt.valueOf(),
|
||||
};
|
||||
}
|
||||
|
||||
async getSourceSchedule(
|
||||
silent: boolean = false,
|
||||
): Promise<ScheduleParseResult> {
|
||||
return cacheGetOrFill(this.cacheManager, "sourceSchedule", async () => {
|
||||
const schedule = await this.scheduleParser.getSchedule();
|
||||
schedule.groups = V1ScheduleService.toObject(
|
||||
schedule.groups,
|
||||
) as Array<V1GroupDto>;
|
||||
|
||||
this.cacheUpdatedAt = new Date();
|
||||
|
||||
const oldHash = this.cacheHash;
|
||||
this.cacheHash = crypto
|
||||
.createHash("sha1")
|
||||
.update(
|
||||
JSON.stringify(schedule.groups, null, 0) + schedule.etag,
|
||||
)
|
||||
.digest("hex");
|
||||
|
||||
if (
|
||||
this.scheduleUpdatedAt.valueOf() === 0 ||
|
||||
this.cacheHash !== oldHash
|
||||
) {
|
||||
if (this.scheduleUpdatedAt.valueOf() !== 0 && !silent)
|
||||
await this.v2ScheduleService.refreshCache(true);
|
||||
this.scheduleUpdatedAt = new Date();
|
||||
}
|
||||
|
||||
return schedule;
|
||||
});
|
||||
}
|
||||
|
||||
private static toObject<T>(array: Array<T>): object {
|
||||
const object = {};
|
||||
|
||||
for (const item in array) object[item] = array[item];
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
async getSchedule(): Promise<V1ScheduleDto> {
|
||||
return cacheGetOrFill(
|
||||
this.cacheManager,
|
||||
"schedule",
|
||||
async (): Promise<V1ScheduleDto> => {
|
||||
const sourceSchedule = await this.getSourceSchedule();
|
||||
|
||||
for (const groupName in sourceSchedule.affectedDays) {
|
||||
const affectedDays = sourceSchedule.affectedDays[groupName];
|
||||
|
||||
if (affectedDays?.length !== 0)
|
||||
this.lastChangedDays[groupName] = affectedDays;
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAt: this.cacheUpdatedAt,
|
||||
groups: V1ScheduleService.toObject(sourceSchedule.groups),
|
||||
lastChangedDays: this.lastChangedDays,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getGroup(group: string): Promise<V1GroupScheduleDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
|
||||
if ((schedule.groups as object)[group] === undefined) {
|
||||
throw new NotFoundException(
|
||||
"Группы с таким названием не существует!",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAt: this.cacheUpdatedAt,
|
||||
group: schedule.groups[group],
|
||||
lastChangedDays: this.lastChangedDays[group] ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getGroupNames(): Promise<V1ScheduleGroupNamesDto> {
|
||||
let groupNames: V1ScheduleGroupNamesDto | undefined =
|
||||
await this.cacheManager.get("groupNames");
|
||||
|
||||
if (!groupNames) {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
|
||||
for (const groupName in schedule.groups) names.push(groupName);
|
||||
|
||||
groupNames = { names };
|
||||
await this.cacheManager.set(
|
||||
"groupNames",
|
||||
instanceToPlain(groupNames),
|
||||
24 * 60 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
return groupNames;
|
||||
}
|
||||
|
||||
private async getDOM(preparedData: any): Promise<JSDOM | null> {
|
||||
try {
|
||||
return new JSDOM(atob(preparedData), {
|
||||
url: "https://politehnikum-eng.ru/index/raspisanie_zanjatij/0-409",
|
||||
contentType: "text/html",
|
||||
});
|
||||
} catch {
|
||||
throw new NotAcceptableException(
|
||||
"Передан некорректный код страницы",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private parseData(dom: JSDOM): string {
|
||||
try {
|
||||
const scheduleBlock = dom.window.document.getElementById("cont-i");
|
||||
if (scheduleBlock === null)
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Не удалось найти блок расписаний!");
|
||||
|
||||
const schedules = scheduleBlock.getElementsByTagName("div");
|
||||
if (schedules === null || schedules.length === 0)
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Не удалось найти строку с расписанием!");
|
||||
|
||||
const poltavskaya = schedules[0];
|
||||
const link = poltavskaya.getElementsByTagName("a")[0]!;
|
||||
|
||||
return link.href;
|
||||
} catch (exception) {
|
||||
console.error(exception);
|
||||
throw new NotAcceptableException(
|
||||
"Передан некорректный код страницы",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async updateSiteMainPage(
|
||||
siteMainPageDto: V1SiteMainPageDto,
|
||||
): Promise<CacheStatusDto> {
|
||||
const dom = await this.getDOM(siteMainPageDto.mainPage);
|
||||
const url = this.parseData(dom);
|
||||
|
||||
return await this.updateDownloadUrl(url);
|
||||
}
|
||||
|
||||
async updateDownloadUrl(
|
||||
url: string,
|
||||
silent: boolean = false,
|
||||
): Promise<CacheStatusDto> {
|
||||
await this.scheduleParser.getXlsDownloader().setDownloadUrl(url);
|
||||
await this.v2ScheduleService.scheduleParser
|
||||
.getXlsDownloader()
|
||||
.setDownloadUrl(url);
|
||||
|
||||
if (!silent) {
|
||||
await this.refreshCache(false);
|
||||
await this.v2ScheduleService.refreshCache(true);
|
||||
}
|
||||
|
||||
return this.getCacheStatus();
|
||||
}
|
||||
|
||||
async refreshCache(silent: boolean = false) {
|
||||
if (!silent) {
|
||||
await this.cacheManager.reset();
|
||||
await this.v2ScheduleService.refreshCache(true);
|
||||
}
|
||||
|
||||
await this.getSourceSchedule(silent);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import { AuthGuard } from "../auth/auth.guard";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
@@ -21,26 +20,25 @@ import {
|
||||
import { AuthRoles, AuthUnauthorized } from "../auth/auth-role.decorator";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { UserPipe } from "../auth/auth.pipe";
|
||||
import { V2ScheduleService } from "./v2-schedule.service";
|
||||
import { V2ScheduleDto } from "./dto/v2/v2-schedule.dto";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { ScheduleDto } from "./dto/schedule.dto";
|
||||
import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import { V1CacheStatusDto } from "./dto/v1/v1-cache-status.dto";
|
||||
import { V2CacheStatusDto } from "./dto/v2/v2-cache-status.dto";
|
||||
import { V2UpdateDownloadUrlDto } from "./dto/v2/v2-update-download-url.dto";
|
||||
import { V2GroupScheduleByNameDto } from "./dto/v2/v2-group-schedule-by-name.dto";
|
||||
import { V2GroupScheduleDto } from "./dto/v2/v2-group-schedule.dto";
|
||||
import { V2ScheduleGroupNamesDto } from "./dto/v2/v2-schedule-group-names.dto";
|
||||
import { V2TeacherScheduleDto } from "./dto/v2/v2-teacher-schedule.dto";
|
||||
import { V2ScheduleTeacherNamesDto } from "./dto/v2/v2-schedule-teacher-names.dto";
|
||||
import { CacheStatusDto } from "./dto/cache-status.dto";
|
||||
import { UpdateDownloadUrlDto } from "./dto/update-download-url.dto";
|
||||
import { GroupScheduleDto } from "./dto/group-schedule.dto";
|
||||
import { ScheduleGroupNamesDto } from "./dto/schedule-group-names.dto";
|
||||
import { TeacherScheduleDto } from "./dto/teacher-schedule.dto";
|
||||
import { ScheduleTeacherNamesDto } from "./dto/schedule-teacher-names.dto";
|
||||
import instanceToInstance2 from "../utility/class-trasformer/instance-to-instance-2";
|
||||
|
||||
@ApiTags("v2/schedule")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "schedule", version: "2" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V2ScheduleController {
|
||||
constructor(private readonly scheduleService: V2ScheduleService) {}
|
||||
constructor(private readonly scheduleService: ScheduleService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: "Получение расписания",
|
||||
@@ -49,88 +47,97 @@ export class V2ScheduleController {
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: V2ScheduleDto,
|
||||
type: ScheduleDto,
|
||||
})
|
||||
@ResultDto(V2ScheduleDto)
|
||||
@ResultDto(ScheduleDto)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@CacheKey("v2-schedule")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get()
|
||||
async getSchedule(): Promise<V2ScheduleDto> {
|
||||
return await this.scheduleService.getSchedule();
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
return await this.scheduleService.getSchedule().then((result) =>
|
||||
instanceToInstance2(ScheduleDto, result, {
|
||||
groups: ["v1"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания группы" })
|
||||
@ApiBody({ type: V2GroupScheduleByNameDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: V2GroupScheduleDto,
|
||||
type: GroupScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемая группа не найдена",
|
||||
})
|
||||
@ResultDto(V2GroupScheduleDto)
|
||||
@ResultDto(GroupScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("group")
|
||||
async getGroupSchedule(
|
||||
@Body() reqDto: V2GroupScheduleByNameDto,
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<V2GroupScheduleDto> {
|
||||
return await this.scheduleService.getGroup(reqDto.name ?? user.group);
|
||||
): Promise<GroupScheduleDto> {
|
||||
return await this.scheduleService.getGroup(user.group).then((result) =>
|
||||
instanceToInstance2(GroupScheduleDto, result, {
|
||||
groups: ["v1"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение списка названий групп" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Список получен успешно",
|
||||
type: V2ScheduleGroupNamesDto,
|
||||
type: ScheduleGroupNamesDto,
|
||||
})
|
||||
@ResultDto(V2ScheduleGroupNamesDto)
|
||||
@ResultDto(ScheduleGroupNamesDto)
|
||||
@CacheKey("v2-schedule-group-names")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@AuthUnauthorized()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("group-names")
|
||||
async getGroupNames(): Promise<V2ScheduleGroupNamesDto> {
|
||||
async getGroupNames(): Promise<ScheduleGroupNamesDto> {
|
||||
return await this.scheduleService.getGroupNames();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания преподавателя" })
|
||||
@ApiBody({ type: V2GroupScheduleByNameDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: V2TeacherScheduleDto,
|
||||
type: TeacherScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемый преподаватель не найден",
|
||||
})
|
||||
@ResultDto(V2TeacherScheduleDto)
|
||||
@ResultDto(TeacherScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("teacher/:name")
|
||||
async getTeacherSchedule(
|
||||
@Param("name") name: string,
|
||||
): Promise<V2TeacherScheduleDto> {
|
||||
return await this.scheduleService.getTeacher(name);
|
||||
): Promise<TeacherScheduleDto> {
|
||||
return await this.scheduleService.getTeacher(name).then((result) =>
|
||||
instanceToInstance2(TeacherScheduleDto, result, {
|
||||
groups: ["v1"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение списка ФИО преподавателей" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Список получен успешно",
|
||||
type: V2ScheduleTeacherNamesDto,
|
||||
type: ScheduleTeacherNamesDto,
|
||||
})
|
||||
@ResultDto(V2ScheduleTeacherNamesDto)
|
||||
@ResultDto(ScheduleTeacherNamesDto)
|
||||
@CacheKey("v2-schedule-teacher-names")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@AuthUnauthorized()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("teacher-names")
|
||||
async getTeacherNames(): Promise<V2ScheduleTeacherNamesDto> {
|
||||
async getTeacherNames(): Promise<ScheduleTeacherNamesDto> {
|
||||
return await this.scheduleService.getTeacherNames();
|
||||
}
|
||||
|
||||
@@ -138,18 +145,18 @@ export class V2ScheduleController {
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Данные обновлены успешно",
|
||||
type: V2CacheStatusDto,
|
||||
type: CacheStatusDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_ACCEPTABLE,
|
||||
description: "Передан некорректный код страницы",
|
||||
})
|
||||
@ResultDto(V2CacheStatusDto)
|
||||
@ResultDto(CacheStatusDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Patch("update-download-url")
|
||||
async updateDownloadUrl(
|
||||
@Body() reqDto: V2UpdateDownloadUrlDto,
|
||||
): Promise<V1CacheStatusDto> {
|
||||
@Body() reqDto: UpdateDownloadUrlDto,
|
||||
): Promise<CacheStatusDto> {
|
||||
return await this.scheduleService.updateDownloadUrl(reqDto.url);
|
||||
}
|
||||
|
||||
@@ -160,12 +167,12 @@ export class V2ScheduleController {
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Получение данных прошло успешно",
|
||||
type: V2CacheStatusDto,
|
||||
type: CacheStatusDto,
|
||||
})
|
||||
@ResultDto(V2CacheStatusDto)
|
||||
@ResultDto(CacheStatusDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("cache-status")
|
||||
getCacheStatus(): V2CacheStatusDto {
|
||||
getCacheStatus(): CacheStatusDto {
|
||||
return this.scheduleService.getCacheStatus();
|
||||
}
|
||||
}
|
||||
|
||||
92
src/schedule/v3-schedule.controller.ts
Normal file
92
src/schedule/v3-schedule.controller.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { AuthRoles } from "../auth/auth-role.decorator";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { UserPipe } from "../auth/auth.pipe";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { ScheduleDto } from "./dto/schedule.dto";
|
||||
import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import { GroupScheduleDto } from "./dto/group-schedule.dto";
|
||||
import { TeacherScheduleDto } from "./dto/teacher-schedule.dto";
|
||||
|
||||
@ApiTags("v3/schedule")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "schedule", version: "3" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V3ScheduleController {
|
||||
constructor(private readonly scheduleService: ScheduleService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: "Получение расписания",
|
||||
tags: ["admin"],
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: ScheduleDto,
|
||||
})
|
||||
@ResultDto(ScheduleDto)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@CacheKey("v3-schedule")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get()
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
return await this.scheduleService.getSchedule();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания группы" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: GroupScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемая группа не найдена",
|
||||
})
|
||||
@ResultDto(GroupScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("group")
|
||||
async getGroupSchedule(
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<GroupScheduleDto> {
|
||||
return await this.scheduleService.getGroup(user.group);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания преподавателя" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: TeacherScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемый преподаватель не найден",
|
||||
})
|
||||
@ResultDto(TeacherScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("teacher/:name")
|
||||
async getTeacherSchedule(
|
||||
@Param("name") name: string,
|
||||
): Promise<TeacherScheduleDto> {
|
||||
return await this.scheduleService.getTeacher(name);
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,18 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { V1ScheduleService } from "../schedule/v1-schedule.service";
|
||||
import { User } from "./entity/user.entity";
|
||||
import { ChangeUsernameDto } from "./dto/change-username.dto";
|
||||
import { ChangeGroupDto } from "./dto/change-group.dto";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
@Inject(forwardRef(() => V1ScheduleService))
|
||||
private readonly scheduleService: V1ScheduleService,
|
||||
@Inject(forwardRef(() => ScheduleService))
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
async findUnique(where: Prisma.UserWhereUniqueInput): Promise<User | null> {
|
||||
|
||||
13
src/utility/class-trasformer/instance-to-instance-2.ts
Normal file
13
src/utility/class-trasformer/instance-to-instance-2.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
ClassConstructor,
|
||||
ClassTransformOptions,
|
||||
} from "class-transformer/types/interfaces";
|
||||
import { instanceToPlain, plainToInstance } from "class-transformer";
|
||||
|
||||
export default function instanceToInstance2<T, V>(
|
||||
cls: ClassConstructor<T>,
|
||||
instance: V,
|
||||
options?: ClassTransformOptions,
|
||||
): T {
|
||||
return plainToInstance(cls, instanceToPlain(instance), options);
|
||||
}
|
||||
Reference in New Issue
Block a user