diff --git a/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java
index 014913e639c..b1982ab798b 100644
--- a/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java
+++ b/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java
@@ -26,26 +26,25 @@ public void testAll() {
AdminHomePage homePage = loginAdminToPage(url, AdminHomePage.class);
______TS("Test adding instructors with both valid and invalid details");
-
String name = "AHPUiT Instrúctör WithPlusInEmail";
String email = "AHPUiT+++_.instr1!@gmail.tmt";
String institute = "TEAMMATES Test Institute 1";
-
homePage.queueInstructorForAdding(name, email, institute);
String singleLineDetails = "Instructor With Invalid Email | invalidemail | TEAMMATES Test Institute 1";
-
homePage.queueInstructorForAdding(singleLineDetails);
-
- homePage.addAllInstructors();
-
- String successMessage = homePage.getMessageForInstructor(0);
- assertTrue(successMessage.contains(
- "Instructor \"AHPUiT Instrúctör WithPlusInEmail\" has been successfully created"));
-
- String failureMessage = homePage.getMessageForInstructor(1);
- assertTrue(failureMessage.contains(
- "\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not in the correct format."));
+ homePage.verifyStatusMessage("\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not "
+ + "in the correct format. An email address contains some text followed by one '@' sign followed by some "
+ + "more text, and should end with a top level domain address like .com. It cannot be longer than 254 "
+ + "characters, cannot be empty and cannot contain spaces.");
+
+ ______TS("Verify that newly added instructor appears in account request table");
+ homePage.verifyInstructorInAccountRequestTable(name, email, institute);
+
+ ______TS("Test approving a valid account request");
+ homePage.clickApproveAccountRequestButton(name, email, institute);
+ homePage.verifyStatusMessage("Account request was successfully approved. Email has been sent to "
+ + "AHPUiT+++_.instr1!@gmail.tmt.");
}
}
diff --git a/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java
index e4e4d8c3560..4335ab6a226 100644
--- a/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java
+++ b/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java
@@ -23,33 +23,25 @@ public void testAll() {
AdminHomePage homePage = loginAdminToPage(url, AdminHomePage.class);
______TS("Test adding instructors with both valid and invalid details");
-
String name = "AHPUiT Instrúctör WithPlusInEmail";
String email = "AHPUiT+++_.instr1!@gmail.tmt";
String institute = "TEAMMATES Test Institute 1";
-
homePage.queueInstructorForAdding(name, email, institute);
String singleLineDetails = "Instructor With Invalid Email | invalidemail | TEAMMATES Test Institute 1";
-
homePage.queueInstructorForAdding(singleLineDetails);
-
- homePage.addAllInstructors();
-
- String successMessage = homePage.getMessageForInstructor(0);
- assertTrue(successMessage.contains(
- "Instructor \"AHPUiT Instrúctör WithPlusInEmail\" has been successfully created"));
-
- String failureMessage = homePage.getMessageForInstructor(1);
- assertTrue(failureMessage.contains(
- "\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not in the correct format."));
-
- homePage.reloadPage();
+ homePage.verifyStatusMessage("\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not "
+ + "in the correct format. An email address contains some text followed by one '@' sign followed by some "
+ + "more text, and should end with a top level domain address like .com. It cannot be longer than 254 "
+ + "characters, cannot be empty and cannot contain spaces.");
______TS("Verify that newly added instructor appears in account request table");
-
homePage.verifyInstructorInAccountRequestTable(name, email, institute);
+ ______TS("Test approving a valid account request");
+ homePage.clickApproveAccountRequestButton(name, email, institute);
+ homePage.verifyStatusMessage("Account request was successfully approved. Email has been sent to "
+ + "AHPUiT+++_.instr1!@gmail.tmt.");
}
}
diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java
index 9316f042077..aa9503b45eb 100644
--- a/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java
+++ b/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java
@@ -36,9 +36,6 @@ public class AdminHomePage extends AppPage {
@FindBy (id = "add-instructor-single-line")
private WebElement submitButtonDetailsSingleLineForm;
- @FindBy (id = "add-all-instructors")
- private WebElement addAllInstructorsButton;
-
public AdminHomePage(Browser browser) {
super(browser);
}
@@ -69,19 +66,13 @@ public void queueInstructorForAdding(String instructorDetails) {
click(submitButtonDetailsSingleLineForm);
}
- public void addAllInstructors() {
- click(addAllInstructorsButton);
- waitForElementToBeClickable(addAllInstructorsButton);
- }
-
- public String getMessageForInstructor(int i) {
- By by = By.id("message-instructor-" + i);
- waitForElementVisibility(by);
- WebElement element = browser.driver.findElement(by);
- if (element == null) {
- return null;
- }
- return element.getText();
+ public void clickApproveAccountRequestButton(String name, String email, String institute) {
+ WebElement accountRequestRow = getAccountRequestRow(name, email, institute);
+ waitForElementPresence(By.cssSelector("[id^='approve-account-request-']"));
+ WebElement approveButton = accountRequestRow.findElement(By.cssSelector("[id^='approve-account-request-']"));
+ waitForElementToBeClickable(approveButton);
+ approveButton.click();
+ waitForPageToLoad();
}
public void clickMoreInfoButtonForRegisteredInstructor(int i) {
diff --git a/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap b/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap
index b091996522f..83d1122d34f 100644
--- a/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap
+++ b/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap
@@ -11,10 +11,7 @@ exports[`AdminHomePageComponent should snap with default view 1`] = `
instructorEmail=""
instructorInstitution=""
instructorName=""
- instructorsConsolidated={[Function Array]}
- isAddingInstructors="false"
items$={[Function Observable]}
- linkService={[Function LinkService]}
pageSize={[Function Number]}
statusMessageService={[Function StatusMessageService]}
timezoneService={[Function TimezoneService]}
@@ -128,21 +125,18 @@ exports[`AdminHomePageComponent should snap with default view 1`] = `
`;
-exports[`AdminHomePageComponent should snap with disabled adding instructor button if there are active requests 1`] = `
+exports[`AdminHomePageComponent should snap with some instructors details 1`] = `
-
+
-
+
@@ -280,501 +273,336 @@ exports[`AdminHomePageComponent should snap with disabled adding instructor butt
Email
- Institution
+ Status
- Action
+ Institute, Country
- Status
+ Created At
+
+
+ Comments
- Message
+ Options
-
+
-
- Instructor A
-
+ Instructor A
-
- instructora@example.com
-
+ instructora@example.com
-
- Sample Institution A
-
-
-
+ PENDING
-
- ADDING
+
+ Institution and Country A
-
-
-
-
-
- Instructor B
-
+ Created Time A
-
- instructorb@example.com
-
-
-
-
- Sample Institution B
-
+ Comment A
+
+
+
- Edit
+ Approve
-
- Remove
-
+
+ Reject
+
+
+
-
- PENDING
-
-
-
-
-
-
-
-
-
- Add All Instructors
-
-
-
-
-
-`;
-
-exports[`AdminHomePageComponent should snap with some instructors details 1`] = `
-
-
-
-
-
- Adding Multiple Instructors
-
-
-
- Add Instructor Details in the format: Name | Email | Institution
-
-
-
-
-
- Add Instructors
-
-
-
-
-
-
-
- Adding a Single Instructor
-
-
-
-
-
- Name:
-
-
-
-
-
-
-
- Email:
-
-
-
-
-
-
-
- Institution:
-
-
-
-
-
-
- Add Instructor
-
-
-
-
-
-
-
-
-
-
- Name
-
-
- Email
-
-
- Institution
-
-
- Action
-
-
- Status
-
-
- Message
-
-
-
-
-
-
- Instructor A
-
+ Instructor B
-
- instructora@example.com
-
+ instructorb@example.com
+
+
+ APPROVED
+
+
+ Institution and Country B
+
+
+ Created Time B
-
- Sample Institution A
-
+ Comment B
+
+
+
- Edit
+ Approve
-
- Remove
-
+
+ Reject
+
+
+
-
- PENDING
-
-
-
-
+
-
- Instructor B
-
+ Instructor C
-
- instructorb@example.com
-
+ instructorc@example.com
-
- Sample Institution B
-
-
-
+ REJECTED
-
- SUCCESS
+
+ Institution and Country C
-
- Instructor "Instructor B" has been successfully created. [
-
- join link
-
- ]
-
-
-
-
-
-
- Instructor C
-
-
-
-
- instructorc@example.com
-
+ Created Time C
-
- Sample Institution C
-
+ Comment C
+
+
+
- Edit
+ Approve
-
- Remove
-
+
+ Reject
+
+
+
-
- FAIL
-
-
-
- The instructor cannot be added for some reason
-
-
-
- Add All Instructors
-
-
+
`;
diff --git a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.html b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.html
index bd4e12fa2db..81024bc9040 100644
--- a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.html
+++ b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.html
@@ -47,41 +47,4 @@
-
-
-
-
-
-
-
- Name
- Email
- Institution
- Action
- Status
- Message
-
-
-
- 0"
- (addInstructorEvent)="addInstructor(i)"
- (removeInstructorEvent)="removeInstructor(i)"
- (toggleEditModeEvent)="setInstructorRowEditModeEnabled(i, $event)"
- >
-
-
-
0 || isAddingInstructors">
-
- Add All Instructors
-
-
-
-
-
diff --git a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts
index 357a21ba951..b410774f40e 100644
--- a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts
+++ b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts
@@ -4,22 +4,61 @@ import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { of, throwError } from 'rxjs';
import { AdminHomePageComponent } from './admin-home-page.component';
-import { InstructorData } from './instructor-data';
import { NewInstructorDataRowComponent } from './new-instructor-data-row/new-instructor-data-row.component';
import { AccountService } from '../../../services/account.service';
-import { LinkService } from '../../../services/link.service';
import { StatusMessageService } from '../../../services/status-message.service';
-import { AccountRequestStatus } from '../../../types/api-output';
+import { createBuilder } from '../../../test-helpers/generic-builder';
+import { AccountRequest, AccountRequests, AccountRequestStatus, MessageOutput } from '../../../types/api-output';
+import { AccountCreateRequest } from '../../../types/api-request';
+import { AccountRequestTableRowModel } from '../../components/account-requests-table/account-request-table-model';
import { AccountRequestTableModule } from '../../components/account-requests-table/account-request-table.module';
import { AjaxLoadingModule } from '../../components/ajax-loading/ajax-loading.module';
import { LoadingSpinnerModule } from '../../components/loading-spinner/loading-spinner.module';
import { FormatDateDetailPipe } from '../../components/teammates-common/format-date-detail.pipe';
+import { ErrorMessageOutput } from '../../error-message-output';
+
+const accountCreateRequestBuilder = createBuilder({
+ instructorEmail: '',
+ instructorName: '',
+ instructorInstitution: '',
+});
+
+const accountRequestBuilder = createBuilder({
+ id: '',
+ email: '',
+ name: '',
+ institute: '',
+ registrationKey: '',
+ status: AccountRequestStatus.PENDING,
+ createdAt: 0,
+});
+
+const messageOutputBuilder = createBuilder({
+ message: '',
+});
+
+const errorMessageOutputBuilder = createBuilder({
+ error: messageOutputBuilder.build(),
+ status: 0,
+});
+
+const accountRequestTableRowModelBuilder = createBuilder({
+ id: '',
+ name: '',
+ email: '',
+ status: AccountRequestStatus.PENDING,
+ instituteAndCountry: '',
+ createdAtText: '',
+ registeredAtText: '',
+ comments: '',
+ registrationLink: '',
+ showLinks: false,
+});
describe('AdminHomePageComponent', () => {
let component: AdminHomePageComponent;
let fixture: ComponentFixture;
let accountService: AccountService;
- let linkService: LinkService;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
@@ -39,7 +78,6 @@ describe('AdminHomePageComponent', () => {
AccountService,
FormatDateDetailPipe,
StatusMessageService,
- LinkService,
],
})
.compileComponents();
@@ -48,7 +86,6 @@ describe('AdminHomePageComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AdminHomePageComponent);
accountService = TestBed.inject(AccountService);
- linkService = TestBed.inject(LinkService);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -57,73 +94,109 @@ describe('AdminHomePageComponent', () => {
expect(component).toBeTruthy();
});
- it('should add one instructor to list if all fields are filled', () => {
+ it('validateAndAddInstructorDetail: should create one instructor account request if all fields are filled', () => {
component.instructorName = 'Instructor Name';
component.instructorEmail = 'instructor@example.com';
component.instructorInstitution = 'Instructor Institution';
fixture.detectChanges();
+ const accountCreateRequest = accountCreateRequestBuilder
+ .instructorName('Instructor Name')
+ .instructorEmail('instructor@example.com')
+ .instructorInstitution('Instructor Institution')
+ .build();
+
+ const createAccountRequestSpy = jest.spyOn(accountService, 'createAccountRequest')
+ .mockReturnValue(of(accountRequestBuilder.build()));
+
const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor');
button.click();
+ expect(createAccountRequestSpy).toHaveBeenCalledTimes(1);
+ expect(createAccountRequestSpy).toHaveBeenCalledWith(accountCreateRequest);
+
+ // Clear instructor fields
expect(component.instructorName).toEqual('');
expect(component.instructorEmail).toEqual('');
expect(component.instructorInstitution).toEqual('');
- expect(component.instructorsConsolidated.length).toEqual(1);
- expect(component.instructorsConsolidated[0]).toEqual({
- email: 'instructor@example.com',
- institution: 'Instructor Institution',
- name: 'Instructor Name',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- });
});
- it('should not add one instructor to list if some fields are empty', () => {
- component.instructorName = 'Instructor Name';
- component.instructorEmail = '';
+ it('validateAndAddInstructorDetail: should not create one instructor account request '
+ + 'if some fields are empty', () => {
+ const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor');
+ const createAccountRequestSpy = jest.spyOn(accountService, 'createAccountRequest');
+
+ component.instructorName = '';
+ component.instructorEmail = 'instructor@example.com';
component.instructorInstitution = 'Instructor Institution';
fixture.detectChanges();
- const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor');
button.click();
- expect(component.instructorName).toEqual('Instructor Name');
- expect(component.instructorEmail).toEqual('');
+ expect(component.instructorName).toEqual('');
+ expect(component.instructorEmail).toEqual('instructor@example.com');
expect(component.instructorInstitution).toEqual('Instructor Institution');
- expect(component.instructorsConsolidated.length).toEqual(0);
+ expect(createAccountRequestSpy).not.toHaveBeenCalled();
- component.instructorName = '';
- component.instructorEmail = 'instructor@example.com';
+ component.instructorName = 'Instructor Name';
+ component.instructorEmail = '';
+ component.instructorInstitution = 'Instructor Institution';
+ fixture.detectChanges();
button.click();
- expect(component.instructorName).toEqual('');
- expect(component.instructorEmail).toEqual('instructor@example.com');
+ expect(component.instructorName).toEqual('Instructor Name');
+ expect(component.instructorEmail).toEqual('');
expect(component.instructorInstitution).toEqual('Instructor Institution');
- expect(component.instructorsConsolidated.length).toEqual(0);
+ expect(createAccountRequestSpy).not.toHaveBeenCalled();
component.instructorName = 'Instructor Name';
+ component.instructorEmail = 'instructor@example.com';
component.instructorInstitution = '';
+ fixture.detectChanges();
button.click();
expect(component.instructorName).toEqual('Instructor Name');
expect(component.instructorEmail).toEqual('instructor@example.com');
expect(component.instructorInstitution).toEqual('');
- expect(component.instructorsConsolidated.length).toEqual(0);
+ expect(createAccountRequestSpy).not.toHaveBeenCalled();
});
- it('should only add valid instructor details in the single line field', () => {
+ it('validateAndAddInstructorDetails: should only create account requests for valid instructor details '
+ + 'when there are invalid lines in the single line field', () => {
component.instructorDetails = [
- 'Instructor A | instructora@example.com | Institution A',
- 'Instructor B | instructorb@example.com',
- 'Instructor C | | instructorc@example.com',
- 'Instructor D | instructord@example.com | Institution D',
- '| instructore@example.com | Institution E',
+ 'Instructor A | instructora@example.com | Institution A',
+ 'Instructor B | instructorb@example.com',
+ 'Instructor C | | instructorc@example.com',
+ 'Instructor D | instructord@example.com | Institution D',
+ '| instructore@example.com | Institution E',
].join('\n');
fixture.detectChanges();
+ const accountCreateRequestA = accountCreateRequestBuilder
+ .instructorName('Instructor A')
+ .instructorEmail('instructora@example.com')
+ .instructorInstitution('Institution A')
+ .build();
+ const accountCreateRequestD = accountCreateRequestBuilder
+ .instructorName('Instructor D')
+ .instructorEmail('instructord@example.com')
+ .instructorInstitution('Institution D')
+ .build();
+
+ const createAccountRequestSpy = jest.spyOn(accountService, 'createAccountRequest')
+ .mockImplementation((request) => {
+ switch (request.instructorEmail) {
+ case accountCreateRequestA.instructorEmail:
+ return of(accountRequestBuilder.build());
+ case accountCreateRequestD.instructorEmail:
+ return of(accountRequestBuilder.build());
+ default:
+ return throwError(() => errorMessageOutputBuilder.build());
+ }
+ });
+
const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor-single-line');
button.click();
@@ -132,401 +205,171 @@ describe('AdminHomePageComponent', () => {
'Instructor C | | instructorc@example.com',
'| instructore@example.com | Institution E',
].join('\r\n'));
- expect(component.instructorsConsolidated.length).toEqual(2);
- expect(component.instructorsConsolidated[0]).toEqual({
- email: 'instructora@example.com',
- institution: 'Institution A',
- name: 'Instructor A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- });
- expect(component.instructorsConsolidated[1]).toEqual({
- email: 'instructord@example.com',
- institution: 'Institution D',
- name: 'Instructor D',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- });
- });
- it('should remove instructor out of queue if REMOVE is requested', () => {
- const instructorData: InstructorData = {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- };
- component.instructorsConsolidated = [instructorData];
- fixture.detectChanges();
-
- const index: number = 0;
- component.removeInstructor(index);
-
- expect(component.instructorsConsolidated.includes(instructorData)).toBeFalsy();
- expect(component.instructorsConsolidated.length).toEqual(0);
+ expect(createAccountRequestSpy).toHaveBeenCalledTimes(2);
+ expect(createAccountRequestSpy).toHaveBeenNthCalledWith(1, accountCreateRequestA);
+ expect(createAccountRequestSpy).toHaveBeenNthCalledWith(2, accountCreateRequestD);
});
- it('should add instructor and update field when successful', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- ];
- jest.spyOn(accountService, 'createAccountRequest').mockReturnValue(of({
- id: 'some.person@example.com%NUS',
- email: 'some.person@example.com',
- name: 'Some Person',
- institute: 'NUS',
- status: AccountRequestStatus.APPROVED,
- registrationKey: 'registrationKey',
- createdAt: 528,
- }));
- jest.spyOn(linkService, 'generateAccountRegistrationLink')
- .mockReturnValue('http://localhost:4200/web/join?iscreatingaccount=true&key=registrationKey');
- fixture.detectChanges();
-
- const index: number = 0;
- component.addInstructor(index);
-
- expect(component.instructorsConsolidated[index].status).toEqual('SUCCESS');
- expect(component.instructorsConsolidated[index].joinLink)
- .toEqual('http://localhost:4200/web/join?iscreatingaccount=true&key=registrationKey');
- expect(component.activeRequests).toEqual(0);
+ it('should snap with default view', () => {
+ expect(fixture).toMatchSnapshot();
});
- it('should not add instructor and update field during failure', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
+ it('should snap with some instructors details', () => {
+ component.accountReqs = [
+ accountRequestTableRowModelBuilder
+ .name('Instructor A')
+ .email('instructora@example.com')
+ .status(AccountRequestStatus.PENDING)
+ .instituteAndCountry('Institution and Country A')
+ .createdAtText('Created Time A')
+ .comments('Comment A')
+ .build(),
+
+ accountRequestTableRowModelBuilder
+ .name('Instructor B')
+ .email('instructorb@example.com')
+ .status(AccountRequestStatus.APPROVED)
+ .instituteAndCountry('Institution and Country B')
+ .createdAtText('Created Time B')
+ .comments('Comment B')
+ .build(),
+
+ accountRequestTableRowModelBuilder
+ .name('Instructor C')
+ .email('instructorc@example.com')
+ .status(AccountRequestStatus.REJECTED)
+ .instituteAndCountry('Institution and Country C')
+ .createdAtText('Created Time C')
+ .comments('Comment C')
+ .build(),
];
- jest.spyOn(accountService, 'createAccountRequest').mockReturnValue(throwError(() => ({
- error: {
- message: 'This is the error message',
- },
- })));
fixture.detectChanges();
- const index: number = 0;
- component.addInstructor(index);
-
- expect(component.instructorsConsolidated[index].status).toEqual('FAIL');
- expect(component.instructorsConsolidated[index].message).toEqual('This is the error message');
- expect(component.activeRequests).toEqual(0);
- });
-
- it('should enter edit mode for only the specified instructor', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'SUCCESS',
- statusCode: 200,
- isCurrentlyBeingEdited: false,
- joinLink: 'http://localhost:4200/web/join',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor C',
- email: 'instructorc@example.com',
- institution: 'Sample Institution C',
- status: 'FAIL',
- statusCode: 400,
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'The instructor cannot be added for some reason',
- },
- ];
-
- const index: number = 2;
- component.setInstructorRowEditModeEnabled(index, true);
-
- for (let i: number = 0; i < component.instructorsConsolidated.length; i += 1) {
- expect(component.instructorsConsolidated[i].isCurrentlyBeingEdited).toEqual(i === index);
- }
+ expect(fixture).toMatchSnapshot();
});
- it('should exit edit mode for only the specified instructor', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor C',
- email: 'instructorc@example.com',
- institution: 'Sample Institution C',
- status: 'FAIL',
- statusCode: 400,
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'The instructor cannot be added for some reason',
- },
- ];
- for (let i: number = 0; i < component.instructorsConsolidated.length; i += 1) {
- component.setInstructorRowEditModeEnabled(i, true);
- }
+ it('validateAndAddInstructorDetails: should create multiple instructor account requests when split by tabs', () => {
+ component.instructorDetails =
+ `Instructor A \t instructora@example.com \t Institution A\n
+ Instructor B \t instructorb@example.com \t Institution B`;
fixture.detectChanges();
- const index: number = 1;
- component.setInstructorRowEditModeEnabled(index, false);
-
- for (let i: number = 0; i < component.instructorsConsolidated.length; i += 1) {
- expect(component.instructorsConsolidated[i].isCurrentlyBeingEdited).toEqual(i !== index);
- }
- });
+ const accountCreateRequestA = accountCreateRequestBuilder
+ .instructorName('Instructor A')
+ .instructorEmail('instructora@example.com')
+ .instructorInstitution('Institution A')
+ .build();
+ const accountCreateRequestB = accountCreateRequestBuilder
+ .instructorName('Instructor B')
+ .instructorEmail('instructorb@example.com')
+ .instructorInstitution('Institution B')
+ .build();
+
+ const createAccountRequestSpy = jest.spyOn(accountService, 'createAccountRequest')
+ .mockImplementation((request) => {
+ switch (request.instructorEmail) {
+ case accountCreateRequestA.instructorEmail:
+ return of(accountRequestBuilder.build());
+ case accountCreateRequestB.instructorEmail:
+ return of(accountRequestBuilder.build());
+ default:
+ return throwError(() => errorMessageOutputBuilder.build());
+ }
+ });
- it('should add all instructors when prompted', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'SUCCESS',
- statusCode: 200,
- isCurrentlyBeingEdited: false,
- joinLink: 'http://localhost:4200/web/join',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor C',
- email: 'instructorc@example.com',
- institution: 'Sample Institution C',
- status: 'FAIL',
- statusCode: 400,
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'The instructor cannot be added for some reason',
- },
- ];
- // No need to spy here as this test only tests the number of active requests added
- // Testing of adding individual instructors have been done before
- fixture.detectChanges();
-
- const button: any = fixture.debugElement.nativeElement.querySelector('#add-all-instructors');
+ const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor-single-line');
button.click();
- expect(component.instructorsConsolidated[0].status).toEqual('ADDING');
- expect(component.instructorsConsolidated[1].status).toEqual('SUCCESS');
- expect(component.instructorsConsolidated[2].status).toEqual('ADDING');
- expect(component.activeRequests).toEqual(2);
+ expect(component.instructorDetails).toEqual('');
+
+ expect(createAccountRequestSpy).toHaveBeenNthCalledWith(1, accountCreateRequestA);
+ expect(createAccountRequestSpy).toHaveBeenNthCalledWith(2, accountCreateRequestB);
});
- it('should add only instructors that are not currently in edit mode when trying to add all', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'PENDING',
- isCurrentlyBeingEdited: true,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor C',
- email: 'instructorc@example.com',
- institution: 'Sample Institution C',
- status: 'FAIL',
- statusCode: 400,
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'The instructor cannot be added for some reason',
- },
- ];
+ it('validateAndAddInstructorDetails: should create multiple instructor account requests '
+ + 'when split by vertical bars', () => {
+ component.instructorDetails =
+ `Instructor A | instructora@example.com | Institution A\n
+ Instructor B | instructorb@example.com | Institution B`;
fixture.detectChanges();
- const addAllButton: any = fixture.debugElement.nativeElement.querySelector('#add-all-instructors');
- addAllButton.click();
+ const accountCreateRequestA = accountCreateRequestBuilder
+ .instructorName('Instructor A')
+ .instructorEmail('instructora@example.com')
+ .instructorInstitution('Institution A')
+ .build();
+ const accountCreateRequestB = accountCreateRequestBuilder
+ .instructorName('Instructor B')
+ .instructorEmail('instructorb@example.com')
+ .instructorInstitution('Institution B')
+ .build();
+
+ const createAccountRequestSpy = jest.spyOn(accountService, 'createAccountRequest')
+ .mockImplementation((request) => {
+ switch (request.instructorEmail) {
+ case accountCreateRequestA.instructorEmail:
+ return of(accountRequestBuilder.build());
+ case accountCreateRequestB.instructorEmail:
+ return of(accountRequestBuilder.build());
+ default:
+ return throwError(() => errorMessageOutputBuilder.build());
+ }
+ });
- expect(component.instructorsConsolidated[0].status).toEqual('ADDING');
- expect(component.instructorsConsolidated[1].status).toEqual('PENDING');
- expect(component.instructorsConsolidated[2].status).toEqual('ADDING');
- expect(component.activeRequests).toEqual(2);
- });
+ const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor-single-line');
+ button.click();
- it('should snap with default view', () => {
- expect(fixture).toMatchSnapshot();
- });
+ expect(component.instructorDetails).toEqual('');
- it('should snap with some instructors details', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'SUCCESS',
- statusCode: 200,
- isCurrentlyBeingEdited: false,
- joinLink: 'http://localhost:4200/web/join',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor C',
- email: 'instructorc@example.com',
- institution: 'Sample Institution C',
- status: 'FAIL',
- statusCode: 400,
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'The instructor cannot be added for some reason',
- },
- ];
- fixture.detectChanges();
- expect(fixture).toMatchSnapshot();
+ expect(createAccountRequestSpy).toHaveBeenNthCalledWith(1, accountCreateRequestA);
+ expect(createAccountRequestSpy).toHaveBeenNthCalledWith(2, accountCreateRequestB);
});
- it('should snap with disabled adding instructor button if there are active requests', () => {
- component.instructorsConsolidated = [
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'ADDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- joinLink: 'This should not be displayed',
- message: 'This should not be displayed',
- },
- ];
- component.activeRequests = 1;
- component.isAddingInstructors = true;
+ it('fetchAccountRequests: should update account requests binding if pending account requests exist', () => {
+ const accountRequestA = accountRequestBuilder
+ .name('Instructor A')
+ .email('instructora@example.com')
+ .institute('Institution A')
+ .build();
+ const accountRequestB = accountRequestBuilder
+ .name('Instructor B')
+ .email('instructorb@example.com')
+ .institute('Institution B')
+ .build();
+ const accountRequests: AccountRequests = {
+ accountRequests: [
+ accountRequestA,
+ accountRequestB,
+ ],
+ };
- fixture.detectChanges();
- expect(fixture).toMatchSnapshot();
- });
+ jest.spyOn(accountService, 'getPendingAccountRequests')
+ .mockReturnValue(of(accountRequests));
- it('should add multiple instructors split by tabs', () => {
- component.instructorDetails = `Instructor A \t instructora@example.com \t Sample Institution A\n
- Instructor B \t instructorb@example.com \t Sample Institution B`;
+ component.fetchAccountRequests();
- fixture.detectChanges();
+ expect(component.accountReqs.length).toEqual(2);
- const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor-single-line');
- button.click();
+ expect(component.accountReqs[0].name).toEqual('Instructor A');
+ expect(component.accountReqs[0].email).toEqual('instructora@example.com');
+ expect(component.accountReqs[0].instituteAndCountry).toEqual('Institution A');
- expect(component.instructorsConsolidated.length).toEqual(2);
- expect(component.instructorsConsolidated[0]).toEqual(
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- },
- );
- expect(component.instructorsConsolidated[1]).toEqual(
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- },
- );
+ expect(component.accountReqs[1].name).toEqual('Instructor B');
+ expect(component.accountReqs[1].email).toEqual('instructorb@example.com');
+ expect(component.accountReqs[1].instituteAndCountry).toEqual('Institution B');
});
- it('should add multiple instructors split by vertical bars', () => {
- component.instructorDetails = `Instructor A | instructora@example.com | Sample Institution A\n
- Instructor B | instructorb@example.com | Sample Institution B`;
+ it('fetchAccountRequests: should not update account requests binding if no pending account requests exist', () => {
+ const accountRequests: AccountRequests = {
+ accountRequests: [],
+ };
- fixture.detectChanges();
+ jest.spyOn(accountService, 'getPendingAccountRequests')
+ .mockReturnValue(of(accountRequests));
- const button: any = fixture.debugElement.nativeElement.querySelector('#add-instructor-single-line');
- button.click();
+ component.fetchAccountRequests();
- expect(component.instructorsConsolidated.length).toEqual(2);
- expect(component.instructorsConsolidated[0]).toEqual(
- {
- name: 'Instructor A',
- email: 'instructora@example.com',
- institution: 'Sample Institution A',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- },
- );
- expect(component.instructorsConsolidated[1]).toEqual(
- {
- name: 'Instructor B',
- email: 'instructorb@example.com',
- institution: 'Sample Institution B',
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- },
- );
+ expect(component.accountReqs.length).toEqual(0);
});
});
diff --git a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts
index 0fd8d9f6eb1..75c69675bdd 100644
--- a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts
+++ b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts
@@ -1,12 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
-import { finalize } from 'rxjs/operators';
-import { InstructorData } from './instructor-data';
import { AccountService } from '../../../services/account.service';
-import { LinkService } from '../../../services/link.service';
import { StatusMessageService } from '../../../services/status-message.service';
import { TimezoneService } from '../../../services/timezone.service';
-import { AccountRequest, AccountRequests } from '../../../types/api-output';
+import { AccountRequests } from '../../../types/api-output';
+import { AccountCreateRequest } from '../../../types/api-request';
import { AccountRequestTableRowModel } from '../../components/account-requests-table/account-request-table-model';
import { FormatDateDetailPipe } from '../../components/teammates-common/format-date-detail.pipe';
import { ErrorMessageOutput } from '../../error-message-output';
@@ -26,20 +24,16 @@ export class AdminHomePageComponent implements OnInit {
instructorEmail: string = '';
instructorInstitution: string = '';
- instructorsConsolidated: InstructorData[] = [];
accountReqs: AccountRequestTableRowModel[] = [];
activeRequests: number = 0;
currentPage: number = 1;
pageSize: number = 20;
items$: Observable = of([]);
- isAddingInstructors: boolean = false;
-
constructor(
private accountService: AccountService,
private statusMessageService: StatusMessageService,
private timezoneService: TimezoneService,
- private linkService: LinkService,
private formatDateDetailPipe: FormatDateDetailPipe,
) {}
@@ -50,29 +44,86 @@ export class AdminHomePageComponent implements OnInit {
/**
* Validates and adds the instructor details filled with first form.
*/
- validateAndAddInstructorDetails(): void {
+ validateAndAddInstructorDetails(): Promise {
+ const lines: string[] = this.instructorDetails.split(/\r?\n/);
const invalidLines: string[] = [];
- for (const instructorDetail of this.instructorDetails.split(/\r?\n/)) {
- const instructorDetailSplit: string[] = instructorDetail.split(/[|\t]/).map((item: string) => item.trim());
- if (instructorDetailSplit.length < 3) {
- // TODO handle error
- invalidLines.push(instructorDetail);
- continue;
+ const accountRequests: Promise[] = [];
+ for (const line of lines) {
+ const instructorDetailsSplit: string[] = line.split(/[|\t]/).map((item: string) => item.trim());
+ let errorMessage = '';
+ switch (instructorDetailsSplit.length) {
+ case 1:
+ // Triggers when instructorDetails is empty
+ if (!instructorDetailsSplit[0]) {
+ continue;
+ }
+
+ invalidLines.push(line);
+ this.statusMessageService.showErrorToast('email cannot be null');
+ continue;
+ case 2:
+ invalidLines.push(line);
+ this.statusMessageService.showErrorToast('institution cannot be null');
+ continue;
+ case 3:
+ break;
+ default:
+ invalidLines.push(line);
+ this.statusMessageService.showErrorToast('Too many fields, please check that all lines contains only a '
+ + 'name, email and institution.');
+ continue;
+ }
+
+ // Update the character numbers if it changes in the backend.
+ if (!instructorDetailsSplit[0]) {
+ errorMessage = 'The field \'person name\' is empty. '
+ + 'The value of a/an person name should be no longer than 100 characters. '
+ + 'It should not be empty.\n';
+ }
+ if (!instructorDetailsSplit[1]) {
+ errorMessage += 'The field \'email\' is empty. '
+ + 'An email address contains some text followed by one \'@\' sign followed by some more text, '
+ + 'and should end with a top level domain address like .com. '
+ + 'It cannot be longer than 254 characters, cannot be empty and cannot contain spaces.\n';
}
- if (!instructorDetailSplit[0] || !instructorDetailSplit[1] || !instructorDetailSplit[2]) {
- // TODO handle error
- invalidLines.push(instructorDetail);
+ if (!instructorDetailsSplit[2]) {
+ errorMessage += 'The field \'institute name\' is empty. '
+ + 'The value of a/an institute name should be no longer than 128 characters. '
+ + 'It should not be empty.';
+ }
+ if (errorMessage !== '') {
+ invalidLines.push(line);
+ this.statusMessageService.showErrorToast(errorMessage);
continue;
}
- this.instructorsConsolidated.push({
- name: instructorDetailSplit[0],
- email: instructorDetailSplit[1],
- institution: instructorDetailSplit[2],
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
+
+ const requestData: AccountCreateRequest = {
+ instructorEmail: instructorDetailsSplit[1],
+ instructorName: instructorDetailsSplit[0],
+ instructorInstitution: instructorDetailsSplit[2],
+ };
+
+ const newRequest: Promise = new Promise((resolve, reject) => {
+ this.accountService.createAccountRequest(requestData)
+ .subscribe({
+ next: () => {
+ resolve();
+ },
+ error: (resp: ErrorMessageOutput) => {
+ invalidLines.push(line);
+ this.statusMessageService.showErrorToast(resp.error.message);
+ reject();
+ },
+ });
});
+
+ accountRequests.push(newRequest);
}
- this.instructorDetails = invalidLines.join('\r\n');
+
+ return Promise.allSettled(accountRequests).then(() => {
+ this.instructorDetails = invalidLines.join('\r\n');
+ this.fetchAccountRequests();
+ });
}
/**
@@ -83,79 +134,26 @@ export class AdminHomePageComponent implements OnInit {
// TODO handle error
return;
}
- this.instructorsConsolidated.push({
- name: this.instructorName,
- email: this.instructorEmail,
- institution: this.instructorInstitution,
- status: 'PENDING',
- isCurrentlyBeingEdited: false,
- });
- this.instructorName = '';
- this.instructorEmail = '';
- this.instructorInstitution = '';
- }
- /**
- * Adds the instructor at the i-th index.
- */
- addInstructor(i: number): void {
- const instructor: InstructorData = this.instructorsConsolidated[i];
- if (this.instructorsConsolidated[i].isCurrentlyBeingEdited
- || (instructor.status !== 'PENDING' && instructor.status !== 'FAIL')) {
- return;
- }
- this.activeRequests += 1;
- instructor.status = 'ADDING';
-
- this.isAddingInstructors = true;
- this.accountService.createAccountRequest({
- instructorEmail: instructor.email,
- instructorName: instructor.name,
- instructorInstitution: instructor.institution,
- })
- .pipe(finalize(() => {
- this.isAddingInstructors = false;
- }))
- .subscribe({
- next: (resp: AccountRequest) => {
- instructor.status = 'SUCCESS';
- instructor.statusCode = 200;
- instructor.joinLink = this.linkService.generateAccountRegistrationLink(resp.registrationKey);
- this.activeRequests -= 1;
- },
- error: (resp: ErrorMessageOutput) => {
- instructor.status = 'FAIL';
- instructor.statusCode = resp.status;
- instructor.message = resp.error.message;
- this.activeRequests -= 1;
- },
- });
- }
-
- /**
- * Removes the instructor at the i-th index.
- */
- removeInstructor(i: number): void {
- this.instructorsConsolidated.splice(i, 1);
- }
+ const requestData: AccountCreateRequest = {
+ instructorEmail: this.instructorEmail,
+ instructorName: this.instructorName,
+ instructorInstitution: this.instructorInstitution,
+ };
- /**
- * Sets the i-th instructor data row's edit mode status.
- *
- * @param i The index.
- * @param isEnabled Whether the edit mode status is enabled.
- */
- setInstructorRowEditModeEnabled(i: number, isEnabled: boolean): void {
- this.instructorsConsolidated[i].isCurrentlyBeingEdited = isEnabled;
- }
+ this.accountService.createAccountRequest(requestData)
+ .subscribe({
+ next: () => {
+ this.instructorName = '';
+ this.instructorEmail = '';
+ this.instructorInstitution = '';
- /**
- * Adds all the pending and failed-to-add instructors.
- */
- addAllInstructors(): void {
- for (let i: number = 0; i < this.instructorsConsolidated.length; i += 1) {
- this.addInstructor(i);
- }
+ this.fetchAccountRequests();
+ },
+ error: (resp: ErrorMessageOutput) => {
+ this.statusMessageService.showErrorToast(resp.error.message);
+ },
+ });
}
private formatAccountRequests(requests: AccountRequests): AccountRequestTableRowModel[] {