From efeffd4d84e2045ceb1a72c8c255d30b7a6d0677 Mon Sep 17 00:00:00 2001 From: Andy <128531452+Andy-W-Developer@users.noreply.github.com> Date: Sat, 29 Jun 2024 01:04:55 +1200 Subject: [PATCH] [#13102] Admin rejecting account request: give actual account in the email template (#13118) * Change existingEmail to googleId * Add extra info to instructions * Fix missing googleId if multiple accounts exist * Add function name to test name * Move account search to function * Change missing googleId string to be shorter * Add check for accounts with no googleId * Add tests for replaceGoogleId * Change instructor search result to use createBuilder * Change a test name to fit convention --------- Co-authored-by: domoberzin <74132255+domoberzin@users.noreply.github.com> Co-authored-by: Wei Qing <48304907+weiquu@users.noreply.github.com> --- ...t-with-reason-modal.component.spec.ts.snap | 2 + ...reject-with-reason-modal.component.spec.ts | 71 +++++++++++++++++-- ...dmin-reject-with-reason-modal.component.ts | 64 ++++++++++++++++- 3 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/__snapshots__/admin-reject-with-reason-modal.component.spec.ts.snap b/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/__snapshots__/admin-reject-with-reason-modal.component.spec.ts.snap index 6d39aa0d076..f657de589c6 100644 --- a/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/__snapshots__/admin-reject-with-reason-modal.component.spec.ts.snap +++ b/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/__snapshots__/admin-reject-with-reason-modal.component.spec.ts.snap @@ -5,8 +5,10 @@ exports[`RejectWithReasonModal should show empty title and body 1`] = ` accountRequestEmail="" accountRequestName="" activeModal={[Function NgbActiveModal]} + existingAccount={[Function Object]} rejectionReasonBody={[Function String]} rejectionReasonTitle={[Function String]} + searchService={[Function SearchService]} statusMessageService={[Function StatusMessageService]} >
({ + name: 'name', + email: 'email', + googleId: 'googleId', + courseId: 'courseId', + courseName: 'courseName', + isCourseDeleted: false, + institute: 'institute', + courseJoinLink: 'courseJoinLink', + homePageLink: 'homePageLink', + manageAccountLink: 'manageAccountLink', + showLinks: false, + awaitingSessions: DEFAULT_FEEDBACK_SESSION_GROUP, + openSessions: DEFAULT_FEEDBACK_SESSION_GROUP, + notOpenSessions: DEFAULT_FEEDBACK_SESSION_GROUP, + publishedSessions: DEFAULT_FEEDBACK_SESSION_GROUP, +}); describe('RejectWithReasonModal', () => { + let searchService: SearchService; let statusMessageService: StatusMessageService; let fixture: ComponentFixture; let component: RejectWithReasonModalComponent; @@ -15,14 +49,16 @@ describe('RejectWithReasonModal', () => { declarations: [], imports: [ HttpClientTestingModule, + RouterTestingModule, ], - providers: [NgbActiveModal, StatusMessageService], + providers: [NgbActiveModal, SearchService, StatusMessageService], }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(RejectWithReasonModalComponent); + searchService = TestBed.inject(SearchService); statusMessageService = TestBed.inject(StatusMessageService); fixture.detectChanges(); component = fixture.componentInstance; @@ -36,7 +72,34 @@ describe('RejectWithReasonModal', () => { expect(fixture).toMatchSnapshot(); }); - it('should show error message when title is empty upon submitting', () => { + it('replaceGoogleId: should set the googleId to an empty string if no instructor accounts are found', () => { + jest.spyOn(searchService, 'searchAdmin').mockReturnValue(of({ + students: [], + instructors: [], + accountRequests: [], + })); + + component.replaceGoogleId(); + + expect(component.existingAccount.googleId).toEqual(''); + }); + + it('replaceGoogleId: should set the googleId to the instructor accounts googleId ' + + 'if an instructor account is found', () => { + const testInstructor = instructorAccountSearchResultBuilder.googleId('instructorGoogleId').build(); + + jest.spyOn(searchService, 'searchAdmin').mockReturnValue(of({ + students: [], + instructors: [testInstructor], + accountRequests: [], + })); + + component.replaceGoogleId(); + + expect(component.existingAccount.googleId).toEqual('instructorGoogleId'); + }); + + it('reject: should show error message when title is empty upon submitting', () => { component.rejectionReasonTitle = ''; fixture.detectChanges(); @@ -51,7 +114,7 @@ describe('RejectWithReasonModal', () => { expect(spyStatusMessageService).toHaveBeenCalled(); }); - it('should show error message when body is empty upon submitting', () => { + it('reject: should show error message when body is empty upon submitting', () => { component.rejectionReasonBody = ''; fixture.detectChanges(); @@ -65,7 +128,7 @@ describe('RejectWithReasonModal', () => { expect(spyStatusMessageService).toHaveBeenCalled(); }); - it('should close modal with data', () => { + it('reject: should close modal with data', () => { const spyActiveModal = jest.spyOn(component.activeModal, 'close'); component.rejectionReasonTitle = 'Rejection Title'; component.rejectionReasonBody = 'Rejection Body'; diff --git a/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/admin-reject-with-reason-modal.component.ts b/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/admin-reject-with-reason-modal.component.ts index 051d145d4d6..080b813161d 100644 --- a/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/admin-reject-with-reason-modal.component.ts +++ b/src/web/app/components/account-requests-table/admin-reject-with-reason-modal/admin-reject-with-reason-modal.component.ts @@ -2,7 +2,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { RejectWithReasonModalComponentResult } from './admin-reject-with-reason-modal-model'; import { environment } from '../../../../environments/environment'; +import { AdminSearchResult, InstructorAccountSearchResult, SearchService } from '../../../../services/search.service'; import { StatusMessageService } from '../../../../services/status-message.service'; +import { ErrorMessageOutput } from '../../../error-message-output'; /** * Modal to select reject account requests with reason. @@ -21,6 +23,24 @@ export class RejectWithReasonModalComponent implements OnInit { @Input() accountRequestEmail: string = ''; + existingAccount: InstructorAccountSearchResult = { + name: '', + email: '', + googleId: '', + courseId: '', + courseName: '', + isCourseDeleted: false, + institute: '', + courseJoinLink: '', + homePageLink: '', + manageAccountLink: '', + showLinks: false, + awaitingSessions: {}, + openSessions: {}, + notOpenSessions: {}, + publishedSessions: {}, + }; + rejectionReasonBody: string = '

Hi, {accountRequestName}

\n\n' + '

Thanks for your interest in using TEAMMATES. ' + 'We are unable to create a TEAMMATES instructor account for you.

' @@ -33,24 +53,62 @@ export class RejectWithReasonModalComponent implements OnInit { + 'Remedy: If you are a student but you still need an instructor account, ' + 'please send your justification to {supportEmail}

\n\n' + '

Reason: You already have an account for this email address and this institution.
' - + 'Remedy: You can login to TEAMMATES using your Google account {existingEmail}

\n\n' + + 'Remedy: You can login to TEAMMATES using your Google account: {googleId}

\n\n' + + '

If you are logged into multiple Google accounts, remember to logout from other Google accounts first, ' + + 'or use an incognito Browser window. Let us know (with a screenshot) if that doesn\'t work.

' + '

If you need further clarification or would like to appeal this decision, please ' + 'feel free to contact us at {supportEmail}

' + '

Regards,
TEAMMATES Team.

'; rejectionReasonTitle: string = 'We are Unable to Create an Account for you'; - constructor(public activeModal: NgbActiveModal, public statusMessageService: StatusMessageService) {} + constructor( + public activeModal: NgbActiveModal, + public statusMessageService: StatusMessageService, + + private searchService: SearchService, + ) {} ngOnInit(): void { this.rejectionReasonBody = this.rejectionReasonBody.replace('{accountRequestName}', this.accountRequestName); - this.rejectionReasonBody = this.rejectionReasonBody.replace('{existingEmail}', this.accountRequestEmail); this.rejectionReasonBody = this.rejectionReasonBody.replaceAll('{supportEmail}', environment.supportEmail); + + this.replaceGoogleId(); } onRejectionReasonBodyChange(updatedText: string): void { this.rejectionReasonBody = updatedText; } + replaceGoogleId(): void { + this.searchService.searchAdmin(this.accountRequestEmail) + .subscribe({ + next: (resp: AdminSearchResult) => { + const hasInstructors: boolean = !!(resp.instructors && resp.instructors.length); + + if (!hasInstructors) { + this.rejectionReasonBody = this.rejectionReasonBody.replace('{googleId}', 'NO_GOOGLEID'); + return; + } + + for (const instructor of resp.instructors) { + if (instructor.googleId !== '') { + this.existingAccount = instructor; + } + } + + if (this.existingAccount.googleId === '') { + // When an instructor account exists, but for some reason does not have a googleId + this.rejectionReasonBody = this.rejectionReasonBody.replace('{googleId}', 'NO_GOOGLEID'); + } else { + this.rejectionReasonBody = this.rejectionReasonBody.replace('{googleId}', this.existingAccount.googleId); + } + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + } + /** * Fires the reject event. */