feat: add optional external_id field#8390
Merged
Merged
Conversation
c646bec to
d6e1808
Compare
6226857 to
9c27231
Compare
There was a problem hiding this comment.
Pull request overview
Adds an optional external_id to course members and pending invitations to support linking Coursemology enrolments to external institutional identifiers, and wires it through invitations, manage-users inline editing, exports, and statistics views.
Changes:
- Added
external_idcolumns + uniqueness validation logic (cross-table at model/service level) and updated registration/invitation flows to carry it through. - Updated UI/TypeScript types and tables/forms (invite flows + manage users) to display/edit
external_id, plus CSV template handling. - Updated score summary export + student statistics table to conditionally include an
external_idcolumn when data exists, and adjusted invitation error handling to return arrays of messages.
Reviewed changes
Copilot reviewed 68 out of 68 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/services/course/user_registration_service_spec.rb | Adds regression test for not updating ext id on already-enrolled registration flow. |
| spec/services/course/user_invitation_service_spec.rb | Expands coverage for ext id parsing, deduping, and conflict handling across invite modes. |
| spec/services/course/statistics/assessment_score_summary_download_service_spec.rb | Tests conditional inclusion of external_id column in score summary CSV. |
| spec/models/user/email_spec.rb | Tests invitation ext id transfer on sign-up + rollback behavior on conflict. |
| spec/models/course/user_invitation_spec.rb | Tests invitation-level ext id uniqueness behavior. |
| spec/models/course_user_spec.rb | Tests course_user-level ext id normalization + uniqueness behavior. |
| spec/fixtures/course/invitation_with_external_id.csv | Fixture for CSV parsing with ext id (timeline enabled). |
| spec/fixtures/course/invitation_with_external_id_no_timeline.csv | Fixture for CSV parsing with ext id (timeline disabled). |
| spec/fixtures/course/invitation_no_external_id_no_timeline.csv | Fixture for CSV parsing without ext id (timeline disabled). |
| spec/fixtures/course/invitation_external_id_wrong_header.csv | Fixture to ensure header detection works with nonstandard ext id header. |
| spec/fixtures/course/invitation_duplicate_external_id.csv | Fixture for ext id conflict scenario. |
| spec/features/course/student_management_spec.rb | Adjusts selectors for inline-edit name changes. |
| spec/features/course/staff_management_spec.rb | Adjusts selectors for inline-edit name changes. |
| spec/controllers/course/user_invitations_controller_spec.rb | Updates controller specs for new error propagation format + ext id scenarios. |
| db/schema.rb | Reflects new columns/indexes and (also) includes a new unrelated table. |
| db/migrate/20260514052933_add_external_id_to_course_users_and_invitations.rb | Adds external_id columns + per-table uniqueness indexes. |
| db/migrate/20260423065615_add_test_spreadsheets_to_text_response_solutions.rb | Adds a new spreadsheets table (appears unrelated to ext id feature). |
| config/locales/zh/errors.yml | Updates invitation error messages for new per-record error surfaces. |
| config/locales/zh/csv.yml | Adds CSV header translation for external_id. |
| config/locales/zh/activerecord/errors.yml | Adds AR validation messages for external_id taken. |
| config/locales/ko/errors.yml | Updates invitation error messages for new per-record error surfaces. |
| config/locales/ko/csv.yml | Adds CSV header translation for external_id. |
| config/locales/ko/activerecord/errors.yml | Adds AR validation messages for external_id taken. |
| config/locales/en/errors.yml | Updates invitation error messages for new per-record error surfaces. |
| config/locales/en/csv.yml | Adds CSV header translation for external_id. |
| config/locales/en/activerecord/errors.yml | Adds AR validation messages for external_id taken. |
| client/locales/zh.json | Updates UI strings for ext id + duplicate reasons (some still English). |
| client/locales/ko.json | Updates UI strings for ext id + duplicate reasons (some still English). |
| client/locales/en.json | Updates UI strings for ext id + error handling. |
| client/app/types/course/userInvitations.ts | Extends invitation result typing with externalId + duplicate reason. |
| client/app/types/course/courseUsers.ts | Adds externalId + patch support for course user updates. |
| client/app/lib/translations/table.ts | Adds table column translations for externalId + optional. |
| client/app/lib/components/form/fields/DataTableInlineEditable/TextField.tsx | Adds allowEmpty support for inline editable text field. |
| client/app/bundles/course/users/translations.ts | Adds toast messages for ext id add/change/delete operations. |
| client/app/bundles/course/users/operations.ts | Sends external_id in update payload. |
| client/app/bundles/course/users/components/tables/ManageUsersTable/UserNameField.tsx | Passes allowEmpty explicitly for name field. |
| client/app/bundles/course/users/components/tables/ManageUsersTable/index.tsx | Adds External ID column to manage users table. |
| client/app/bundles/course/users/components/tables/ManageUsersTable/ExternalIdField.tsx | New inline editable External ID field with toasts + dispatch update. |
| client/app/bundles/course/user-invitations/pages/InviteUsersFileUpload/index.tsx | Adds ext id CSV guidance, template selection, and improved error display. |
| client/app/bundles/course/user-invitations/operations.ts | Adds externalId field mapping into invitation FormData. |
| client/app/bundles/course/user-invitations/components/tables/UserInvitationsTable.tsx | Conditionally displays externalId column in invitations table. |
| client/app/bundles/course/user-invitations/components/tables/InvitationResultUsersTable.tsx | Shows externalId + duplicate reasons for duplicate users result table. |
| client/app/bundles/course/user-invitations/components/tables/InvitationResultInvitationsTable.tsx | Shows externalId column in invitation results tables when present. |
| client/app/bundles/course/user-invitations/components/misc/InvitationResultDialog.tsx | Updates duplicate-users wording to be email/externalId-agnostic. |
| client/app/bundles/course/user-invitations/components/forms/IndividualInviteForm.tsx | Adds per-error toast output for form invites and removes injectIntl. |
| client/app/bundles/course/user-invitations/components/forms/IndividualInvitation.tsx | Adds externalId input field to individual invite rows. |
| client/app/bundles/course/user-invitations/components/buttons/InvitationActionButtons.tsx | Improves error handling + adds generic failure messages. |
| client/app/bundles/course/statistics/types.ts | Adds externalId + hasExternalIds metadata. |
| client/app/bundles/course/statistics/pages/StatisticsIndex/students/StudentStatisticsTable.tsx | Conditionally adds External ID column to statistics students table. |
| client/app/bundles/course/helper/index.ts | Adds timeline/no-timeline CSV template switching. |
| client/app/assets/templates/course-user-invitation-template.csv | Updates timeline template to include ExternalId column. |
| client/app/assets/templates/course-user-invitation-template-no-timeline.csv | Adds no-timeline template including ExternalId column. |
| app/views/course/users/_user_list_data.json.jbuilder | Exposes externalId in course user list JSON. |
| app/views/course/user_invitations/_invitation_result_data.json.jbuilder | Exposes externalId + duplicate reason in invitation result JSON. |
| app/views/course/user_invitations/_course_user_invitation_list_data.json.jbuilder | Exposes externalId on invitation list rows. |
| app/views/course/statistics/aggregate/all_students.json.jbuilder | Exposes externalId for students and emits hasExternalIds. |
| app/services/course/user_registration_service.rb | Transfers invitation external_id only when creating new course_user. |
| app/services/course/user_invitation_service.rb | Plumbs duplicate user accumulation into invite_users. |
| app/services/course/statistics/assessments_score_summary_download_service.rb | Conditionally includes external_id column in score summary export. |
| app/services/concerns/course/user_invitation_service/process_invitation_concern.rb | Implements ext id “taken” detection and duplicate reason tagging. |
| app/services/concerns/course/user_invitation_service/parse_invitation_concern.rb | Adds ext id CSV parsing + batch-level ext id deduping. |
| app/models/user/email.rb | Confirms invitation before building course_user to avoid ext id uniqueness clash. |
| app/models/user.rb | Copies external_id from invitation to course_user creation. |
| app/models/course/user_invitation.rb | Includes UniqueExternalIdConcern for invitations. |
| app/models/course_user.rb | Includes UniqueExternalIdConcern for course users. |
| app/models/concerns/course/unique_external_id_concern.rb | New shared validation for cross-table per-course ext id uniqueness. |
| app/controllers/course/user_invitations_controller.rb | Returns errors as array and improves per-record error aggregation. |
| app/controllers/concerns/course/users_controller_management_concern.rb | Permits external_id in course user update params. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
73
to
79
| fileUploadExample: { | ||
| id: 'course.userInvitations.InviteUsersFileUpload.fileUploadExample', | ||
| defaultMessage: | ||
| 'Name,Email[,Role,Phantom]' + | ||
| '{br}John,test1@example.org[,student,y]' + | ||
| '{br}Mary,test2@example.org[,teaching_assistant,n]', | ||
| 'Name,Email,Role,Phantom,ExternalId' + | ||
| '{br}John,test1@example.org[,student,y,A0123456' + | ||
| '{br}Mary,test2@example.org[,teaching_assistant,n,A0123457', | ||
| }, |
c9ff06f to
3498902
Compare
…itations
Allows institutions to link Coursemology enrolments to their own student
records or LMS identifiers. The field is stored on CourseUser and
Course::UserInvitation and validated unique per course across both tables
via a cross-table concern and partial DB index - a pending invitation holds
its ext_id until confirmed, preventing collisions with direct enrolments.
Surfaces:
- Individual invite form: ext_id input field
- Bulk CSV invite: ext_id column in both template variants (with and without
Timeline column); set on new records only - existing pending invitations
and enrolled users are read-only in this flow
- Manage users table: inline editable ext_id column (always visible)
- Score summary export: ext_id column included when any student has one
- Statistics > Students table: ext_id column, sortable and searchable,
shown only when at least one student has a non-null ext_id
View-only tables suppress the ext_id column when no course members have
one set, consistent with how group manager, gamification, and video columns
are conditionally shown. Edit tables always show it.
Also changes error responses from the invitations controller from a single
concatenated string to an array of per-record strings, enabling the frontend
to render overflow counts without truncating meaningful error detail.
3498902 to
9c390c4
Compare
adi-herwana-nus
approved these changes
Jun 2, 2026
Contributor
|
Failing tests are due to previous merge to master, and will be addressed in the next PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional
external_idfield to course members and pending invitations, allowing institutions to link Coursemology enrolments to their own student records or LMS identifiers. The field is stored on bothCourseUserandCourse::UserInvitationand validated unique per course across both tables (a pending invitation "holds" an ext_id until confirmed).The field is exposed in five places: the individual invite form, the bulk CSV invite (two templates - with and without Timeline column, depending on whether personal timelines are enabled), the manage users table (inline editable), the score summary export, and the Statistics > Students table. View-only tables (score summary export, statistics table) show the ext_id column only when at least one enrolled student has a non-null ext_id; the manage users edit table always shows it. Error responses from the invitations controller are changed from a single concatenated sentence to an array of per-record strings; the individual invite form shows the first error with a count of remaining errors (e.g. "Email taken (and 2 more)") rather than a generic failure message.
Scope note: two things are intentionally deferred to follow-up PRs: (1) updating ext_ids on existing pending invitations or enrolled users via bulk invite - a follow-up PR will expose those updates in a dedicated result-dialog section with proper UX; (2) additional display surfaces (gradebook table, leaderboard, submission listings) - these are separate in scope.
Design decisions
It does not land in Existing Course USers because with current implementation, the unique identifier is strictly email. as External ID is nullable, while we maintain its uniqueness within the course, for now we want to keep things simple and keep email as the only unique identifier. Thus, we do not intrepret duplicate ext_id without duplicate email as the course user with said ext_id attempting to be re-enrolled.
User::Email- when a user adds an email that matches an existing invitation, the invitation is confirmed before theCourseUseris built. Without this,UniqueExternalIdConcernwould reject the newCourseUserfor sharing an ext_id with what is still a live invitation in the DB.Regression prevention
external_idis optional and blank-normalised to nil. All existing flows behave identically when no ext_id is provided.