feat: model undergraduate schools terms and exam queries
This commit is contained in:
@@ -3,6 +3,7 @@ import 'course.dart';
|
|||||||
import 'semester.dart';
|
import 'semester.dart';
|
||||||
import 'exam_schedule.dart';
|
import 'exam_schedule.dart';
|
||||||
import 'school_term.dart';
|
import 'school_term.dart';
|
||||||
|
import 'school_identity.dart';
|
||||||
|
|
||||||
class ImportMetadata {
|
class ImportMetadata {
|
||||||
const ImportMetadata({
|
const ImportMetadata({
|
||||||
@@ -23,9 +24,12 @@ class AppSnapshot {
|
|||||||
this.examSchedule,
|
this.examSchedule,
|
||||||
SchoolTerm? schoolTerm,
|
SchoolTerm? schoolTerm,
|
||||||
this.calendarCleanupPending = false,
|
this.calendarCleanupPending = false,
|
||||||
|
SupportedSchool? selectedSchool,
|
||||||
Map<String, String> calendarEventIds = const {},
|
Map<String, String> calendarEventIds = const {},
|
||||||
}) : settings = settings ?? AppSettings(),
|
}) : settings = settings ?? AppSettings(),
|
||||||
schoolTerm = schoolTerm ?? _legacyTerm(courses, examSchedule),
|
schoolTerm = schoolTerm ?? _legacyTerm(courses, examSchedule),
|
||||||
|
selectedSchool = selectedSchool ?? schoolTerm?.school ??
|
||||||
|
_legacyTerm(courses, examSchedule)?.school ?? examSchedule?.school,
|
||||||
courses = List.unmodifiable(courses),
|
courses = List.unmodifiable(courses),
|
||||||
calendarEventIds = Map.unmodifiable(calendarEventIds);
|
calendarEventIds = Map.unmodifiable(calendarEventIds);
|
||||||
final AppSettings settings;
|
final AppSettings settings;
|
||||||
@@ -35,6 +39,7 @@ class AppSnapshot {
|
|||||||
final SchoolTerm? schoolTerm;
|
final SchoolTerm? schoolTerm;
|
||||||
final Map<String, String> calendarEventIds;
|
final Map<String, String> calendarEventIds;
|
||||||
final bool calendarCleanupPending;
|
final bool calendarCleanupPending;
|
||||||
|
final SupportedSchool? selectedSchool;
|
||||||
static SchoolTerm? _legacyTerm(List<Course> courses, ExamSchedule? exams) {
|
static SchoolTerm? _legacyTerm(List<Course> courses, ExamSchedule? exams) {
|
||||||
final ids = courses
|
final ids = courses
|
||||||
.map(
|
.map(
|
||||||
@@ -42,7 +47,7 @@ class AppSnapshot {
|
|||||||
)
|
)
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.toSet();
|
.toSet();
|
||||||
if (exams != null) ids.add(exams.termId);
|
if (exams?.school == SupportedSchool.uestcGraduate) ids.add(exams!.termId);
|
||||||
if (ids.length != 1) return null;
|
if (ids.length != 1) return null;
|
||||||
final id = int.tryParse(ids.single);
|
final id = int.tryParse(ids.single);
|
||||||
return id != null && id >= 1 && id <= 9999 ? SchoolTerm(id) : null;
|
return id != null && id >= 1 && id <= 9999 ? SchoolTerm(id) : null;
|
||||||
@@ -58,6 +63,7 @@ class AppSnapshot {
|
|||||||
bool clearExams = false,
|
bool clearExams = false,
|
||||||
SchoolTerm? schoolTerm,
|
SchoolTerm? schoolTerm,
|
||||||
bool? calendarCleanupPending,
|
bool? calendarCleanupPending,
|
||||||
|
SupportedSchool? selectedSchool,
|
||||||
}) => AppSnapshot(
|
}) => AppSnapshot(
|
||||||
settings: settings ?? this.settings,
|
settings: settings ?? this.settings,
|
||||||
courses: courses ?? this.courses,
|
courses: courses ?? this.courses,
|
||||||
@@ -69,11 +75,13 @@ class AppSnapshot {
|
|||||||
calendarEventIds: calendarEventIds ?? this.calendarEventIds,
|
calendarEventIds: calendarEventIds ?? this.calendarEventIds,
|
||||||
calendarCleanupPending:
|
calendarCleanupPending:
|
||||||
calendarCleanupPending ?? this.calendarCleanupPending,
|
calendarCleanupPending ?? this.calendarCleanupPending,
|
||||||
|
selectedSchool:selectedSchool ?? this.selectedSchool,
|
||||||
);
|
);
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'schemaVersion': 1,
|
'schemaVersion': 1,
|
||||||
'examSchedule': examSchedule?.toJson(),
|
'examSchedule': examSchedule?.toJson(),
|
||||||
'schoolTerm': schoolTerm?.toJson(),
|
'schoolTerm': schoolTerm?.toJson(),
|
||||||
|
'selectedSchoolId':selectedSchool?.id,
|
||||||
'courses': courses.map((c) => c.toJson()).toList(),
|
'courses': courses.map((c) => c.toJson()).toList(),
|
||||||
'settings': {
|
'settings': {
|
||||||
'semester': settings.semester == null
|
'semester': settings.semester == null
|
||||||
@@ -144,7 +152,8 @@ class AppSnapshot {
|
|||||||
totalWeeks: sem['totalWeeks'] as int,
|
totalWeeks: sem['totalWeeks'] as int,
|
||||||
);
|
);
|
||||||
final metadata = json['importMetadata'];
|
final metadata = json['importMetadata'];
|
||||||
return AppSnapshot(
|
final snapshot = AppSnapshot(
|
||||||
|
selectedSchool:json['selectedSchoolId']==null ? null : SupportedSchool.fromId(json['selectedSchoolId'] as String),
|
||||||
schoolTerm: json['schoolTerm'] == null
|
schoolTerm: json['schoolTerm'] == null
|
||||||
? null
|
? null
|
||||||
: SchoolTerm.fromJson(
|
: SchoolTerm.fromJson(
|
||||||
@@ -181,6 +190,9 @@ class AppSnapshot {
|
|||||||
(k, v) => MapEntry(k as String, v as String),
|
(k, v) => MapEntry(k as String, v as String),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if(snapshot.schoolTerm?.school != SupportedSchool.uestcUndergraduate &&
|
||||||
|
values.any((c)=>c.endPeriod>11)) throw const FormatException('第12节需要本科课表来源');
|
||||||
|
return snapshot;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw const FormatException('保存文件内容无效,未覆盖已有数据');
|
throw const FormatException('保存文件内容无效,未覆盖已有数据');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ class Course {
|
|||||||
required this.endPeriod,
|
required this.endPeriod,
|
||||||
required Set<int> weeks,
|
required Set<int> weeks,
|
||||||
required this.source,
|
required this.source,
|
||||||
|
this.courseCode = '',
|
||||||
|
this.notes = '',
|
||||||
}) : weeks = Set.unmodifiable(weeks) {
|
}) : weeks = Set.unmodifiable(weeks) {
|
||||||
if ([id, name, teacher, room].any((value) => value.trim().isEmpty) ||
|
if ([id, name, teacher, room].any((value) => value.trim().isEmpty) ||
|
||||||
weekday < 1 ||
|
weekday < 1 ||
|
||||||
weekday > 7 ||
|
weekday > 7 ||
|
||||||
startPeriod < 1 ||
|
startPeriod < 1 ||
|
||||||
endPeriod > 11 ||
|
endPeriod > 12 ||
|
||||||
endPeriod < startPeriod ||
|
endPeriod < startPeriod ||
|
||||||
weeks.isEmpty ||
|
weeks.isEmpty ||
|
||||||
weeks.any((week) => week < 1 || week > 30)) {
|
weeks.any((week) => week < 1 || week > 30)) {
|
||||||
@@ -24,6 +26,7 @@ class Course {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final String id, name, teacher, room;
|
final String id, name, teacher, room;
|
||||||
|
final String courseCode, notes;
|
||||||
final int weekday, startPeriod, endPeriod;
|
final int weekday, startPeriod, endPeriod;
|
||||||
final Set<int> weeks;
|
final Set<int> weeks;
|
||||||
final CourseSource source;
|
final CourseSource source;
|
||||||
@@ -38,6 +41,8 @@ class Course {
|
|||||||
'endPeriod': endPeriod,
|
'endPeriod': endPeriod,
|
||||||
'weeks': weeks.toList()..sort(),
|
'weeks': weeks.toList()..sort(),
|
||||||
'source': source.name,
|
'source': source.name,
|
||||||
|
'courseCode': courseCode,
|
||||||
|
'notes': notes,
|
||||||
};
|
};
|
||||||
|
|
||||||
static Course fromJson(Map<String, dynamic> json, {CourseSource? source}) {
|
static Course fromJson(Map<String, dynamic> json, {CourseSource? source}) {
|
||||||
@@ -62,6 +67,8 @@ class Course {
|
|||||||
endPeriod: json['endPeriod'] as int,
|
endPeriod: json['endPeriod'] as int,
|
||||||
weeks: rawWeeks.cast<int>().toSet(),
|
weeks: rawWeeks.cast<int>().toSet(),
|
||||||
source: source ?? CourseSource.values.byName(json['source'] as String),
|
source: source ?? CourseSource.values.byName(json['source'] as String),
|
||||||
|
courseCode: (json['courseCode'] ?? '') as String,
|
||||||
|
notes: (json['notes'] ?? '') as String,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw const FormatException('课程字段、节次或周次无效');
|
throw const FormatException('课程字段、节次或周次无效');
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'school_identity.dart';
|
||||||
|
import 'school_term.dart';
|
||||||
|
|
||||||
class ExamRecord {
|
class ExamRecord {
|
||||||
const ExamRecord({
|
const ExamRecord({
|
||||||
required this.code,
|
required this.code,
|
||||||
@@ -8,8 +12,12 @@ class ExamRecord {
|
|||||||
this.room = '',
|
this.room = '',
|
||||||
this.seat = '',
|
this.seat = '',
|
||||||
this.time = '',
|
this.time = '',
|
||||||
|
this.date = '', this.status = '', this.notes = '', this.examTypeId,
|
||||||
});
|
});
|
||||||
final String code, name, assessment, candidate, sequence, room, seat, time;
|
final String code, name, assessment, candidate, sequence, room, seat, time;
|
||||||
|
final String date, status, notes;
|
||||||
|
final int? examTypeId;
|
||||||
|
String get undergraduateKey => jsonEncode([examTypeId,code,date,time,room,seat]);
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'code': code,
|
'code': code,
|
||||||
'name': name,
|
'name': name,
|
||||||
@@ -19,10 +27,11 @@ class ExamRecord {
|
|||||||
'room': room,
|
'room': room,
|
||||||
'seat': seat,
|
'seat': seat,
|
||||||
'time': time,
|
'time': time,
|
||||||
|
'date':date, 'status':status, 'notes':notes, 'examTypeId':examTypeId,
|
||||||
};
|
};
|
||||||
factory ExamRecord.fromJson(Map<String, dynamic> json) {
|
factory ExamRecord.fromJson(Map<String, dynamic> json) {
|
||||||
String field(String key) {
|
String field(String key, {bool optional=false}) {
|
||||||
final value = json[key];
|
final value = optional && !json.containsKey(key) ? '' : json[key];
|
||||||
if (value is! String || value.length > 1000) {
|
if (value is! String || value.length > 1000) {
|
||||||
throw const FormatException('考试信息无效');
|
throw const FormatException('考试信息无效');
|
||||||
}
|
}
|
||||||
@@ -38,6 +47,8 @@ class ExamRecord {
|
|||||||
room: field('room'),
|
room: field('room'),
|
||||||
seat: field('seat'),
|
seat: field('seat'),
|
||||||
time: field('time'),
|
time: field('time'),
|
||||||
|
date:field('date',optional:true), status:field('status',optional:true), notes:field('notes',optional:true),
|
||||||
|
examTypeId:json['examTypeId'] as int?,
|
||||||
);
|
);
|
||||||
if (record.code.isEmpty || record.name.isEmpty) {
|
if (record.code.isEmpty || record.name.isEmpty) {
|
||||||
throw const FormatException('考试课程无效');
|
throw const FormatException('考试课程无效');
|
||||||
@@ -51,15 +62,21 @@ class ExamSchedule {
|
|||||||
required this.semesterLabel,
|
required this.semesterLabel,
|
||||||
required this.termId,
|
required this.termId,
|
||||||
required List<ExamRecord> records,
|
required List<ExamRecord> records,
|
||||||
}) : records = List.unmodifiable(records);
|
this.school = SupportedSchool.uestcGraduate,
|
||||||
|
Set<int> requestedExamTypes = const {},
|
||||||
|
}) : records = List.unmodifiable(records), requestedExamTypes=Set.unmodifiable(requestedExamTypes);
|
||||||
final String semesterLabel, termId;
|
final String semesterLabel, termId;
|
||||||
final List<ExamRecord> records;
|
final List<ExamRecord> records;
|
||||||
|
final SupportedSchool school;
|
||||||
|
final Set<int> requestedExamTypes;
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'semesterLabel': semesterLabel,
|
'semesterLabel': semesterLabel,
|
||||||
'termId': termId,
|
'termId': termId,
|
||||||
'records': records.map((r) => r.toJson()).toList(),
|
'records': records.map((r) => r.toJson()).toList(),
|
||||||
|
'schoolId':school.id, 'requestedExamTypes':requestedExamTypes.toList()..sort(),
|
||||||
};
|
};
|
||||||
factory ExamSchedule.fromJson(Map<String, dynamic> json) {
|
factory ExamSchedule.fromJson(Map<String, dynamic> json) {
|
||||||
|
try {
|
||||||
final label = json['semesterLabel'],
|
final label = json['semesterLabel'],
|
||||||
id = json['termId'],
|
id = json['termId'],
|
||||||
rows = json['records'];
|
rows = json['records'];
|
||||||
@@ -74,9 +91,18 @@ class ExamSchedule {
|
|||||||
final records = rows
|
final records = rows
|
||||||
.map((r) => ExamRecord.fromJson(Map<String, dynamic>.from(r as Map)))
|
.map((r) => ExamRecord.fromJson(Map<String, dynamic>.from(r as Map)))
|
||||||
.toList();
|
.toList();
|
||||||
if (records.map((r) => r.code).toSet().length != records.length) {
|
final school=json['schoolId']==null ? SupportedSchool.uestcGraduate : SupportedSchool.fromId(json['schoolId'] as String);
|
||||||
|
final rawTypes=json['requestedExamTypes'] ?? <int>[];
|
||||||
|
if(rawTypes is! List || rawTypes.any((t)=>t is! int || t<1 || t>3)) throw const FormatException('考试类型无效');
|
||||||
|
final types=rawTypes.cast<int>().toSet();
|
||||||
|
SchoolTerm(int.parse(id),school:school);
|
||||||
|
if(school==SupportedSchool.uestcUndergraduate && (types.isEmpty || records.any((r)=>!types.contains(r.examTypeId)))) {
|
||||||
|
throw const FormatException('本科考试类型无效');
|
||||||
|
}
|
||||||
|
if (records.map((r) => school==SupportedSchool.uestcGraduate ? r.code : r.undergraduateKey).toSet().length != records.length) {
|
||||||
throw const FormatException('考试课程重复');
|
throw const FormatException('考试课程重复');
|
||||||
}
|
}
|
||||||
return ExamSchedule(semesterLabel: label, termId: id, records: records);
|
return ExamSchedule(semesterLabel: label, termId: id, records: records,school:school,requestedExamTypes:types);
|
||||||
|
} catch (_) { throw const FormatException('考试列表无效'); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
enum SupportedSchool {
|
||||||
|
uestcGraduate('uestc_graduate', '电子科技大学(研究生)', '研究生课表与考试安排'),
|
||||||
|
uestcUndergraduate('uestc_undergraduate', '电子科技大学(本科生)', '手动导入课表 · 自动查询考试');
|
||||||
|
|
||||||
|
const SupportedSchool(this.id, this.label, this.description);
|
||||||
|
final String id, label, description;
|
||||||
|
static SupportedSchool fromId(String id) => values.firstWhere(
|
||||||
|
(s) => s.id == id, orElse: () => throw const FormatException('学校标识无效'));
|
||||||
|
}
|
||||||
@@ -1,27 +1,54 @@
|
|||||||
/// The graduate system's term number, independent of teaching-week dates.
|
import 'school_identity.dart';
|
||||||
|
|
||||||
|
/// School-specific term numbers, independent of teaching-week dates.
|
||||||
class SchoolTerm {
|
class SchoolTerm {
|
||||||
SchoolTerm(this.id) {
|
SchoolTerm(this.id, {this.school = SupportedSchool.uestcGraduate}) {
|
||||||
if (id < 1 || id > 9999) throw ArgumentError('学校学期编号无效');
|
if (id < 1 || id > 9999 ||
|
||||||
|
(school == SupportedSchool.uestcUndergraduate && (id < 3 || (id-3)%20 != 0))) {
|
||||||
|
throw ArgumentError('学校学期编号无效');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final int id;
|
final int id;
|
||||||
|
final SupportedSchool school;
|
||||||
String get label {
|
String get label {
|
||||||
|
if (school == SupportedSchool.uestcUndergraduate) {
|
||||||
|
final n = (id - 3) ~/ 20, year = 2013 + ((id - 3) ~/ 40);
|
||||||
|
return '$year-${year+1}${n.isEven ? '第一学期' : '第二学期'}';
|
||||||
|
}
|
||||||
final year = 1990 + ((id - 1) ~/ 2);
|
final year = 1990 + ((id - 1) ~/ 2);
|
||||||
return '$year-${year + 1}年${id.isOdd ? '秋' : '春'}';
|
return '$year-${year + 1}年${id.isOdd ? '秋' : '春'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<SchoolTerm> get choices =>
|
static List<SchoolTerm> get choices =>
|
||||||
List.generate(73, (index) => SchoolTerm(73 - index));
|
List.generate(73, (index) => SchoolTerm(73 - index));
|
||||||
Uri get timetableUri =>
|
static List<SchoolTerm> undergraduateChoices(DateTime now, {SchoolTerm? saved}) {
|
||||||
Uri.https('yjsjy.uestc.edu.cn', '/pyxx/pygl/xskbcx/index/$id');
|
final maxN = (now.year + 1 - 2013)*2+1;
|
||||||
Uri get examUri =>
|
final ids = {for(var n=0;n<=maxN;n++) 3+n*20,
|
||||||
Uri.https('yjsjy.uestc.edu.cn', '/pyxx/pygl/kskcxx/index', {'xq': '$id'});
|
if(saved?.school == SupportedSchool.uestcUndergraduate) saved!.id}.toList()
|
||||||
Map<String, dynamic> toJson() => {'id': id};
|
..sort((a,b)=>b.compareTo(a));
|
||||||
|
return ids.where((id)=>id<=9999).map((id)=>SchoolTerm(id,school:SupportedSchool.uestcUndergraduate)).toList();
|
||||||
|
}
|
||||||
|
void _graduateOnly() {
|
||||||
|
if (school != SupportedSchool.uestcGraduate) throw StateError('本科查询不能使用研究生链接');
|
||||||
|
}
|
||||||
|
Uri get timetableUri {
|
||||||
|
_graduateOnly();
|
||||||
|
return Uri.https('yjsjy.uestc.edu.cn', '/pyxx/pygl/xskbcx/index/$id');
|
||||||
|
}
|
||||||
|
Uri get examUri {
|
||||||
|
_graduateOnly();
|
||||||
|
return Uri.https('yjsjy.uestc.edu.cn', '/pyxx/pygl/kskcxx/index', {'xq': '$id'});
|
||||||
|
}
|
||||||
|
Map<String, dynamic> toJson() => {'id': id, 'schoolId': school.id};
|
||||||
factory SchoolTerm.fromJson(Map<String, dynamic> json) {
|
factory SchoolTerm.fromJson(Map<String, dynamic> json) {
|
||||||
if (json['id'] is! int) throw const FormatException('学校学期无效');
|
if (json['id'] is! int) throw const FormatException('学校学期无效');
|
||||||
return SchoolTerm(json['id'] as int);
|
try {
|
||||||
|
return SchoolTerm(json['id'] as int, school: json['schoolId'] == null
|
||||||
|
? SupportedSchool.uestcGraduate : SupportedSchool.fromId(json['schoolId'] as String));
|
||||||
|
} catch (_) { throw const FormatException('学校学期无效'); }
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => other is SchoolTerm && other.id == id;
|
bool operator ==(Object other) => other is SchoolTerm && other.id == id && other.school == school;
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode;
|
int get hashCode => Object.hash(id, school);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'school_identity.dart';
|
||||||
|
import 'school_term.dart';
|
||||||
|
import 'exam_schedule.dart';
|
||||||
|
|
||||||
|
enum UndergraduateExamType {
|
||||||
|
finalExam(1,'期末考试'), midterm(2,'期中考试'), makeup(3,'补考/缓考');
|
||||||
|
const UndergraduateExamType(this.id, this.label);
|
||||||
|
final int id;
|
||||||
|
final String label;
|
||||||
|
static UndergraduateExamType fromId(int id) => values.firstWhere((t)=>t.id==id,
|
||||||
|
orElse:()=>throw const FormatException('考试类型无效'));
|
||||||
|
}
|
||||||
|
|
||||||
|
class UndergraduateExamQuery {
|
||||||
|
UndergraduateExamQuery({required this.term, required Set<UndergraduateExamType> types})
|
||||||
|
: types=Set.unmodifiable(types) {
|
||||||
|
if(term.school != SupportedSchool.uestcUndergraduate || types.isEmpty) throw ArgumentError('请选择本科学期和考试类型');
|
||||||
|
}
|
||||||
|
final SchoolTerm term;
|
||||||
|
final Set<UndergraduateExamType> types;
|
||||||
|
List<UndergraduateExamType> get orderedTypes => UndergraduateExamType.values.where(types.contains).toList();
|
||||||
|
Uri uriFor(UndergraduateExamType type) {
|
||||||
|
if(!types.contains(type)) throw ArgumentError('考试类型不在查询选项中');
|
||||||
|
return Uri.https('eams.uestc.edu.cn','/eams/stdExamTable!examTable.action',{'semester.id':'${term.id}','examType.id':'${type.id}'});
|
||||||
|
}
|
||||||
|
static UndergraduateExamQuery? fromSchedule(ExamSchedule? schedule) {
|
||||||
|
if(schedule?.school != SupportedSchool.uestcUndergraduate) return null;
|
||||||
|
return UndergraduateExamQuery(term:SchoolTerm(int.parse(schedule!.termId),school:schedule.school),
|
||||||
|
types:schedule.requestedExamTypes.map(UndergraduateExamType.fromId).toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../app/app_theme.dart';
|
import '../../app/app_theme.dart';
|
||||||
|
import '../../domain/school_identity.dart';
|
||||||
/// Only list schools whose login and import workflow is available.
|
export '../../domain/school_identity.dart';
|
||||||
enum SupportedSchool {
|
|
||||||
uestcGraduate('电子科技大学(研究生)', '研究生课表与考试安排');
|
|
||||||
|
|
||||||
const SupportedSchool(this.label, this.description);
|
|
||||||
final String label, description;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchoolSelectionPage extends StatelessWidget {
|
class SchoolSelectionPage extends StatelessWidget {
|
||||||
const SchoolSelectionPage({super.key, this.selected});
|
const SchoolSelectionPage({super.key, this.selected});
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hepan_everyday/domain/school_identity.dart';
|
||||||
|
import 'package:hepan_everyday/domain/school_term.dart';
|
||||||
|
import 'package:hepan_everyday/domain/undergraduate_exam_query.dart';
|
||||||
|
import 'package:hepan_everyday/domain/exam_schedule.dart';
|
||||||
|
import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||||
|
import 'package:hepan_everyday/domain/course.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
SchoolTerm under(int id) => SchoolTerm(id, school: SupportedSchool.uestcUndergraduate);
|
||||||
|
test('undergraduate semester labels and final exam URLs', () {
|
||||||
|
expect([483,503,523,543,563].map((id)=>under(id).label).toList(),
|
||||||
|
['2025-2026第一学期','2025-2026第二学期','2026-2027第一学期','2026-2027第二学期','2027-2028第一学期']);
|
||||||
|
final query = UndergraduateExamQuery(term: under(523), types: {UndergraduateExamType.finalExam});
|
||||||
|
expect(query.uriFor(UndergraduateExamType.finalExam).toString(), 'https://eams.uestc.edu.cn/eams/stdExamTable!examTable.action?semester.id=523&examType.id=1');
|
||||||
|
expect(() => UndergraduateExamQuery(term: under(523),types: {}),throwsArgumentError);
|
||||||
|
expect(() => under(504), throwsArgumentError);
|
||||||
|
expect(under(523), isNot(SchoolTerm(523)));
|
||||||
|
expect(()=>under(523).examUri, throwsStateError);
|
||||||
|
expect(SchoolTerm.fromJson({'id':73}).school, SupportedSchool.uestcGraduate);
|
||||||
|
expect(SchoolTerm.undergraduateChoices(DateTime(2026,10)).first.id, 583);
|
||||||
|
});
|
||||||
|
test('undergraduate exam identity retains both types and rejects corruption', () {
|
||||||
|
final schedule=ExamSchedule(semesterLabel:under(523).label,termId:'523',school:SupportedSchool.uestcUndergraduate,
|
||||||
|
requestedExamTypes:{1,2},records:[const ExamRecord(code:'A',name:'课程',examTypeId:1), const ExamRecord(code:'A',name:'课程',examTypeId:2)]);
|
||||||
|
expect(ExamSchedule.fromJson(schedule.toJson()).records,hasLength(2));
|
||||||
|
expect(()=>ExamSchedule.fromJson({...schedule.toJson(),'requestedExamTypes':[]}),throwsFormatException);
|
||||||
|
expect(()=>ExamSchedule.fromJson({...schedule.toJson(),'schoolId':'unknown'}),throwsFormatException);
|
||||||
|
final old=ExamSchedule(semesterLabel:'2026秋',termId:'73',records:[const ExamRecord(code:'A',name:'课程'),const ExamRecord(code:'A',name:'课程')]);
|
||||||
|
expect(()=>ExamSchedule.fromJson(old.toJson()),throwsFormatException);
|
||||||
|
});
|
||||||
|
test('snapshots persist school selection and validate period twelve source', () {
|
||||||
|
final course=Course(id:'under-523-test',name:'课程',teacher:'教师',room:'教室',weekday:1,startPeriod:9,endPeriod:12,weeks:{1},source:CourseSource.school,courseCode:'A',notes:'备注');
|
||||||
|
final saved=AppSnapshot(selectedSchool:SupportedSchool.uestcUndergraduate,schoolTerm:under(523),courses:[course]);
|
||||||
|
final round=AppSnapshot.fromJson(saved.toJson());
|
||||||
|
expect(round.selectedSchool, SupportedSchool.uestcUndergraduate);
|
||||||
|
expect(round.courses.single.notes,'备注');
|
||||||
|
expect(()=>AppSnapshot.fromJson({...saved.toJson(),'schoolTerm':{'id':73}}),throwsFormatException);
|
||||||
|
expect(()=>AppSnapshot.fromJson({...saved.toJson(),'selectedSchoolId':'unknown'}),throwsFormatException);
|
||||||
|
final legacy=AppSnapshot(schoolTerm:SchoolTerm(73)).toJson()..remove('selectedSchoolId');
|
||||||
|
expect(AppSnapshot.fromJson(legacy).selectedSchool,SupportedSchool.uestcGraduate);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user