feat: parse undergraduate timetable activity data
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
import 'json_schedule_import.dart';
|
||||
import '../domain/course.dart';
|
||||
import '../features/school/school_page_adapter.dart';
|
||||
import '../features/school/undergraduate/timetable_adapter.dart';
|
||||
|
||||
class ScheduleFileImport {
|
||||
static ImportDraft parse(String content) {
|
||||
final trimmed = content.replaceFirst('\uFEFF', '').trimLeft();
|
||||
if (trimmed.startsWith('<')) {
|
||||
if (trimmed.contains('manualArrangeCourseTable')) {
|
||||
return UndergraduateTimetableAdapter.extract(
|
||||
trimmed,
|
||||
Uri.parse('https://eams.uestc.edu.cn/eams/courseTableForStd.action'),
|
||||
source: CourseSource.file,
|
||||
);
|
||||
}
|
||||
return SchoolPageAdapter().extract(
|
||||
trimmed,
|
||||
Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1'),
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
class UndergraduateActivity {
|
||||
UndergraduateActivity(
|
||||
this.teacher,
|
||||
this.name,
|
||||
this.courseCode,
|
||||
this.room,
|
||||
this.indices,
|
||||
this.weeks,
|
||||
);
|
||||
final String teacher, name, courseCode, room;
|
||||
final Set<int> indices, weeks;
|
||||
bool get cancelled => room == '停课';
|
||||
}
|
||||
|
||||
List<UndergraduateActivity> parseActivityData(
|
||||
String script, {
|
||||
required int unitCount,
|
||||
}) {
|
||||
if (unitCount != 12) throw const FormatException('本科节次结构无效');
|
||||
final matches = RegExp(
|
||||
r'new\s+TaskActivity\s*\((.*?)\)\s*;',
|
||||
dotAll: true,
|
||||
).allMatches(script).toList();
|
||||
final result = <UndergraduateActivity>[];
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
final m = matches[i], args = _strings(m[1]!);
|
||||
if (args.length != 7) throw const FormatException('课程活动字段不完整');
|
||||
final suffix = RegExp(r'\(([A-Za-z0-9_.-]+)\)$').firstMatch(args[3]);
|
||||
if (suffix == null || args[1].trim().isEmpty || args[5].trim().isEmpty)
|
||||
throw const FormatException('教师、教室或课程序号缺失');
|
||||
final bitmap = args[6];
|
||||
if (!RegExp(r'^0[01]+$').hasMatch(bitmap) ||
|
||||
bitmap.length > 64 ||
|
||||
(bitmap.length > 31 && bitmap.substring(31).contains('1')))
|
||||
throw const FormatException('上课周次超出1–30周');
|
||||
final weeks = {
|
||||
for (var w = 1; w < bitmap.length && w <= 30; w++)
|
||||
if (bitmap[w] == '1') w,
|
||||
};
|
||||
if (weeks.isEmpty) throw const FormatException('课程周次为空');
|
||||
var end = i + 1 < matches.length ? matches[i + 1].start : script.length;
|
||||
final finish = script.indexOf('table0.marshalTable', m.end);
|
||||
if (finish >= 0 && finish < end) end = finish;
|
||||
final body = script.substring(m.end, end);
|
||||
final indices = <int>{};
|
||||
final assignments = RegExp(r'\bindex\s*=\s*([^;]+);')
|
||||
.allMatches(body)
|
||||
.toList();
|
||||
final appends = RegExp(
|
||||
r'table\d+\.activities\[index\]\s*\[[^;]+?\]\s*=\s*activity\s*;',
|
||||
).allMatches(body).length;
|
||||
if (assignments.isEmpty || assignments.length != appends)
|
||||
throw const FormatException('课程节次赋值不完整');
|
||||
for (final assignment in assignments) {
|
||||
final expression = RegExp(r'^\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)\s*$')
|
||||
.firstMatch(assignment[1]!);
|
||||
if (expression == null) throw const FormatException('不支持的节次表达式');
|
||||
final day = int.parse(expression[1]!), period = int.parse(expression[2]!);
|
||||
if (day > 6 || period >= unitCount)
|
||||
throw const FormatException('课程星期或节次无效');
|
||||
indices.add(day * unitCount + period);
|
||||
}
|
||||
result.add(
|
||||
UndergraduateActivity(
|
||||
args[1].trim(),
|
||||
args[3].substring(0, suffix.start).trim(),
|
||||
suffix[1]!,
|
||||
args[5].trim(),
|
||||
indices,
|
||||
weeks,
|
||||
),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Reads only quoted JS literals; expressions and calls are never evaluated.
|
||||
List<String> _strings(String input) {
|
||||
final out = <String>[];
|
||||
var p = 0;
|
||||
void spaces() {
|
||||
while (p < input.length && RegExp(r'\s').hasMatch(input[p])) {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
spaces();
|
||||
if (p >= input.length) break;
|
||||
final quote = input[p++];
|
||||
if (quote != '"' && quote != "'") throw const FormatException('活动参数必须为字符串');
|
||||
final b = StringBuffer();
|
||||
var closed = false;
|
||||
while (p < input.length) {
|
||||
var c = input[p++];
|
||||
if (c == quote) {
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
if (c == '\\') {
|
||||
if (p >= input.length) throw const FormatException('字符串转义不完整');
|
||||
c = input[p++];
|
||||
const escapes = {
|
||||
'n': '\n',
|
||||
'r': '\r',
|
||||
't': '\t',
|
||||
'b': '\b',
|
||||
'f': '\f',
|
||||
'v': '\u000b',
|
||||
'0': '\u0000',
|
||||
};
|
||||
if (c == 'u' || c == 'x') {
|
||||
final count = c == 'u' ? 4 : 2;
|
||||
if (p + count > input.length) throw const FormatException('字符串转义无效');
|
||||
final hex = input.substring(p, p + count);
|
||||
if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(hex))
|
||||
throw const FormatException('字符串转义无效');
|
||||
b.writeCharCode(int.parse(hex, radix: 16));
|
||||
p += count;
|
||||
} else if (escapes.containsKey(c)) {
|
||||
b.write(escapes[c]);
|
||||
} else if (const ['"', "'", '\\', '/'].contains(c)) {
|
||||
b.write(c);
|
||||
} else {
|
||||
throw const FormatException('不支持的字符串转义');
|
||||
}
|
||||
} else {
|
||||
if (c == '\n' || c == '\r') throw const FormatException('字符串未闭合');
|
||||
b.write(c);
|
||||
}
|
||||
}
|
||||
if (!closed) throw const FormatException('字符串未闭合');
|
||||
out.add(b.toString());
|
||||
spaces();
|
||||
if (p == input.length) break;
|
||||
if (input[p++] != ',') throw const FormatException('活动字段分隔无效');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:html/parser.dart' as html;
|
||||
|
||||
import '../../../data/json_schedule_import.dart';
|
||||
import '../../../domain/course.dart';
|
||||
import '../../../domain/school_identity.dart';
|
||||
import '../../../domain/school_term.dart';
|
||||
import 'activity_data.dart';
|
||||
|
||||
class UndergraduateTimetableAdapter {
|
||||
static ImportDraft extract(
|
||||
String content,
|
||||
Uri uri, {
|
||||
CourseSource source = CourseSource.school,
|
||||
}) {
|
||||
try {
|
||||
if (uri.scheme != 'https' ||
|
||||
uri.host != 'eams.uestc.edu.cn' ||
|
||||
uri.port != 443 ||
|
||||
uri.userInfo.isNotEmpty ||
|
||||
!uri.path.startsWith('/eams/') ||
|
||||
source == CourseSource.demo ||
|
||||
utf8.encode(content).length > 2 * 1024 * 1024)
|
||||
throw const FormatException('不是本科课表页面');
|
||||
final doc = html.parse(content),
|
||||
table = html
|
||||
.parse(content)
|
||||
.querySelector('#manualArrangeCourseTable');
|
||||
if (table == null || doc.querySelector('input[type="password"]') != null)
|
||||
throw const FormatException('请先在学校网页打开课表');
|
||||
final termIds = doc
|
||||
.querySelectorAll('input[name="semester.id"]')
|
||||
.map((e) => e.attributes['value'])
|
||||
.toSet();
|
||||
if (termIds.length != 1) throw const FormatException('课表学期不明确');
|
||||
final term = SchoolTerm(
|
||||
int.parse(termIds.single!),
|
||||
school: SupportedSchool.uestcUndergraduate,
|
||||
);
|
||||
final scripts = doc
|
||||
.querySelectorAll('script')
|
||||
.map((e) => e.text)
|
||||
.join('\n');
|
||||
final units = RegExp(r'\b(?:var\s+)?unitCount\s*=\s*(\d+)\s*;')
|
||||
.allMatches(scripts)
|
||||
.map((m) => int.parse(m[1]!))
|
||||
.toSet();
|
||||
final activities = units.length == 1
|
||||
? parseActivityData(scripts, unitCount: units.single)
|
||||
: <UndergraduateActivity>[];
|
||||
final explicitEmpty =
|
||||
activities.isEmpty && RegExp(r'暂无课程|无课程安排|没有课程').hasMatch(table.text);
|
||||
if (activities.isEmpty && !explicitEmpty)
|
||||
throw const FormatException('课表尚未加载完整');
|
||||
final roster = <String, ({String code, String notes})>{};
|
||||
for (final grid in doc.querySelectorAll('table')) {
|
||||
final rows = grid.querySelectorAll('tr');
|
||||
if (rows.isEmpty) continue;
|
||||
final headers = rows.first.children.map((e) => e.text.trim()).toList();
|
||||
if (!headers.contains('课程序号') || !headers.contains('课程代码')) continue;
|
||||
final seq = headers.indexOf('课程序号'),
|
||||
code = headers.indexOf('课程代码'),
|
||||
note = headers.indexOf('选课备注');
|
||||
for (final row in rows.skip(1)) {
|
||||
final cells = row.children;
|
||||
if (cells.length != headers.length)
|
||||
throw const FormatException('课程名单不完整');
|
||||
roster[cells[seq].text.trim()] = (
|
||||
code: cells[code].text.trim(),
|
||||
notes: note >= 0 ? cells[note].text.trim() : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
final cancellations = <String, Set<int>>{};
|
||||
for (final a in activities.where((a) => a.cancelled)) {
|
||||
for (final index in a.indices) {
|
||||
cancellations
|
||||
.putIfAbsent('${a.courseCode}/$index', () => {})
|
||||
.addAll(a.weeks);
|
||||
}
|
||||
}
|
||||
final cells =
|
||||
<String, ({UndergraduateActivity a, int index, Set<int> weeks})>{};
|
||||
for (final a in activities.where((a) => !a.cancelled)) {
|
||||
for (final index in a.indices) {
|
||||
final weeks = a.weeks.difference(
|
||||
cancellations['${a.courseCode}/$index'] ?? {},
|
||||
);
|
||||
if (weeks.isEmpty) continue;
|
||||
final key = jsonEncode([
|
||||
a.courseCode,
|
||||
a.name,
|
||||
a.teacher,
|
||||
a.room,
|
||||
index,
|
||||
]);
|
||||
final cell = cells.putIfAbsent(
|
||||
key,
|
||||
() => (a: a, index: index, weeks: <int>{}),
|
||||
);
|
||||
cell.weeks.addAll(weeks);
|
||||
}
|
||||
}
|
||||
final groups =
|
||||
<
|
||||
String,
|
||||
({UndergraduateActivity a, Set<int> weeks, List<int> indices})
|
||||
>{};
|
||||
for (final cell in cells.values) {
|
||||
final key = jsonEncode([
|
||||
cell.a.courseCode,
|
||||
cell.a.name,
|
||||
cell.a.teacher,
|
||||
cell.a.room,
|
||||
cell.index ~/ 12,
|
||||
cell.weeks.toList()..sort(),
|
||||
]);
|
||||
groups
|
||||
.putIfAbsent(
|
||||
key,
|
||||
() => (a: cell.a, weeks: cell.weeks, indices: <int>[]),
|
||||
)
|
||||
.indices
|
||||
.add(cell.index);
|
||||
}
|
||||
final courses = <Course>[];
|
||||
for (final group in groups.values) {
|
||||
final indices = group.indices..sort();
|
||||
for (var p = 0; p < indices.length;) {
|
||||
final first = indices[p];
|
||||
var last = first;
|
||||
p++;
|
||||
while (p < indices.length && indices[p] == last + 1) {
|
||||
last = indices[p++];
|
||||
}
|
||||
final a = group.a, info = roster[a.courseCode];
|
||||
final identity = jsonEncode([
|
||||
term.id,
|
||||
a.courseCode,
|
||||
a.name,
|
||||
a.teacher,
|
||||
a.room,
|
||||
first,
|
||||
last,
|
||||
group.weeks.toList()..sort(),
|
||||
]);
|
||||
courses.add(
|
||||
Course(
|
||||
id: 'uestc-undergraduate-${term.id}-${_hash(identity)}',
|
||||
name: a.name,
|
||||
teacher: a.teacher,
|
||||
room: a.room,
|
||||
weekday: first ~/ 12 + 1,
|
||||
startPeriod: first % 12 + 1,
|
||||
endPeriod: last % 12 + 1,
|
||||
weeks: group.weeks,
|
||||
source: source,
|
||||
courseCode: info?.code ?? a.courseCode,
|
||||
notes: info?.notes ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (courses.isEmpty && !explicitEmpty)
|
||||
throw const FormatException('没有识别到有效上课安排');
|
||||
return ImportDraft(
|
||||
courses: List.unmodifiable(courses),
|
||||
explicitlyEmpty: explicitEmpty,
|
||||
source: source,
|
||||
parserVersion: 'uestc-undergraduate-v1',
|
||||
schoolTerm: term,
|
||||
semesterLabel: term.label,
|
||||
);
|
||||
} on FormatException {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
throw const FormatException('本科课表结构或学期无效,原课表保持不变');
|
||||
}
|
||||
}
|
||||
|
||||
static String _hash(String value) {
|
||||
var h = 0x811c9dc5;
|
||||
for (final b in utf8.encode(value)) {
|
||||
h = ((h ^ b) * 0x01000193) & 0xffffffff;
|
||||
}
|
||||
return h.toRadixString(16).padLeft(8, '0');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<input name="semester.id" value="523"><table id="manualArrangeCourseTable"><tr><th>节次</th><th>星期一</th><th>星期二</th><th>星期三</th><th>星期四</th><th>星期五</th><th>星期六</th><th>星期日</th></tr></table><table id="gridFixture"><tr><th>序号</th><th>课程代码</th><th>课程名称</th><th>课程类别</th><th>学分</th><th>课程序号</th><th>教师</th><th>职称</th><th>教材信息</th><th>选课备注</th></tr><tr><td>1</td><td>C1</td><td>课程1</td><td>专业课</td><td>3</td><td>C1.01</td><td>教师1</td><td></td><td></td><td>上课时间:19:00-22:00</td></tr><tr><td>2</td><td>C2</td><td>课程2</td><td>专业课</td><td>3</td><td>C2.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>3</td><td>C3</td><td>课程3</td><td>专业课</td><td>3</td><td>C3.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>4</td><td>C4</td><td>课程4</td><td>专业课</td><td>3</td><td>C4.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>5</td><td>C5</td><td>课程5</td><td>专业课</td><td>3</td><td>C5.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>6</td><td>C6</td><td>课程6</td><td>专业课</td><td>3</td><td>C6.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>7</td><td>C7</td><td>课程7</td><td>专业课</td><td>3</td><td>C7.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>8</td><td>C8</td><td>课程8</td><td>专业课</td><td>3</td><td>C8.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>9</td><td>C9</td><td>课程9</td><td>专业课</td><td>3</td><td>C9.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>10</td><td>C10</td><td>课程10</td><td>专业课</td><td>3</td><td>C10.01</td><td>教师1</td><td></td><td></td><td></td></tr></table><script>var table0=new CourseTable(2026,84);var unitCount = 12;
|
||||
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室1","00000000000001000000000000000000000000000000000000000");
|
||||
index = 2*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 2*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室1","00000000001101000000000000000000000000000000000000000");
|
||||
index = 4*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 4*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室2","01110011110010000000000000000000000000000000000000000");
|
||||
index = 4*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 4*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室2","01111111111110000000000000000000000000000000000000000");
|
||||
index = 2*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 2*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T2","教师2","COURSE2","课程2(C2.01)","R2","教室3","01111000000000000000000000000000000000000000000000000");
|
||||
index = 1*unitCount+8; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+9; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+10; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+11; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T2","教师2","COURSE2","课程2(C2.01)","R2","教室4","00000111100000000000000000000000000000000000000000000");
|
||||
index = 1*unitCount+8; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+9; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+10; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+11; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T3","教师3","COURSE3","课程3(C3.01)","R3","教室5","01111011111110000000000000000000000000000000000000000");
|
||||
index = 3*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 3*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T3","教师3","COURSE3","课程3(C3.01)","R3","教室5","01111111111111000000000000000000000000000000000000000");
|
||||
index = 1*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T4","教师4","COURSE4","课程4(C4.01)","R4","教室6","01111011111000000000000000000000000000000000000000000");
|
||||
index = 3*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 3*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T4","教师4","COURSE4","课程4(C4.01)","R4","教室6","01111111111100000000000000000000000000000000000000000");
|
||||
index = 1*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T5","教师5","COURSE5","课程5(C5.01)","R5","教室7","01111111111111111110000000000000000000000000000000000");
|
||||
index = 0*unitCount+8; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 0*unitCount+9; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 0*unitCount+10; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 0*unitCount+11; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T6","教师6、教师7","COURSE6","课程6(C6.01)","R6","教室8","01111111111110000000000000000000000000000000000000000");
|
||||
index = 2*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 2*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T6","教师6、教师7","COURSE6","课程6(C6.01)","R6","教室8","01111111111110000000000000000000000000000000000000000");
|
||||
index = 0*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 0*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T7","教师8、教师9、教师10、教师7、教师11、教师12、教师13、教师14、教师15","COURSE7","课程7(C7.01)","R7","教室9","00001011111111111000000000000000000000000000000000000");
|
||||
index = 3*unitCount+6; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 3*unitCount+7; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T8","教师16","COURSE8","课程8(C8.01)","R8","教室10","01111101111110000000000000000000000000000000000000000");
|
||||
index = 1*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+6; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+7; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T8","教师16","COURSE8","课程8(C8.01)","R8","停课","00000010000000000000000000000000000000000000000000000");
|
||||
index = 1*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+6; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 1*unitCount+7; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T9","教师17","COURSE9","课程9(C9.01)","R9","教室12","00000000000000010000000000000000000000000000000000000");
|
||||
index = 6*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 6*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T10","教师18","COURSE10","课程10(C10.01)","R10","教室13","01111111111110000000000000000000000000000000000000000");
|
||||
index = 0*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 0*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
|
||||
activity = new TaskActivity("T10","教师18","COURSE10","课程10(C10.01)","R10","教室13","01111111111110000000000000000000000000000000000000000");
|
||||
index = 2*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
|
||||
index = 2*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;</script>
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:html/parser.dart' as html;
|
||||
import 'package:hepan_everyday/features/school/undergraduate/activity_data.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/features/school/undergraduate/timetable_adapter.dart';
|
||||
import 'package:hepan_everyday/data/schedule_file_import.dart';
|
||||
import 'package:hepan_everyday/domain/school_identity.dart';
|
||||
|
||||
void main() {
|
||||
final uri = Uri.parse(
|
||||
'https://eams.uestc.edu.cn/eams/courseTableForStd.action',
|
||||
);
|
||||
String fixture() =>
|
||||
File('test/fixtures/school/undergraduate-timetable.html')
|
||||
.readAsStringSync();
|
||||
test('undergraduate activity data preserves teachers rooms weeks and period twelve', () {
|
||||
final draft = UndergraduateTimetableAdapter.extract(fixture(), uri);
|
||||
expect(draft.schoolTerm!.id, 523);
|
||||
expect(draft.schoolTerm!.school, SupportedSchool.uestcUndergraduate);
|
||||
final activities = parseActivityData(
|
||||
html.parse(fixture()).querySelector('script')!.text,
|
||||
unitCount: 12,
|
||||
);
|
||||
final expected = <String>{};
|
||||
for (final a in activities.where((a) => !a.cancelled)) {
|
||||
for (final index in a.indices) {
|
||||
final cancelled = activities
|
||||
.where(
|
||||
(c) =>
|
||||
c.cancelled &&
|
||||
c.courseCode == a.courseCode &&
|
||||
c.indices.contains(index),
|
||||
)
|
||||
.expand((c) => c.weeks)
|
||||
.toSet();
|
||||
for (final week in a.weeks.difference(cancelled)) {
|
||||
expected.add(
|
||||
jsonEncode([
|
||||
a.courseCode.split('.').first,
|
||||
a.teacher,
|
||||
a.room,
|
||||
index,
|
||||
week,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
final actual = {
|
||||
for (final c in draft.courses)
|
||||
for (var p = c.startPeriod; p <= c.endPeriod; p++)
|
||||
for (final w in c.weeks)
|
||||
jsonEncode([
|
||||
c.courseCode,
|
||||
c.teacher,
|
||||
c.room,
|
||||
(c.weekday - 1) * 12 + p - 1,
|
||||
w,
|
||||
]),
|
||||
};
|
||||
expect(
|
||||
actual,
|
||||
expected,
|
||||
reason: 'continuous grouping must preserve every original active index and week',
|
||||
);
|
||||
expect(draft.courses, hasLength(18));
|
||||
expect(draft.courses.map((c) => c.courseCode).toSet(), hasLength(10));
|
||||
expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3));
|
||||
expect(draft.courses.any((c) => c.room == '停课'), isFalse);
|
||||
expect(draft.courses.any((c) => c.teacher.contains('教师9')), isTrue);
|
||||
expect(draft.courses.any((c) => c.notes.contains('19:00-22:00')), isTrue);
|
||||
expect(ScheduleFileImport.parse(fixture()).schoolTerm, draft.schoolTerm);
|
||||
expect(
|
||||
UndergraduateTimetableAdapter.extract(
|
||||
fixture(),
|
||||
uri,
|
||||
).courses.map((c) => c.id),
|
||||
draft.courses.map((c) => c.id),
|
||||
);
|
||||
});
|
||||
test(
|
||||
'invalid or incomplete activity data never becomes an empty timetable',
|
||||
() {
|
||||
for (final html in [
|
||||
'<html>登录</html>',
|
||||
fixture().replaceAll('unitCount = 12', 'unitCount = 11'),
|
||||
fixture().replaceFirst(
|
||||
RegExp(r'index = \d+\s*\*\s*unitCount\s*\+\s*\d+'),
|
||||
'index = unknown()',
|
||||
),
|
||||
fixture().replaceFirst('value="523"', 'value="504"'),
|
||||
fixture().replaceFirst('"教师1"', '""'),
|
||||
]) {
|
||||
expect(
|
||||
() => UndergraduateTimetableAdapter.extract(html, uri),
|
||||
throwsFormatException,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
() => UndergraduateTimetableAdapter.extract(
|
||||
fixture(),
|
||||
Uri.parse('https://idas.uestc.edu.cn/'),
|
||||
),
|
||||
throwsFormatException,
|
||||
);
|
||||
},
|
||||
);
|
||||
test('explicit empty pages require recognized table and actual term', () {
|
||||
final empty =
|
||||
'<input name="semester.id" value="523"><table id="manualArrangeCourseTable"><tr><td>暂无课程</td></tr></table>';
|
||||
expect(
|
||||
UndergraduateTimetableAdapter.extract(empty, uri).explicitlyEmpty,
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
() => UndergraduateTimetableAdapter.extract(
|
||||
empty.replaceAll('暂无课程', ''),
|
||||
uri,
|
||||
),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
final original =
|
||||
Platform.environment['UESTC_UNDERGRADUATE_TIMETABLE_FIXTURE'];
|
||||
if (original != null)
|
||||
test(
|
||||
'actual supplied undergraduate HTML has complete normalized arrangements',
|
||||
() {
|
||||
final draft = UndergraduateTimetableAdapter.extract(
|
||||
File(original).readAsStringSync(),
|
||||
uri,
|
||||
);
|
||||
expect(draft.courses, hasLength(18));
|
||||
expect(draft.courses.map((c) => c.courseCode).toSet(), hasLength(10));
|
||||
expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3));
|
||||
expect(
|
||||
draft.courses.every((c) => c.teacher.isNotEmpty && c.room.isNotEmpty),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user