Skip to content
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

feat(Run): Allow teachers to archive runs #1173

Merged
merged 47 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b9c561e
feat(Run): Allow teachers to archive runs
geoffreykwan Apr 6, 2023
340e7e1
chore(Run): Change order of run display to completed, running, scheduled
geoffreykwan Apr 9, 2023
620eff3
feat(Run): Change archiving to use tags instead of isDeleted field
geoffreykwan Jun 22, 2023
e6f23c9
feat(Run): Move tags from run to project and use project id for archi…
geoffreykwan Aug 11, 2023
cec8b19
test(Archive): Fix archive run tests
geoffreykwan Aug 11, 2023
fd243a6
Merge branch 'develop' into issue-1012-allow-teacher-to-archive-runs
geoffreykwan Aug 11, 2023
d1ca0b4
feat(Archive): Allow archiving runs that are shared with you
geoffreykwan Aug 11, 2023
9ad4fa4
feat(Archive): Fix select runs drop down
geoffreykwan Aug 11, 2023
c2d5c92
test(Archive): Fix test
geoffreykwan Aug 11, 2023
523e9aa
feat(Archive): Create component for select runs checkbox and drop down
geoffreykwan Aug 11, 2023
55551cd
test(Archive): Add test harnesses to teacher run list tests
geoffreykwan Aug 17, 2023
5efc759
test(Archive Run): Clean up test harnesses
geoffreykwan Aug 21, 2023
c8c0463
feat(Archive Run): Change http requests to use new methods and paths
geoffreykwan Aug 21, 2023
28a971e
chore(Archive Run): Create ArchiveProjectService and move functions in
geoffreykwan Aug 21, 2023
9fbbd48
feat(Archive Run): Archive requests now look at project status in res…
geoffreykwan Aug 22, 2023
c0aea94
chore(Archive): Change boolean variable names to not start with is
geoffreykwan Aug 22, 2023
00ec848
chore(Teacher Run List): Add private/protected modifiers to variables
geoffreykwan Aug 23, 2023
8d58f9a
chore(Teacher Run List Item): Add private/protected modifiers to vari…
geoffreykwan Aug 23, 2023
13ccc92
chore(Select Runs Controls): Add private/protected modifiers to varia…
geoffreykwan Aug 23, 2023
328dc03
chore(Run Menu): Add private/protected modifiers and create test harness
geoffreykwan Aug 23, 2023
2557f0e
chore(Run Menu): Remove duplication in archive()/unarchive()
geoffreykwan Aug 24, 2023
3de99b1
chore(Archive Run): Change snackbar wording to use Unit instead of Run
geoffreykwan Aug 24, 2023
21be774
Update run menu text and snackbar messages
breity Aug 24, 2023
26ce6b0
Update teacher run list item styles
breity Aug 24, 2023
9fe80a2
Replace active/archived slide toggle with select
breity Aug 24, 2023
cd6a1a0
Update run selection tools
breity Aug 24, 2023
32a73f4
Merge remote-tracking branch 'refs/remotes/origin/issue-1012-allow-te…
breity Aug 24, 2023
9053114
Fix tests, update some text and function names
breity Aug 25, 2023
9edbd45
fix(Archive): Do not show all and select all includes shared runs
geoffreykwan Aug 25, 2023
c1196d6
chore(Archive Run): Move archive button into select run controls
geoffreykwan Aug 27, 2023
52d1350
feat(Archive Run): Allow undoing an archive action
geoffreykwan Aug 28, 2023
986132c
chore(Archive Run): Change function name from restore to unarchive
geoffreykwan Aug 28, 2023
e6f9bd0
chore(Archive Run): Clean up code
geoffreykwan Aug 28, 2023
249d5d0
feat(Archive Run): Show message when there are no active or archived …
geoffreykwan Aug 29, 2023
728a994
feat(Archive Run): Hide run count when there are no runs
geoffreykwan Aug 29, 2023
3b1e579
feat(Archive Run): Add scheduled option to select runs drop down
geoffreykwan Aug 29, 2023
e3a6f88
chore(Archive Run): Clean up code
geoffreykwan Aug 29, 2023
b11404e
chore(Archive Run): Clean up tests
geoffreykwan Aug 29, 2023
1239af0
Remove warn color text in archived view
breity Aug 30, 2023
ed251b8
Merge branch 'develop' into issue-1012-allow-teacher-to-archive-runs
geoffreykwan Aug 30, 2023
0287de6
test(Archive Run): Fix teacher-run-list-item test
geoffreykwan Aug 30, 2023
f260318
feat(Archive Run): Use select runs option enum
geoffreykwan Aug 31, 2023
0a905b3
test(Archive Run): Clean up test and add missing new line
geoffreykwan Sep 3, 2023
6772561
test(Archive Run): Clean up test
geoffreykwan Sep 6, 2023
2aaef4e
Merge branch 'develop' into issue-1012-allow-teacher-to-archive-runs
geoffreykwan Sep 6, 2023
83d4924
fix(Archive Run): Display first 10 runs immediately
geoffreykwan Sep 12, 2023
71c635a
feat(Archive Run): Only show number of runs after all runs are loaded
geoffreykwan Sep 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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