feat: keep undergraduate exams independent of course terms
This commit is contained in:
@@ -28,8 +28,11 @@ class AppSnapshot {
|
||||
Map<String, String> calendarEventIds = const {},
|
||||
}) : settings = settings ?? AppSettings(),
|
||||
schoolTerm = schoolTerm ?? _legacyTerm(courses, examSchedule),
|
||||
selectedSchool = selectedSchool ?? schoolTerm?.school ??
|
||||
_legacyTerm(courses, examSchedule)?.school ?? examSchedule?.school,
|
||||
selectedSchool =
|
||||
selectedSchool ??
|
||||
schoolTerm?.school ??
|
||||
_legacyTerm(courses, examSchedule)?.school ??
|
||||
examSchedule?.school,
|
||||
courses = List.unmodifiable(courses),
|
||||
calendarEventIds = Map.unmodifiable(calendarEventIds);
|
||||
final AppSettings settings;
|
||||
@@ -75,13 +78,13 @@ class AppSnapshot {
|
||||
calendarEventIds: calendarEventIds ?? this.calendarEventIds,
|
||||
calendarCleanupPending:
|
||||
calendarCleanupPending ?? this.calendarCleanupPending,
|
||||
selectedSchool:selectedSchool ?? this.selectedSchool,
|
||||
selectedSchool: selectedSchool ?? this.selectedSchool,
|
||||
);
|
||||
Map<String, dynamic> toJson() => {
|
||||
'schemaVersion': 1,
|
||||
'examSchedule': examSchedule?.toJson(),
|
||||
'schoolTerm': schoolTerm?.toJson(),
|
||||
'selectedSchoolId':selectedSchool?.id,
|
||||
'selectedSchoolId': selectedSchool?.id,
|
||||
'courses': courses.map((c) => c.toJson()).toList(),
|
||||
'settings': {
|
||||
'semester': settings.semester == null
|
||||
@@ -153,7 +156,9 @@ class AppSnapshot {
|
||||
);
|
||||
final metadata = json['importMetadata'];
|
||||
final snapshot = AppSnapshot(
|
||||
selectedSchool:json['selectedSchoolId']==null ? null : SupportedSchool.fromId(json['selectedSchoolId'] as String),
|
||||
selectedSchool: json['selectedSchoolId'] == null
|
||||
? null
|
||||
: SupportedSchool.fromId(json['selectedSchoolId'] as String),
|
||||
schoolTerm: json['schoolTerm'] == null
|
||||
? null
|
||||
: SchoolTerm.fromJson(
|
||||
@@ -190,8 +195,9 @@ class AppSnapshot {
|
||||
(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节需要本科课表来源');
|
||||
if (snapshot.schoolTerm?.school != SupportedSchool.uestcUndergraduate &&
|
||||
values.any((c) => c.endPeriod > 11))
|
||||
throw const FormatException('第12节需要本科课表来源');
|
||||
return snapshot;
|
||||
} catch (_) {
|
||||
throw const FormatException('保存文件内容无效,未覆盖已有数据');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'school_identity.dart';
|
||||
import 'school_term.dart';
|
||||
|
||||
@@ -12,12 +13,16 @@ class ExamRecord {
|
||||
this.room = '',
|
||||
this.seat = '',
|
||||
this.time = '',
|
||||
this.date = '', this.status = '', this.notes = '', this.examTypeId,
|
||||
this.date = '',
|
||||
this.status = '',
|
||||
this.notes = '',
|
||||
this.examTypeId,
|
||||
});
|
||||
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]);
|
||||
String get undergraduateKey =>
|
||||
jsonEncode([examTypeId, code, date, time, room, seat]);
|
||||
Map<String, dynamic> toJson() => {
|
||||
'code': code,
|
||||
'name': name,
|
||||
@@ -27,10 +32,13 @@ class ExamRecord {
|
||||
'room': room,
|
||||
'seat': seat,
|
||||
'time': time,
|
||||
'date':date, 'status':status, 'notes':notes, 'examTypeId':examTypeId,
|
||||
'date': date,
|
||||
'status': status,
|
||||
'notes': notes,
|
||||
'examTypeId': examTypeId,
|
||||
};
|
||||
factory ExamRecord.fromJson(Map<String, dynamic> json) {
|
||||
String field(String key, {bool optional=false}) {
|
||||
String field(String key, {bool optional = false}) {
|
||||
final value = optional && !json.containsKey(key) ? '' : json[key];
|
||||
if (value is! String || value.length > 1000) {
|
||||
throw const FormatException('考试信息无效');
|
||||
@@ -47,8 +55,10 @@ class ExamRecord {
|
||||
room: field('room'),
|
||||
seat: field('seat'),
|
||||
time: field('time'),
|
||||
date:field('date',optional:true), status:field('status',optional:true), notes:field('notes',optional:true),
|
||||
examTypeId:json['examTypeId'] as int?,
|
||||
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) {
|
||||
throw const FormatException('考试课程无效');
|
||||
@@ -64,7 +74,8 @@ class ExamSchedule {
|
||||
required List<ExamRecord> records,
|
||||
this.school = SupportedSchool.uestcGraduate,
|
||||
Set<int> requestedExamTypes = const {},
|
||||
}) : records = List.unmodifiable(records), requestedExamTypes=Set.unmodifiable(requestedExamTypes);
|
||||
}) : records = List.unmodifiable(records),
|
||||
requestedExamTypes = Set.unmodifiable(requestedExamTypes);
|
||||
final String semesterLabel, termId;
|
||||
final List<ExamRecord> records;
|
||||
final SupportedSchool school;
|
||||
@@ -73,36 +84,58 @@ class ExamSchedule {
|
||||
'semesterLabel': semesterLabel,
|
||||
'termId': termId,
|
||||
'records': records.map((r) => r.toJson()).toList(),
|
||||
'schoolId':school.id, 'requestedExamTypes':requestedExamTypes.toList()..sort(),
|
||||
'schoolId': school.id,
|
||||
'requestedExamTypes': requestedExamTypes.toList()..sort(),
|
||||
};
|
||||
factory ExamSchedule.fromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
final label = json['semesterLabel'],
|
||||
id = json['termId'],
|
||||
rows = json['records'];
|
||||
if (label is! String ||
|
||||
label.trim().isEmpty ||
|
||||
id is! String ||
|
||||
!RegExp(r'^\d+$').hasMatch(id) ||
|
||||
rows is! List ||
|
||||
rows.length > 500) {
|
||||
final label = json['semesterLabel'],
|
||||
id = json['termId'],
|
||||
rows = json['records'];
|
||||
if (label is! String ||
|
||||
label.trim().isEmpty ||
|
||||
id is! String ||
|
||||
!RegExp(r'^\d+$').hasMatch(id) ||
|
||||
rows is! List ||
|
||||
rows.length > 500) {
|
||||
throw const FormatException('考试列表无效');
|
||||
}
|
||||
final records = rows
|
||||
.map((r) => ExamRecord.fromJson(Map<String, dynamic>.from(r as Map)))
|
||||
.toList();
|
||||
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('考试课程重复');
|
||||
}
|
||||
return ExamSchedule(
|
||||
semesterLabel: label,
|
||||
termId: id,
|
||||
records: records,
|
||||
school: school,
|
||||
requestedExamTypes: types,
|
||||
);
|
||||
} catch (_) {
|
||||
throw const FormatException('考试列表无效');
|
||||
}
|
||||
final records = rows
|
||||
.map((r) => ExamRecord.fromJson(Map<String, dynamic>.from(r as Map)))
|
||||
.toList();
|
||||
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('考试课程重复');
|
||||
}
|
||||
return ExamSchedule(semesterLabel: label, termId: id, records: records,school:school,requestedExamTypes:types);
|
||||
} catch (_) { throw const FormatException('考试列表无效'); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,7 @@ enum SupportedSchool {
|
||||
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('学校标识无效'));
|
||||
(s) => s.id == id,
|
||||
orElse: () => throw const FormatException('学校标识无效'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'school_identity.dart';
|
||||
/// School-specific term numbers, independent of teaching-week dates.
|
||||
class SchoolTerm {
|
||||
SchoolTerm(this.id, {this.school = SupportedSchool.uestcGraduate}) {
|
||||
if (id < 1 || id > 9999 ||
|
||||
(school == SupportedSchool.uestcUndergraduate && (id < 3 || (id-3)%20 != 0))) {
|
||||
if (id < 1 ||
|
||||
id > 9999 ||
|
||||
(school == SupportedSchool.uestcUndergraduate &&
|
||||
(id < 3 || (id - 3) % 20 != 0))) {
|
||||
throw ArgumentError('学校学期编号无效');
|
||||
}
|
||||
}
|
||||
@@ -13,7 +15,7 @@ class SchoolTerm {
|
||||
String get label {
|
||||
if (school == SupportedSchool.uestcUndergraduate) {
|
||||
final n = (id - 3) ~/ 20, year = 2013 + ((id - 3) ~/ 40);
|
||||
return '$year-${year+1}${n.isEven ? '第一学期' : '第二学期'}';
|
||||
return '$year-${year + 1}${n.isEven ? '第一学期' : '第二学期'}';
|
||||
}
|
||||
final year = 1990 + ((id - 1) ~/ 2);
|
||||
return '$year-${year + 1}年${id.isOdd ? '秋' : '春'}';
|
||||
@@ -21,34 +23,55 @@ class SchoolTerm {
|
||||
|
||||
static List<SchoolTerm> get choices =>
|
||||
List.generate(73, (index) => SchoolTerm(73 - index));
|
||||
static List<SchoolTerm> undergraduateChoices(DateTime now, {SchoolTerm? saved}) {
|
||||
final maxN = (now.year + 1 - 2013)*2+1;
|
||||
final ids = {for(var n=0;n<=maxN;n++) 3+n*20,
|
||||
if(saved?.school == SupportedSchool.uestcUndergraduate) saved!.id}.toList()
|
||||
..sort((a,b)=>b.compareTo(a));
|
||||
return ids.where((id)=>id<=9999).map((id)=>SchoolTerm(id,school:SupportedSchool.uestcUndergraduate)).toList();
|
||||
static List<SchoolTerm> undergraduateChoices(
|
||||
DateTime now, {
|
||||
SchoolTerm? saved,
|
||||
}) {
|
||||
final maxN = (now.year + 1 - 2013) * 2 + 1;
|
||||
final ids = {
|
||||
for (var n = 0; n <= maxN; n++) 3 + n * 20,
|
||||
if (saved?.school == SupportedSchool.uestcUndergraduate) saved!.id,
|
||||
}.toList()..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('本科查询不能使用研究生链接');
|
||||
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'});
|
||||
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) {
|
||||
if (json['id'] is! int) throw const FormatException('学校学期无效');
|
||||
try {
|
||||
return SchoolTerm(json['id'] as int, school: json['schoolId'] == null
|
||||
? SupportedSchool.uestcGraduate : SupportedSchool.fromId(json['schoolId'] as String));
|
||||
} catch (_) { throw const FormatException('学校学期无效'); }
|
||||
return SchoolTerm(
|
||||
json['id'] as int,
|
||||
school: json['schoolId'] == null
|
||||
? SupportedSchool.uestcGraduate
|
||||
: SupportedSchool.fromId(json['schoolId'] as String),
|
||||
);
|
||||
} catch (_) {
|
||||
throw const FormatException('学校学期无效');
|
||||
}
|
||||
}
|
||||
@override
|
||||
bool operator ==(Object other) => other is SchoolTerm && other.id == id && other.school == school;
|
||||
bool operator ==(Object other) =>
|
||||
other is SchoolTerm && other.id == id && other.school == school;
|
||||
@override
|
||||
int get hashCode => Object.hash(id, school);
|
||||
}
|
||||
|
||||
@@ -3,29 +3,47 @@ import 'school_term.dart';
|
||||
import 'exam_schedule.dart';
|
||||
|
||||
enum UndergraduateExamType {
|
||||
finalExam(1,'期末考试'), midterm(2,'期中考试'), makeup(3,'补考/缓考');
|
||||
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('考试类型无效'));
|
||||
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('请选择本科学期和考试类型');
|
||||
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();
|
||||
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}'});
|
||||
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());
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user