-
Notifications
You must be signed in to change notification settings - Fork 344
Change representation of lesson configs from classNo to lesson indices #4225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
💡 Enable Vercel Agent with $100 free credit for automated AI reviews |
@zehata is attempting to deploy a commit to the modsbot's projects Team on Vercel. A member of the Team first needs to authorize it. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4225 +/- ##
==========================================
+ Coverage 54.52% 56.47% +1.94%
==========================================
Files 274 297 +23
Lines 6076 6901 +825
Branches 1455 1661 +206
==========================================
+ Hits 3313 3897 +584
- Misses 2763 3004 +241 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Changes from #4226 is only a bugfix. The delta from this PR will apply to the fixed master too. |
@leslieyip02 Any updates? |
Sorry about the delay, I'm a bit busy at the moment but I will try to review once I have the time. Thanks for your patience! |
debd302
to
539f662
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No major comments on the implementation, just a few things to add/fix.
Also, just in case we missed something in the migration logic, I think we should wait until the end of the semester before merging this PR (lest we nuke everyone's active timetables).
That said, I really appreciate the thought and effort that went into this! Let me know if you want help for anything (e.g. implementing tests).
abbrev := constants.LessonTypeAbbrev[strings.ToUpper(lessonType)] | ||
|
||
lessonParams = append(lessonParams, fmt.Sprintf("%s:%s", abbrev, classNo)) | ||
lessonParams = append(lessonParams, fmt.Sprintf("%s:%s", abbrev, "("+strings.Trim(strings.Join(strings.Fields(fmt.Sprint(lessonIndex)), ","), "[]"))+")") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make a function for this instead? The nesting makes it harder to read.
} | ||
if len(lessonParams) > 0 { | ||
moduleParams = append(moduleParams, fmt.Sprintf("%s=%s", moduleCode, strings.Join(lessonParams, ","))) | ||
moduleParams = append(moduleParams, fmt.Sprintf("%s=%s", moduleCode, strings.Join(lessonParams, ";"))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can define a constant for the separator.
|
||
action(dispatch, () => state); | ||
|
||
expect(dispatch).toHaveBeenCalled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than just checking that dispatch
is called, I think we could also add a toMatchSnapshot()
check in this test.
|
||
// ModuleLessonConfig is a mapping of lessonType to ClassNo for a module. | ||
export type ModuleLessonConfig = { | ||
[lessonType: LessonType]: LessonIndex[]; | ||
}; | ||
|
||
// | ||
/** | ||
* ModuleLessonConfig is the v1 representation of module configs\ | ||
* It is a mapping of lessonType to classNo\ | ||
* It is only used for type annotations in the migration logic | ||
*/ | ||
export type ClassNoModuleLessonConfig = { | ||
[lessonType: LessonType]: ClassNo; | ||
}; | ||
|
||
// SemTimetableConfig is the timetable data for each semester. | ||
export type SemTimetableConfig = { | ||
[moduleCode: ModuleCode]: ModuleLessonConfig; | ||
}; | ||
|
||
// TaModulesConfig is a mapping of moduleCode to the TA's lesson types. | ||
export type TaModulesConfig = { | ||
/** | ||
* ClassNoSemTimetableConfig is the v1 representation of semester timetables\ | ||
* It is a mapping of module code to the module config\ | ||
* It is only used for type annotations in the migration logic | ||
*/ | ||
export type ClassNoSemTimetableConfig = { | ||
[moduleCode: ModuleCode]: ClassNoModuleLessonConfig; | ||
}; | ||
|
||
export type TaModulesConfig = ModuleCode[]; | ||
|
||
/** | ||
* ClassNoTaModulesConfig is the v1 representation of TA modules\ | ||
* It is a mapping of moduleCode to the TA's lesson types\ | ||
* It is only used for type annotations in the migration logic | ||
*/ | ||
export type ClassNoTaModulesConfig = { | ||
[moduleCode: ModuleCode]: [lessonType: LessonType, classNo: ClassNo][]; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be clearer to just name these ModuleLessonConfigV1
/ModuleLessonConfigV2
and TaModulesConfigV1
/TaModulesConfigV2
. It would make it more explicit that the V1
versions are no longer used and only kept for compatibility.
Also, I personally think ClassNoTaModulesConfig
is a bit too verbose and the NoTa
substring in the name could be misleading.
export function changeLesson( | ||
semester: Semester, | ||
moduleCode: ModuleCode, | ||
lessonType: LessonType, | ||
lessonIndices: LessonIndex[], | ||
) { | ||
return setLesson(semester, moduleCode, lessonType, lessonIndices); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can just rename setLesson
to changeLesson
instead of adding this function. setLesson
doesn't seem to be used anywhere else.
export const groupLessonsByLessonTypeByClassNo = ( | ||
lessonsWithIndex: readonly RawLessonWithIndex[], | ||
): LessonsByLessonTypeByClassNo => { | ||
const lessonsByLessonType = groupBy(lessonsWithIndex, 'lessonType'); | ||
return mapValues(lessonsByLessonType, (lessonsWithLessonType) => { | ||
const lessonsByClassNo = groupBy(lessonsWithLessonType, 'classNo'); | ||
return mapValues(lessonsByClassNo, (lessonsWithClassNo) => | ||
map(lessonsWithClassNo, 'lessonIndex'), | ||
); | ||
}); | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is grouping lesson indices, and not lessons, so we might want to rename this.
LessonsByLessonTypeByClassNo
and groupLessonsByLessonTypeByClassNo
are both quite verbose, so one possibility is to rename them to LessonsIndicesMap
and makeLessonIndicesMap()
. Then we define a getLessonIndices(lessonIndicesMap, lessonType, classNo)
helper function to get the lesson indices. That way, the exact order of grouping doesn't matter since it will handled by the helper.
export function deserializeTimetable(serialized: string): SemTimetableConfig { | ||
const params = qs.parse(serialized); | ||
return mapValues(omit(params, ['hidden', 'ta']), parseModuleConfig); | ||
// TODO merge logic for TA modules and hidden modules |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nusmods/website/src/views/routes/paths.ts
Lines 25 to 34 in 3f774f3
export function timetableShare( | |
semester: Semester, | |
timetable: SemTimetableConfig, | |
hiddenModules: ModuleCode[], | |
taModules: TaModulesConfig, | |
): string { | |
// Convert the list of hidden modules to a comma-separated string, if there are any | |
const serializedHidden = hiddenModules.length === 0 ? '' : serializeHidden(hiddenModules); | |
const serializedTa = isEmpty(taModules) ? '' : serializeTa(taModules); | |
Are you referring to abstracting the serialization + empty list logic into a common helper function?
export function serializeModuleListWithKey(modules: ModuleCode[], key: string): string {
return isEmpty(modules) ? '' : `&${key}=${modules.join(LESSON_SEP)}`;
}
If so, I think that would be worth doing.
// CS2100(TUT:2,TUT:3,LAB:1),CS2107(TUT:8) | ||
const trimmedSerializedTaModulesConfig = taSerialized.slice(0, -1); | ||
// CS2100(TUT:2,TUT:3,LAB:1),CS2107(TUT:8 | ||
return reduce( | ||
trimmedSerializedTaModulesConfig.split(`)${LESSON_SEP}`), | ||
(accumulatedTaTimetableConfig, moduleConfig) => { | ||
// CS2100(TUT:2,TUT:3,LAB:1 | ||
// CS2107(TUT:8 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Truncating the last character feels a bit hacky to me. Instead, maybe we could use a regex:
taSerialized.split(/(?<=\)),/); // ['CS2100(TUT:2,TUT:3,LAB:1)', 'CS2107(TUT:8)']
test('should show remove button when the module is in timetable', () => { | ||
// eslint-disable-next-line no-useless-computed-key | ||
const container = make(CS3216, { [1]: { CS3216: { Lecture: '1' } } }); | ||
const container = make(CS3216, { [1]: { CS3216: { Lecture: [0] } } }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the test at line 62 needs to be edited too.
nusmods/website/src/views/components/module-info/AddModuleDropdown.test.tsx
Lines 62 to 65 in 3f774f3
test('should show "loading" when the module is added timetable', () => { | |
// eslint-disable-next-line no-useless-computed-key | |
const timetables = { [1]: { CS3216: { Lecture: '1' } } }; | |
const container = make(CS3216); |
colorIndex: colors[lesson.moduleCode], | ||
isTaInTimetable: this.isTaInTimetable(lesson.moduleCode), | ||
}), | ||
const coloredTimetableLessons: InteractableLesson[] = this.getInteractableLessons( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename this to interactableLessons
to reflect the change.
Implementation
["CS1010S"]
classNo
lesson configs and TA lesson configs will be migrated to the newlessonGroup
format(..., ...)
denotes arrays of lesson indices?CG1111A=LAB:03
?CG1111A=LAB:(5,6)
;
separates different lesson types?CS1010S=LEC:1,TUT:1
?CS1010S=LEC:(53);TUT:(41)
?CG1111A=LAB:03&ta=CG1111A(LAB:03)
?CG1111A=LAB:(5,6)&ta=CG1111A
lessonGroup
format.Resolves
All modules can now be set as TA modules.
Resolves #3929 and #3930 , from which this implementation came from
Issue with deserialization of TA module lesson configs with multiple lessons in a module, from #4214 (comment).
All lessons of a single classNo now show up, and are highlighted when one of them is hovered over. Provided that the module is not set as a TA mod. Resolves #4168.
TA module lessons can now be added and removed individually.
User request from: https://t.me/NUSMods/12508 (No issue opened, at least I can't find one)
Known issues
Array Index Stability
Using the index of an array comes with the inherent issue of stability. Furthermore, it may fail silently, since lessons of the same lesson type are typically placed next to each other in the timetable list in the API response. So the module lesson config may remain valid even after the module timetable had been updated.
An alternative is to serialize additional info as proposed in #3930 . It will make share links very long, because of the serialization of weeks information.
Personally I think the lessons provided by the faculties are stable enough, and they will most likely not add an additional class in the middle of the semester, so I think it will be better idea to create UI to inform users that the module info has changed if it changes, which can be done simply by hashing the module info during scraping.