Skip to content

Commit

Permalink
feat(Run): Allow teachers to archive runs (#1173)
Browse files Browse the repository at this point in the history
  • Loading branch information
geoffreykwan authored Sep 18, 2023
1 parent 2c2a7ae commit 16d6b90
Show file tree
Hide file tree
Showing 32 changed files with 1,671 additions and 403 deletions.
8 changes: 5 additions & 3 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AnnouncementComponent } from './announcement/announcement.component';
import { AnnouncementDialogComponent } from './announcement/announcement.component';
import { TrackScrollDirective } from './track-scroll.directive';
import { RecaptchaV3Module, RECAPTCHA_V3_SITE_KEY, RECAPTCHA_BASE_URL } from 'ng-recaptcha';
import { ArchiveProjectService } from './services/archive-project.service';

export function initialize(
configService: ConfigService,
Expand Down Expand Up @@ -59,11 +60,12 @@ export function initialize(
MatDialogModule,
RecaptchaV3Module,
RouterModule.forRoot([], {
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled'
})
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled'
})
],
providers: [
ArchiveProjectService,
ConfigService,
StudentService,
TeacherService,
Expand Down
8 changes: 8 additions & 0 deletions src/app/common/harness-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MatMenuHarness } from '@angular/material/menu/testing';

export async function clickMenuButton(thisContext: any, menuButtonText: string): Promise<void> {
const getMenu = thisContext.locatorFor(MatMenuHarness);
const menu = await getMenu();
await menu.open();
return menu.clickItem({ text: menuButtonText });
}
4 changes: 4 additions & 0 deletions src/app/domain/archiveProjectResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class ArchiveProjectResponse {
archived: boolean;
id: number;
}
24 changes: 13 additions & 11 deletions src/app/domain/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ import { Run } from './run';
import { User } from '../domain/user';

export class Project {
id: number;
name: string;
metadata: any;
dateCreated: string;
archived: boolean;
dateArchived: string;
lastEdited: string;
projectThumb: string;
thumbStyle: any;
dateCreated: string;
id: number;
isHighlighted: boolean;
lastEdited: string;
license: String;
metadata: any;
name: string;
owner: User;
sharedOwners: User[] = [];
run: Run;
parentId: number;
wiseVersion: number;
projectThumb: string;
run: Run;
sharedOwners: User[] = [];
tags: string[];
thumbStyle: any;
uri: String;
license: String;
wiseVersion: number;

static readonly VIEW_PERMISSION: number = 1;
static readonly EDIT_PERMISSION: number = 2;
Expand Down
40 changes: 40 additions & 0 deletions src/app/services/archive-project.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Project } from '../domain/project';
import { ArchiveProjectResponse } from '../domain/archiveProjectResponse';

@Injectable()
export class ArchiveProjectService {
private refreshProjectsEventSource: Subject<void> = new Subject<void>();
public refreshProjectsEvent$ = this.refreshProjectsEventSource.asObservable();

constructor(private http: HttpClient) {}

archiveProject(project: Project): Observable<ArchiveProjectResponse> {
return this.http.put<ArchiveProjectResponse>(`/api/project/${project.id}/archived`, null);
}

archiveProjects(projects: Project[]): Observable<ArchiveProjectResponse[]> {
const projectIds = projects.map((project) => project.id);
return this.http.put<ArchiveProjectResponse[]>(`/api/projects/archived`, projectIds);
}

unarchiveProject(project: Project): Observable<ArchiveProjectResponse> {
return this.http.delete<ArchiveProjectResponse>(`/api/project/${project.id}/archived`);
}

unarchiveProjects(projects: Project[]): Observable<ArchiveProjectResponse[]> {
let params = new HttpParams();
for (const project of projects) {
params = params.append('projectIds', project.id);
}
return this.http.delete<ArchiveProjectResponse[]>(`/api/projects/archived`, {
params: params
});
}

refreshProjects(): void {
this.refreshProjectsEventSource.next();
}
}
44 changes: 44 additions & 0 deletions src/app/services/mock-archive-project.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Observable, Subject, of } from 'rxjs';
import { Project } from '../domain/project';
import { ArchiveProjectResponse } from '../domain/archiveProjectResponse';

