-
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.54% +2.01%
==========================================
Files 274 297 +23
Lines 6076 6915 +839
Branches 1455 1670 +215
==========================================
+ Hits 3313 3910 +597
- Misses 2763 3005 +242 ☔ 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.
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.
Fix: 04319c9
| } | ||
| 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.
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.
Fix: 04319c9
|
|
||
| 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.
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 added equality tests so that it's easier to see the action called b05656f
|
|
||
| // 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.
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.
Fix: 8137c0a
website/src/actions/timetables.ts
Outdated
| 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.
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.
Fix: 670ff1d
website/src/utils/timetables.ts
Outdated
| 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.
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.
hmm, I agree with the implementation but I think the name still needs work, I am struggling to think of one too, perhaps LessonGroup so it becomes groupLesson and getLessonGroupIndices?
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.
website/src/utils/timetables.ts
Outdated
| 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.
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.
Fix: 9a9f2c4
Note that I moved the params key logic, as I think the abstraction for adding the params key is unnecessary
website/src/utils/timetables.ts
Outdated
| // 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)']
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.
Fix: 6c43b1f Let me know if I missed any in the same file
| 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); |
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.
Fix: 14df7f4
| 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.
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.
Fix: 7059840
Same thought actually haha Thanks for reviewing, I was thinking it might be worth for us to ask other people to take a look at the implementation too, since it is definitely quite disruptive |
…"ClassNo*" to "*V1"
…eflect purpose of the function rather than its implementation
… access function getLessonIndices
…d-splitting method
… type in TA modules
| expect( | ||
| deserializeTimetable( | ||
| 'CS1010S=LEC:1,TUT:8&CS3216=LEC:1&ta=CS3216(LEC:1),CS1010S(LEC:1,TUT:2)&hidden=CS3216', | ||
| 'CS1010S=LEC:1,TUT:8&CS3216=LEC:1&ta=CS3216(LEC:1),CS1010S(LEC:1,TUT:2,TUT:3)&hidden=CS3216', |
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 noticed that I hadn't add a test case to test multiple lessons for a particular lesson type
website/src/utils/timetables.test.ts
Outdated
| }); | ||
| }); | ||
|
|
||
| // TODO: how to test TA config... |
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 wrote this to remind myself to write the test cases to deserialize TA configs. I had forgotten to remove this.
Implementation
["CS1010S"]classNolesson configs and TA lesson configs will be migrated to the newlessonGroupformat(..., ...)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=CG1111AlessonGroupformat.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.