export class MockArchiveProjectService {
private refreshProjectsEventSource: Subject<void> = new Subject<void>();
public refreshProjectsEvent$ = this.refreshProjectsEventSource.asObservable();

archiveProject(project: Project): Observable<ArchiveProjectResponse> {
return this.archiveProjectHelper(project, true);
}

unarchiveProject(project: Project): Observable<ArchiveProjectResponse> {
return this.archiveProjectHelper(project, false);
}

private archiveProjectHelper(
project: Project,
archived: boolean
): Observable<ArchiveProjectResponse> {
project.archived = archived;
return of(project);
}

archiveProjects(projects: Project[]): Observable<ArchiveProjectResponse[]> {
return this.archiveProjectsHelper(projects, true);
}

unarchiveProjects(projects: Project[]): Observable<ArchiveProjectResponse[]> {
return this.archiveProjectsHelper(projects, false);
}

private archiveProjectsHelper(
projects: Project[],
archived: boolean
): Observable<ArchiveProjectResponse[]> {
projects.forEach((project) => (project.archived = archived));
return of(projects);
}

refreshProjects(): void {
this.refreshProjectsEventSource.next();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ConfigService } from '../../services/config.service';
import { ActivatedRoute } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { AddProjectDialogComponent } from '../add-project-dialog/add-project-dialog.component';
import { formatDate } from '@angular/common';
import { runSpansDays } from '../../../assets/wise5/common/datetime/datetime';

@Component({
selector: 'app-student-run-list',
Expand Down Expand Up @@ -83,9 +83,7 @@ export class StudentRunListComponent implements OnInit {
}

runSpansDays(run: StudentRun) {
const startDay = formatDate(run.startTime, 'shortDate', this.localeID);
const endDay = formatDate(run.endTime, 'shortDate', this.localeID);
return startDay != endDay;
return runSpansDays(run, this.localeID);
}

activeTotal(): number {
Expand Down
8 changes: 8 additions & 0 deletions src/app/teacher/run-menu/run-menu.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,13 @@
<mat-icon>report_problem</mat-icon>
<span i18n>Report Problem</span>
</a>
<a mat-menu-item *ngIf="!run.archived" (click)="archive(true)">
<mat-icon>archive</mat-icon>
<span i18n>Archive</span>
</a>
<a mat-menu-item *ngIf="run.archived" (click)="archive(false)">
<mat-icon>unarchive</mat-icon>
<span i18n>Restore</span>
</a>
</div>
</mat-menu>
151 changes: 117 additions & 34 deletions src/app/teacher/run-menu/run-menu.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RunMenuComponent } from './run-menu.component';
import { TeacherService } from '../teacher.service';
import { Project } from '../../domain/project';
import { BehaviorSubject, Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
Expand All @@ -12,6 +11,17 @@ import { TeacherRun } from '../teacher-run';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Course } from '../../domain/course';
import { RouterTestingModule } from '@angular/router/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ArchiveProjectService } from '../../services/archive-project.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RunMenuHarness } from './run-menu.harness';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MockArchiveProjectService } from '../../services/mock-archive-project.service';
import { MatSnackBarHarness } from '@angular/material/snack-bar/testing';
import { HarnessLoader } from '@angular/cdk/testing';

export class MockTeacherService {
checkClassroomAuthorization(): Observable<string> {
Expand Down Expand Up @@ -56,44 +66,117 @@ export class MockConfigService {
}
}

describe('RunMenuComponent', () => {
let component: RunMenuComponent;
let fixture: ComponentFixture<RunMenuComponent>;
let archiveProjectService: ArchiveProjectService;
let component: RunMenuComponent;
let fixture: ComponentFixture<RunMenuComponent>;
const owner = new User();
let rootLoader: HarnessLoader;
let runMenuHarness: RunMenuHarness;
let teacherService: TeacherService;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatMenuModule, RouterTestingModule],
declarations: [RunMenuComponent],
providers: [
{ provide: TeacherService, useClass: MockTeacherService },
{ provide: UserService, useClass: MockUserService },
{ provide: ConfigService, useClass: MockConfigService },
{ provide: MatDialog, useValue: {} }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
describe('RunMenuComponent', () => {
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
MatButtonModule,
MatIconModule,
MatMenuModule,
MatSnackBarModule,
RouterTestingModule
],
declarations: [RunMenuComponent],
providers: [
{ provide: ArchiveProjectService, useClass: MockArchiveProjectService },
{ provide: TeacherService, useClass: MockTeacherService },
{ provide: UserService, useClass: MockUserService },
{ provide: ConfigService, useClass: MockConfigService },
{ provide: MatDialog, useValue: {} }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
})
);

beforeEach(() => {
beforeEach(async () => {
fixture = TestBed.createComponent(RunMenuComponent);
component = fixture.componentInstance;
const run: TeacherRun = new TeacherRun();
run.id = 1;
run.name = 'Photosynthesis';
const owner = new User();
owner.id = 1;
run.owner = owner;
const project = new Project();
project.id = 1;
project.owner = owner;
project.sharedOwners = [];
run.project = project;
run.sharedOwners = [];
component.run = run;
setRun(false);
archiveProjectService = TestBed.inject(ArchiveProjectService);
teacherService = TestBed.inject(TeacherService);
fixture.detectChanges();
runMenuHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, RunMenuHarness);
rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});

it('should create', () => {
expect(component).toBeTruthy();
});
archive();
unarchive();
});

function setRun(archived: boolean): void {
component.run = new TeacherRun({
id: 1,
name: 'Photosynthesis',
owner: owner,
project: {
id: 1,
owner: owner,
sharedOwners: []
},
archived: archived
});
}

function archive() {
describe('archive()', () => {
it('should archive a run', async () => {
await runMenuHarness.clickArchiveMenuButton();
expect(component.run.archived).toEqual(true);
const snackBar = await getSnackBar();
expect(await snackBar.getMessage()).toEqual('Successfully archived unit.');
});
it('should archive a run and then undo', async () => {
await runMenuHarness.clickArchiveMenuButton();
expect(component.run.archived).toEqual(true);
let snackBar = await getSnackBar();
expect(await snackBar.getMessage()).toEqual('Successfully archived unit.');
expect(await snackBar.getActionDescription()).toEqual('Undo');
await snackBar.dismissWithAction();
expect(component.run.archived).toEqual(false);
snackBar = await getSnackBar();
expect(await snackBar.getMessage()).toEqual('Action undone.');
});
});
}

function unarchive() {
describe('unarchive()', () => {
it('should unarchive a run', async () => {
setRun(true);
component.ngOnInit();
await runMenuHarness.clickUnarchiveMenuButton();
expect(component.run.archived).toEqual(false);
const snackBar = await getSnackBar();
expect(await snackBar.getMessage()).toEqual('Successfully restored unit.');
});
it('should unarchive a run and then undo', async () => {
setRun(true);
component.ngOnInit();
await runMenuHarness.clickUnarchiveMenuButton();
expect(component.run.archived).toEqual(false);
let snackBar = await getSnackBar();
expect(await snackBar.getMessage()).toEqual('Successfully restored unit.');
expect(await snackBar.getActionDescription()).toEqual('Undo');
await snackBar.dismissWithAction();
expect(component.run.archived).toEqual(true);
snackBar = await getSnackBar();
expect(await snackBar.getMessage()).toEqual('Action undone.');
});
});
}

async function getSnackBar() {
return await rootLoader.getHarness(MatSnackBarHarness);
}
Loading

0 comments on commit 16d6b90

Please sign in to comment.