diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7646f44884c..d67aecf7795 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,9 +1,9 @@ +import { FormsModule } from '@angular/forms'; import { HTTP_INTERCEPTORS, HttpRequest, HttpHandler, HttpInterceptor } from '@angular/common/http'; import { Injectable, NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { FormsModule } from '@angular/forms'; -import { PublicLibraryComponent } from './modules/library/public-library/public-library.component'; import { PersonalLibraryComponent } from './modules/library/personal-library/personal-library.component'; +import { PublicLibraryComponent } from './modules/library/public-library/public-library.component'; +import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: '', loadChildren: () => import('./home/home.module').then((m) => m.HomeModule) }, @@ -56,6 +56,10 @@ const routes: Routes = [ { path: 'teacher', loadChildren: () => import('./teacher/teacher.module').then((m) => m.TeacherModule) + }, + { + path: 'survey', + loadChildren: () => import('./student/survey/survey.module').then((m) => m.SurveyModule) } ]; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 808b00ed04f..5f5c39966c0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -190,7 +190,8 @@ export class AppComponent { !this.router.url.includes('/login') && !this.router.url.includes('/join') && !this.router.url.includes('/contact') && - !this.router.url.includes('/forgot') + !this.router.url.includes('/forgot') && + !this.router.url.includes('/survey') ); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8deefe3cc8e..fbe181f347b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,29 +1,29 @@ -import { NgModule, inject, provideAppInitializer } from '@angular/core'; +import { AnnouncementComponent } from './announcement/announcement.component'; +import { AnnouncementDialogComponent } from './announcement/announcement.component'; +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; +import { ArchiveProjectService } from './services/archive-project.service'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserModule } from '@angular/platform-browser'; +import { ConfigService } from './services/config.service'; +import { FooterComponent } from './modules/footer/footer.component'; import { FormsModule } from '@angular/forms'; -import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { HeaderComponent } from './modules/header/header.component'; +import { HomeModule } from './home/home.module'; import { HttpErrorInterceptor } from './http-error.interceptor'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterModule } from '@angular/router'; +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { MatDialogModule } from '@angular/material/dialog'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar'; -import { AppComponent } from './app.component'; -import { AppRoutingModule } from './app-routing.module'; -import { ConfigService } from './services/config.service'; -import { HomeModule } from './home/home.module'; +import { MobileMenuComponent } from './modules/mobile-menu/mobile-menu.component'; +import { NgModule, inject, provideAppInitializer } from '@angular/core'; +import { RecaptchaV3Module, RECAPTCHA_V3_SITE_KEY, RECAPTCHA_BASE_URL } from 'ng-recaptcha-2'; +import { RouterModule } from '@angular/router'; import { StudentService } from './student/student.service'; -import { UserService } from './services/user.service'; import { TeacherService } from './teacher/teacher.service'; -import { MobileMenuComponent } from './modules/mobile-menu/mobile-menu.component'; -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-2'; -import { ArchiveProjectService } from './services/archive-project.service'; -import { FooterComponent } from './modules/footer/footer.component'; -import { HeaderComponent } from './modules/header/header.component'; +import { UserService } from './services/user.service'; export function initialize( configService: ConfigService, @@ -47,17 +47,17 @@ export function initialize( bootstrap: [AppComponent], imports: [ AnnouncementComponent, - BrowserModule, - BrowserAnimationsModule, - FormsModule, AppRoutingModule, + BrowserAnimationsModule, + BrowserModule, FooterComponent, + FormsModule, HeaderComponent, HomeModule, - MobileMenuComponent, + MatDialogModule, MatSidenavModule, MatSnackBarModule, - MatDialogModule, + MobileMenuComponent, RecaptchaV3Module, RouterModule.forRoot([], { scrollPositionRestoration: 'enabled', @@ -73,9 +73,9 @@ export function initialize( TeacherService, UserService, provideAppInitializer(() => { - const initializerFn = (initialize)(inject(ConfigService), inject(UserService)); - return initializerFn(); - }), + const initializerFn = initialize(inject(ConfigService), inject(UserService)); + return initializerFn(); + }), { provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { diff --git a/src/app/domain/run.ts b/src/app/domain/run.ts index 51960909f40..86e1d56704e 100644 --- a/src/app/domain/run.ts +++ b/src/app/domain/run.ts @@ -15,6 +15,7 @@ export class Run { owner: User; sharedOwners: User[] = []; project: Project; + private isSurvey: boolean; static readonly VIEW_STUDENT_WORK_PERMISSION: number = 1; static readonly GRADE_AND_MANAGE_PERMISSION: number = 2; @@ -39,32 +40,32 @@ export class Run { } } - public canViewStudentWork(userId) { + public canViewStudentWork(userId): boolean { return ( this.isOwner(userId) || this.isSharedOwnerWithPermission(userId, Run.VIEW_STUDENT_WORK_PERMISSION) ); } - public canGradeAndManage(userId) { + public canGradeAndManage(userId): boolean { return ( this.isOwner(userId) || this.isSharedOwnerWithPermission(userId, Run.GRADE_AND_MANAGE_PERMISSION) ); } - public canViewStudentNames(userId) { + public canViewStudentNames(userId): boolean { return ( this.isOwner(userId) || this.isSharedOwnerWithPermission(userId, Run.VIEW_STUDENT_NAMES_PERMISSION) ); } - isOwner(userId) { + isOwner(userId): boolean { return this.owner.id == userId; } - isSharedOwnerWithPermission(userId, permissionId) { + isSharedOwnerWithPermission(userId, permissionId): boolean { for (const sharedOwner of this.sharedOwners) { if (sharedOwner.id == userId) { return this.userHasPermission(sharedOwner, permissionId); @@ -73,7 +74,7 @@ export class Run { return false; } - userHasPermission(user: User, permission: number) { + userHasPermission(user: User, permission: number): boolean { return user.permissions.includes(permission); } @@ -92,6 +93,10 @@ export class Run { private hasEndTime(): boolean { return this.endTime != null; } + + isSurveyRun(): boolean { + return this.isSurvey; + } } export function sortByRunStartTimeDesc(a: Run, b: Run): number { diff --git a/src/app/modules/header/header-account-menu/header-account-menu.component.html b/src/app/modules/header/header-account-menu/header-account-menu.component.html index 541a6e0bc16..db4550b0f1e 100644 --- a/src/app/modules/header/header-account-menu/header-account-menu.component.html +++ b/src/app/modules/header/header-account-menu/header-account-menu.component.html @@ -6,80 +6,82 @@
- + + + - - account_box - - {{ firstName }} {{ lastName }} -
-
- Switch back to original user -
- - - home - Student Home - - - home - Teacher Home - - - edit - Edit Profile - - - edit - Edit Profile - - - settings - Researcher Tools - - - settings - Admin Tools - - - help - Help - - + home + Student Home + + + home + Teacher Home + + + edit + Edit Profile + + + edit + Edit Profile + + + settings + Researcher Tools + + + settings + Admin Tools + + + help + Help + + + } exit_to_app Sign Out diff --git a/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts b/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts index 642a3db0a80..b7c611e9b94 100644 --- a/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts +++ b/src/app/modules/header/header-account-menu/header-account-menu.component.spec.ts @@ -1,40 +1,33 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HeaderAccountMenuComponent } from './header-account-menu.component'; -import { User } from '../../../domain/user'; -import { MatMenuModule } from '@angular/material/menu'; import { ConfigService } from '../../../services/config.service'; -import { Observable } from 'rxjs'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { Config } from '../../../domain/config'; -import { provideRouter } from '@angular/router'; +import { HeaderAccountMenuComponent } from './header-account-menu.component'; +import { of } from 'rxjs'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -export class MockConfigService { - getConfig(): Observable { - const config: Config = { - contextPath: '/wise', - logOutURL: '/logout', - currentTime: new Date('2018-10-17T00:00:00.0').getTime() - }; - return Observable.create((observer) => { - observer.next(config); - observer.complete(); - }); - } -} +import { provideRouter } from '@angular/router'; +import { User } from '../../../domain/user'; +import { MockProvider } from 'ng-mocks'; describe('HeaderAccountMenuComponent', () => { let component: HeaderAccountMenuComponent; let fixture: ComponentFixture; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [HeaderAccountMenuComponent, MatMenuModule], - providers: [{ provide: ConfigService, useClass: MockConfigService }, provideRouter([]), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}).compileComponents(); - }) - ); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HeaderAccountMenuComponent], + providers: [ + MockProvider(ConfigService, { + getConfig: () => + of({ + contextPath: '/wise', + logOutURL: '/logout', + currentTime: new Date('2018-10-17T00:00:00.0').getTime() + }) + }), + provideRouter([]), + provideHttpClient(withInterceptorsFromDi()) + ] + }).compileComponents(); + })); beforeEach(() => { fixture = TestBed.createComponent(HeaderAccountMenuComponent); diff --git a/src/app/modules/header/header-account-menu/header-account-menu.component.ts b/src/app/modules/header/header-account-menu/header-account-menu.component.ts index 652c3bde686..6dfa7070406 100644 --- a/src/app/modules/header/header-account-menu/header-account-menu.component.ts +++ b/src/app/modules/header/header-account-menu/header-account-menu.component.ts @@ -11,18 +11,18 @@ import { CommonModule } from '@angular/common'; import { FlexLayoutModule } from '@angular/flex-layout'; @Component({ - selector: 'app-header-account-menu', - templateUrl: './header-account-menu.component.html', - styleUrl: './header-account-menu.component.scss', - imports: [ - CommonModule, - FlexLayoutModule, - MatButtonModule, - MatIconModule, - MatMenuModule, - MatDividerModule, - RouterModule - ] + selector: 'app-header-account-menu', + templateUrl: './header-account-menu.component.html', + styleUrl: './header-account-menu.component.scss', + imports: [ + CommonModule, + FlexLayoutModule, + MatButtonModule, + MatDividerModule, + MatIconModule, + MatMenuModule, + RouterModule + ] }) export class HeaderAccountMenuComponent implements OnInit { protected firstName: string = ''; @@ -33,7 +33,10 @@ export class HeaderAccountMenuComponent implements OnInit { private switchToOriginalUserURL = '/api/logout/impersonate'; @Input() user: User; - constructor(private configService: ConfigService, private http: HttpClient) {} + constructor( + private configService: ConfigService, + private http: HttpClient + ) {} ngOnInit(): void { this.configService.getConfig().subscribe((config) => { diff --git a/src/app/services/accessLinkService.spec.ts b/src/app/services/accessLinkService.spec.ts new file mode 100644 index 00000000000..ba46165b344 --- /dev/null +++ b/src/app/services/accessLinkService.spec.ts @@ -0,0 +1,33 @@ +import { AccessLinkService } from './accessLinkService'; +import { ConfigService } from './config.service'; +import { TestBed } from '@angular/core/testing'; +import { HttpClient } from '@angular/common/http'; +import { MockProviders } from 'ng-mocks'; + +let service: AccessLinkService; +export class MockConfigService {} +describe('AccessLinkService', () => { + const linkBase = 'wise.berkeley.edu/run-survey/dog1234-'; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AccessLinkService, MockProviders(ConfigService, HttpClient)] + }); + service = TestBed.inject(AccessLinkService); + spyOn(TestBed.inject(ConfigService), 'getWISEHostname').and.returnValue('wise.berkeley.edu'); + spyOn(TestBed.inject(ConfigService), 'getContextPath').and.returnValue(''); + }); + + it('should get access links', () => { + const accessLinks = service.getAccessLinks('dog1234', ['1', 'test', 'this is a test']); + expect(accessLinks[0]).toEqual(linkBase + '1'); + expect(accessLinks[1]).toEqual(linkBase + 'test'); + expect(accessLinks[2]).toEqual(linkBase + 'this is a test'); + }); + + it('should get period from access link', () => { + expect(service.getPeriodFromAccessLink(linkBase + '1')).toEqual('1'); + expect(service.getPeriodFromAccessLink(linkBase + 'test')).toEqual('test'); + expect(service.getPeriodFromAccessLink(linkBase + 'this is a test')).toEqual('this is a test'); + }); +}); diff --git a/src/app/services/accessLinkService.ts b/src/app/services/accessLinkService.ts new file mode 100644 index 00000000000..219d08efbfe --- /dev/null +++ b/src/app/services/accessLinkService.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { ConfigService } from './config.service'; + +@Injectable() +export class AccessLinkService { + constructor(private configService: ConfigService) {} + + getAccessLinks(runCode: string, periods: string[]): string[] { + const host = this.configService.getWISEHostname() + this.configService.getContextPath(); + const linkBase = `${host}/run-survey/${runCode}-`; + return periods.map((period) => linkBase + period); + } + + getPeriodFromAccessLink(link: string): string { + return link.slice(link.lastIndexOf('-') + 1); + } +} diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts index 993ff48218d..d19e24ab221 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/user.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; +import { ConfigService } from './config.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { concatMap, tap } from 'rxjs/operators'; -import { User } from '../domain/user'; import { HttpParams } from '@angular/common/http'; -import { ConfigService } from './config.service'; -import { Teacher } from '../domain/teacher'; +import { Injectable } from '@angular/core'; import { Student } from '../domain/student'; +import { tap } from 'rxjs/operators'; +import { Teacher } from '../domain/teacher'; +import { User } from '../domain/user'; @Injectable() export class UserService { @@ -23,7 +23,10 @@ export class UserService { isAuthenticated = false; redirectUrl: string; // redirect here after logging in - constructor(private http: HttpClient, private configService: ConfigService) {} + constructor( + private configService: ConfigService, + private http: HttpClient + ) {} getUser(): BehaviorSubject { return this.user$; @@ -57,6 +60,10 @@ export class UserService { return this.isRole('admin'); } + isSurveyStudent(): boolean { + return this.isRole('surveyStudent'); + } + private isRole(role: string): boolean { return this.isAuthenticated && this.getRoles().includes(role); } @@ -75,16 +82,14 @@ export class UserService { retrieveUser(username?: string): Observable { const params = new HttpParams().set('username', username); - return this.http - .get(this.userUrl, { params: params }) - .pipe( - tap((user) => { - if (user != null && user.id != null) { - this.isAuthenticated = true; - } - this.user$.next(user); - }) - ); + return this.http.get(this.userUrl, { params: params }).pipe( + tap((user) => { + if (user != null && user.id != null) { + this.isAuthenticated = true; + } + this.user$.next(user); + }) + ); } checkAuthentication(username, password) { @@ -146,13 +151,11 @@ export class UserService { const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'); let body = new HttpParams(); body = body.set('newPassword', newPassword); - return this.http - .post(this.unlinkGoogleAccountUrl, body, { headers: headers }) - .pipe( - tap((user) => { - this.user$.next(user); - }) - ); + return this.http.post(this.unlinkGoogleAccountUrl, body, { headers: headers }).pipe( + tap((user) => { + this.user$.next(user); + }) + ); } getUserByGoogleId(googleUserId: string) { diff --git a/src/app/student/auth.guard.ts b/src/app/student/auth.guard.ts index af6190eebe0..de8ceca5cde 100644 --- a/src/app/student/auth.guard.ts +++ b/src/app/student/auth.guard.ts @@ -1,25 +1,37 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; import { UserService } from '../services/user.service'; @Injectable() export class AuthGuard { - constructor(private userService: UserService, private router: Router) {} + constructor( + private userService: UserService, + private router: Router + ) {} canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.checkLogin(state.url); } - checkLogin(url: string): boolean { - if (this.userService.isStudent() || url.includes('/preview/unit')) { - return true; + private checkLogin(url: string): boolean { + let canAccessPage = false; + if (this.canAccess(url)) { + canAccessPage = true; } else if (this.userService.isAuthenticated) { this.router.navigate(['/']); - return false; } else { this.userService.redirectUrl = url; this.router.navigate(['/login']); - return false; } + return canAccessPage; + } + + private canAccess(url: string): boolean { + return ( + (this.userService.isStudent() || + url.includes('/preview/unit') || + url.includes('/workgroupLimitReached')) && + !(this.userService.isSurveyStudent() && url.includes('/home')) + ); } } diff --git a/src/app/student/run-info.ts b/src/app/student/run-info.ts index 5dd5e449ad6..1ff607b851b 100644 --- a/src/app/student/run-info.ts +++ b/src/app/student/run-info.ts @@ -8,4 +8,5 @@ export class RunInfo { error: string; name: string; wiseVersion: number; + isSurvey: boolean; } diff --git a/src/app/student/student.component.ts b/src/app/student/student.component.ts index e80097f191b..06ea93d924f 100644 --- a/src/app/student/student.component.ts +++ b/src/app/student/student.component.ts @@ -2,10 +2,10 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ - selector: 'app-student', - templateUrl: './student.component.html', - styleUrls: ['./student.component.scss'], - standalone: false + selector: 'app-student', + templateUrl: './student.component.html', + styleUrls: ['./student.component.scss'], + standalone: false }) export class StudentComponent implements OnInit { constructor(private router: Router) {} @@ -13,6 +13,10 @@ export class StudentComponent implements OnInit { ngOnInit() {} isShowingAngularJSApp() { - return this.router.url.includes('/student/unit') || this.router.url.includes('/preview/unit'); + return ( + this.router.url.includes('/student/unit') || + this.router.url.includes('/preview/unit') || + this.router.url.includes('/survey') + ); } } diff --git a/src/app/student/survey/survey-completed/survey-completed.component.html b/src/app/student/survey/survey-completed/survey-completed.component.html new file mode 100644 index 00000000000..f7fd1100657 --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.html @@ -0,0 +1,22 @@ +
+ + + +

Your responses have been submitted. You can close this window.

+

Thank you!

+
+
+
diff --git a/src/app/student/survey/survey-completed/survey-completed.component.scss b/src/app/student/survey/survey-completed/survey-completed.component.scss new file mode 100644 index 00000000000..d9477abdc8b --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.scss @@ -0,0 +1,3 @@ +mat-card { + margin: 0% 30%; +} \ No newline at end of file diff --git a/src/app/student/survey/survey-completed/survey-completed.component.spec.ts b/src/app/student/survey/survey-completed/survey-completed.component.spec.ts new file mode 100644 index 00000000000..cabdc09e799 --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SurveyCompletedComponent } from './survey-completed.component'; +import { provideRouter } from '@angular/router'; + +describe('SurveyCompletedComponent', () => { + let component: SurveyCompletedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SurveyCompletedComponent], + providers: [provideRouter([])] + }).compileComponents(); + + fixture = TestBed.createComponent(SurveyCompletedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/student/survey/survey-completed/survey-completed.component.ts b/src/app/student/survey/survey-completed/survey-completed.component.ts new file mode 100644 index 00000000000..42b8650e9d5 --- /dev/null +++ b/src/app/student/survey/survey-completed/survey-completed.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { RouterModule } from '@angular/router'; + +@Component({ + imports: [MatCardModule, RouterModule], + templateUrl: './survey-completed.component.html', + selector: 'survey-completed', + styleUrl: './survey-completed.component.scss' +}) +export class SurveyCompletedComponent {} diff --git a/src/app/student/survey/survey-routing.module.ts b/src/app/student/survey/survey-routing.module.ts new file mode 100644 index 00000000000..bbb1ac8fec4 --- /dev/null +++ b/src/app/student/survey/survey-routing.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { StudentComponent } from '../student.component'; +import { SurveyCompletedComponent } from './survey-completed/survey-completed.component'; +import { WorkgroupLimitReachedComponent } from './workgroup-limit-reached/workgroup-limit-reached.component'; + +const studentRoutes: Routes = [ + { + path: '', + component: StudentComponent, + children: [ + { path: '', redirectTo: '/', pathMatch: 'full' }, + { path: 'completed', component: SurveyCompletedComponent, pathMatch: 'full' }, + { + path: 'workgroupLimitReached', + component: WorkgroupLimitReachedComponent, + pathMatch: 'full' + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(studentRoutes)], + exports: [RouterModule] +}) +export class SurveyRoutingModule {} diff --git a/src/app/student/survey/survey.module.ts b/src/app/student/survey/survey.module.ts new file mode 100644 index 00000000000..2c41a9c3914 --- /dev/null +++ b/src/app/student/survey/survey.module.ts @@ -0,0 +1,7 @@ +import { NgModule } from '@angular/core'; +import { SurveyRoutingModule } from './survey-routing.module'; + +@NgModule({ + imports: [SurveyRoutingModule] +}) +export class SurveyModule {} diff --git a/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html new file mode 100644 index 00000000000..43f66ae2fe8 --- /dev/null +++ b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html @@ -0,0 +1,21 @@ +
+ + + +

Sorry, this unit has reached the maximum number of submissions.

+
+
+
diff --git a/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.spec.ts b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.spec.ts new file mode 100644 index 00000000000..6a0a5b89252 --- /dev/null +++ b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { WorkgroupLimitReachedComponent } from './workgroup-limit-reached.component'; + +describe('WorkgroupLimitReachedComponent', () => { + let component: WorkgroupLimitReachedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WorkgroupLimitReachedComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(WorkgroupLimitReachedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.ts b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.ts new file mode 100644 index 00000000000..3ad55ad5d7d --- /dev/null +++ b/src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; + +@Component({ + selector: 'workgroup-limit-reached', + imports: [MatCardModule], + templateUrl: './workgroup-limit-reached.component.html' +}) +export class WorkgroupLimitReachedComponent {} diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.html b/src/app/teacher/create-run-dialog/create-run-dialog.component.html index 85f7783d72f..09b40e7b180 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.html +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.html @@ -1,123 +1,173 @@

Use with Class

-
- -

- {{ project.metadata.title }} (Unit ID: {{ project.id }}) -

-

1. Choose Periods

-

- - - {{ control.controls.name.value }} - - -

- - Add your own periods - - -

- For "Period 9", just enter the number 9. Separate periods with commas (e.g. "Section 1, - Section 2"). Manually named periods should be no more than 16 characters long. -

- -

2. Choose Students Per Team

- - Only 1 student - 1-3 students - - -

3. Set Schedule

-
-
- - Start date - - - - Start date is required - -
-
- - End date - - - - -
-
- - Lock After End Date   +@if (!isCreated) { + + +

+ {{ project.metadata.title }} + (Unit ID: {{ project.id }}) +

+

1. Choose Run Type

+
+ + Default + Survey + + help
-
-

- Note: These dates can be changed at any time from your Class Schedule. Just select "Edit - Settings" from the unit's dropdown menu. -

- - - - - - - + + @if (isDefaultRun()) { +

2. Choose Periods

+

+ @for (control of selectedPeriodsControl.controls; track $index) { + + + {{ control.controls.name.value }} + + + } +

+ + Add your own periods + + +

+ For "Period 9", just enter the number 9. Separate periods with commas (e.g. "Section 1, + Section 2"). Manually named periods should be no more than 16 characters long. +

+ +

3. Choose Students Per Team

+ + Only 1 student + 1-3 students + + +

4. Set Schedule

+ } @else { +

2. Set Schedule

+ } +
+
+ + Start date + + + + Start date is required + +
+
+ + End date + + + + +
+ @if (isDefaultRun()) { +
+ + Lock After End Date  + help +
+ } +
+

+ Note: These dates can be changed at any time from your Class Schedule. Just select "Edit + Settings" from the unit's dropdown menu. +

+ + + + + + +} @else {

Success! This unit has been added to your Class Schedule.

{{ run.name }}

-

Access Code: {{ run.runCode }}

-

- Important: Every classroom unit has a unique Access Code. Students use this code to register - for a unit. Give the code to your students when they first sign up for a WISE account. If - they already have WISE accounts, have them log in and then select "Add Unit" from the - student home page. -

-

- You can always find the Access Code for each classroom unit in your Class Schedule. -

+ @if (isDefaultRun()) { +

Access Code: {{ run.runCode }}

+

+ Important: Every classroom unit has a unique Access Code. Students use this code to + register for a unit. Give the code to your students when they first sign up for a WISE + account. If they already have WISE accounts, have them log in and then select "Add Unit" + from the student home page. +

+

+ You can always find the Access Code for each classroom unit in your Class Schedule. +

+ } @else { +

+ Access Link: + +

+

+ Important: Every survey unit has a unique Access Link. Participants can use this link to + complete the unit without a WISE account. +

+

You can always find the Access Link for each survey unit in your Class Schedule.

+ }
- + @if (isGoogleUser() && isGoogleClassroomEnabled()) { + + } -
+} diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.scss b/src/app/teacher/create-run-dialog/create-run-dialog.component.scss index 3140da7417d..6d21ba01128 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.scss +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.scss @@ -5,3 +5,7 @@ .period-input { width: 100%; } + +.access-link { + text-transform: lowercase; +} \ No newline at end of file diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts b/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts index 1a04d0b61d9..956397f296e 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.spec.ts @@ -1,19 +1,19 @@ +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { By } from '@angular/platform-browser'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TeacherService } from '../teacher.service'; +import { ConfigService } from '../../services/config.service'; +import { Course } from '../../domain/course'; import { CreateRunDialogComponent } from './create-run-dialog.component'; import { MatDialogRef, MatDialog } from '@angular/material/dialog'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Observable } from 'rxjs'; import { of } from 'rxjs'; import { Project } from '../../domain/project'; +import { Router } from '@angular/router'; import { Run } from '../../domain/run'; -import { By } from '@angular/platform-browser'; -import { Course } from '../../domain/course'; -import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { TeacherService } from '../teacher.service'; import { User } from '../../domain/user'; import { UserService } from '../../services/user.service'; -import { ConfigService } from '../../services/config.service'; -import { Router } from '@angular/router'; export class MockTeacherService { createRun() { @@ -135,10 +135,29 @@ describe('CreateRunDialogComponent', () => { component.periodsGroup.controls[0].get('checkbox').setValue(true); component.periodsGroup.controls[2].get('checkbox').setValue(true); component.periodsGroup.controls[4].get('checkbox').setValue(true); - component.customPeriods.setValue('hello'); + component['customPeriods'].setValue('hello'); expect(component.getPeriodsString()).toEqual('1,3,5,hello'); }); + it('should not show period or max workgroup size options if survey is checked', () => { + let h3ElementsText = Array.from(fixture.nativeElement.querySelectorAll('h3')).map( + (element: HTMLElement) => element.innerText + ); + expect(h3ElementsText.length).toEqual(4); + expect(h3ElementsText.includes('2. Choose Periods')).toBeTrue(); + expect(h3ElementsText.includes('3. Choose Students Per Team')).toBeTrue(); + + component.form.controls['runType'].setValue('survey'); + fixture.detectChanges(); + + h3ElementsText = Array.from(fixture.nativeElement.querySelectorAll('h3')).map( + (element: HTMLElement) => element.innerText + ); + expect(h3ElementsText.length).toEqual(2); + expect(h3ElementsText.includes('2. Choose Periods')).toBeFalse(); + expect(h3ElementsText.includes('3. Choose Students Per Team')).toBeFalse(); + }); + it('should disable submit button and invalidate form on initial state (when no period is selected)', () => { const submitButton = getSubmitButton(); expect(component.form.valid).toBeFalsy(); @@ -150,7 +169,7 @@ describe('CreateRunDialogComponent', () => { fixture.detectChanges(); expect(component.form.valid).toBeTruthy(); component.periodsGroup.controls[0].get('checkbox').setValue(false); - component.customPeriods.setValue('Section A, Section B'); + component['customPeriods'].setValue('Section A, Section B'); fixture.detectChanges(); expect(component.form.valid).toBeTruthy(); }); @@ -185,6 +204,7 @@ describe('CreateRunDialogComponent', () => { expect(teacherService.createRun).toHaveBeenCalledWith( 1, '1,', + false, '3', jasmine.any(Number), jasmine.any(Number), @@ -205,6 +225,7 @@ describe('CreateRunDialogComponent', () => { expect(teacherService.createRun).toHaveBeenCalledWith( 1, '1,', + false, '3', jasmine.any(Number), jasmine.any(Number), diff --git a/src/app/teacher/create-run-dialog/create-run-dialog.component.ts b/src/app/teacher/create-run-dialog/create-run-dialog.component.ts index beaa061d1e1..ae689a58345 100644 --- a/src/app/teacher/create-run-dialog/create-run-dialog.component.ts +++ b/src/app/teacher/create-run-dialog/create-run-dialog.component.ts @@ -1,5 +1,8 @@ -import { Component, Inject } from '@angular/core'; +import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; +import { Component, Inject } from '@angular/core'; +import { ConfigService } from '../../services/config.service'; +import { finalize } from 'rxjs/operators'; import { FormsModule, ReactiveFormsModule, @@ -9,85 +12,87 @@ import { FormGroup, Validators } from '@angular/forms'; +import { ListClassroomCoursesDialogComponent } from '../list-classroom-courses-dialog/list-classroom-courses-dialog.component'; import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; -import { MatInputModule } from '@angular/material/input'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDatepickerModule } from '@angular/material/datepicker'; -import { provideNativeDateAdapter } from '@angular/material/core'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatCardModule } from '@angular/material/card'; -import { Router } from '@angular/router'; -import { finalize } from 'rxjs/operators'; import { Project } from '../../domain/project'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { Router } from '@angular/router'; +import { TeacherRun } from '../teacher-run'; import { TeacherService } from '../teacher.service'; import { UserService } from '../../services/user.service'; -import { ConfigService } from '../../services/config.service'; -import { ListClassroomCoursesDialogComponent } from '../list-classroom-courses-dialog/list-classroom-courses-dialog.component'; -import { TeacherRun } from '../teacher-run'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatRadioModule } from '@angular/material/radio'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { AccessLinkService } from '../../services/accessLinkService'; @Component({ imports: [ + ClipboardModule, CommonModule, FormsModule, - ReactiveFormsModule, MatButtonModule, - MatDialogModule, - MatDividerModule, - MatInputModule, + MatCardModule, MatCheckboxModule, MatDatepickerModule, + MatDialogModule, + MatDividerModule, + MatFormFieldModule, MatIconModule, + MatInputModule, + MatProgressBarModule, MatRadioModule, MatTooltipModule, - MatFormFieldModule, - MatProgressBarModule, - MatCardModule + ReactiveFormsModule ], - providers: [provideNativeDateAdapter()], + providers: [AccessLinkService, provideNativeDateAdapter()], selector: 'create-run-dialog', styleUrl: './create-run-dialog.component.scss', templateUrl: './create-run-dialog.component.html' }) export class CreateRunDialogComponent { + protected accessLinks: string[] = []; + protected customPeriods: FormControl; + private endDateControl: FormControl; form: FormGroup; - project: Project; + protected isCreated: boolean = false; + protected isCreating: boolean = false; + protected maxStartDate: Date; + protected minEndDate: Date; + private periodOptions: string[] = []; periodsGroup: FormArray; - customPeriods: FormControl; - maxStudentsPerTeam: number; - maxStartDate: Date; - minEndDate: Date; - endDateControl: FormControl; - periodOptions: string[] = []; - isCreating: boolean = false; - isCreated: boolean = false; + project: Project; run: TeacherRun = null; constructor( + private accessLinkService: AccessLinkService, private configService: ConfigService, @Inject(MAT_DIALOG_DATA) public data: any, public dialog: MatDialog, public dialogRef: MatDialogRef, private fb: FormBuilder, private router: Router, + private snackBar: MatSnackBar, private teacherService: TeacherService, private userService: UserService ) { this.project = data.project; - this.maxStudentsPerTeam = 3; } - ngOnInit() { + ngOnInit(): void { this.setPeriodOptions(); let hiddenControl = new FormControl('', Validators.required); this.periodsGroup = new FormArray( @@ -114,6 +119,7 @@ export class CreateRunDialogComponent { selectedPeriods: this.periodsGroup, customPeriods: this.customPeriods, periods: hiddenControl, + runType: new FormControl('default', Validators.required), maxStudentsPerTeam: new FormControl('3', Validators.required), startDate: new FormControl(new Date(), Validators.required), endDate: this.endDateControl, @@ -122,15 +128,15 @@ export class CreateRunDialogComponent { this.setDateRange(); } - isGoogleUser() { + protected isGoogleUser(): boolean { return this.userService.isGoogleUser(); } - isGoogleClassroomEnabled() { + protected isGoogleClassroomEnabled(): boolean { return this.configService.isGoogleClassroomEnabled(); } - setPeriodOptions() { + private setPeriodOptions(): void { for (let i = 1; i < 9; i++) { this.periodOptions.push(i.toString()); } @@ -140,27 +146,33 @@ export class CreateRunDialogComponent { return this.form.get('selectedPeriods'); } - mapPeriods(items: any[]): string[] { + private mapPeriods(items: any[]): string[] { const selectedPeriods = items.filter((item) => item.checkbox).map((item) => item.name); return selectedPeriods.length ? selectedPeriods : []; } - create() { + create(): void { this.isCreating = true; - const combinedPeriods = this.getPeriodsString(); - const startDate = this.form.controls['startDate'].value.getTime(); - let endDateValue = this.form.controls['endDate'].value; - let endDate = null; + const isSurvey: boolean = this.getFormControlValue('runType') === 'survey'; + const combinedPeriods = isSurvey + ? this.mapPeriods(this.periodsGroup.value).toString() + : this.getPeriodsString(); + const startDate: number = this.getFormControlValue('startDate').getTime(); + let endDateValue: Date = this.getFormControlValue('endDate'); + let endDate: number = null; if (endDateValue) { endDateValue.setHours(23, 59, 59); endDate = endDateValue.getTime(); } - const isLockedAfterEndDate = this.form.controls['isLockedAfterEndDate'].value; - const maxStudentsPerTeam = this.form.controls['maxStudentsPerTeam'].value; + const isLockedAfterEndDate: boolean = this.getFormControlValue('isLockedAfterEndDate'); + const maxStudentsPerTeam: number = isSurvey + ? 1 + : this.getFormControlValue('maxStudentsPerTeam'); this.teacherService .createRun( this.project.id, combinedPeriods, + isSurvey, maxStudentsPerTeam, startDate, endDate, @@ -173,6 +185,12 @@ export class CreateRunDialogComponent { ) .subscribe((newRun: TeacherRun) => { this.run = new TeacherRun(newRun); + if (this.run.isSurveyRun()) { + this.accessLinks = this.accessLinkService.getAccessLinks( + this.run.runCode, + this.run.periods + ); + } this.dialogRef.afterClosed().subscribe(() => { this.router.navigate(['/teacher/home/schedule'], { queryParams: { newRunId: newRun.id } @@ -195,16 +213,16 @@ export class CreateRunDialogComponent { } } - setDateRange() { - this.minEndDate = this.form.controls['startDate'].value; - this.maxStartDate = this.form.controls['endDate'].value; + protected setDateRange(): void { + this.minEndDate = this.getFormControlValue('startDate'); + this.maxStartDate = this.getFormControlValue('endDate'); } - closeAll() { + protected closeAll(): void { this.dialog.closeAll(); } - checkClassroomAuthorization() { + protected checkClassroomAuthorization(): void { this.teacherService .getClassroomAuthorizationUrl(this.userService.getUser().getValue().username) .subscribe(({ authorizationUrl }) => { @@ -222,7 +240,7 @@ export class CreateRunDialogComponent { }); } - getClassroomCourses() { + private getClassroomCourses(): void { this.teacherService .getClassroomCourses(this.userService.getUser().getValue().username) .subscribe((courses) => { @@ -233,7 +251,7 @@ export class CreateRunDialogComponent { }); } - updateLockedAfterEndDateCheckbox() { + updateLockedAfterEndDateCheckbox(): void { if (this.endDateControl.value == null) { this.form.controls['isLockedAfterEndDate'].setValue(false); this.form.controls['isLockedAfterEndDate'].disable(); @@ -241,4 +259,29 @@ export class CreateRunDialogComponent { this.form.controls['isLockedAfterEndDate'].enable(); } } + + protected isDefaultRun(): boolean { + return this.getFormControlValue('runType') === 'default'; + } + + private getFormControlValue(control: string): any { + return this.form.controls[control].value; + } + + protected copyMsg(): void { + this.snackBar.open($localize`Copied to clipboard.`); + } + + protected getPeriodFromAccessLink(link: string): string { + return this.accessLinkService.getPeriodFromAccessLink(link); + } + + protected setAsSurveyUnit(): void { + this.customPeriods.setValue(''); + this.periodsGroup.controls.forEach((control, index) => { + index === 0 + ? control.get('checkbox').setValue(true) + : control.get('checkbox').setValue(false); + }); + } } diff --git a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html index 0c6de51e89c..272eba9b018 100644 --- a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html +++ b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.html @@ -3,65 +3,69 @@

Edit Settings

{{ run.name }} (Run ID: {{ run.id }})

-

Class Periods

-
-
- {{ period }} - + @if (isDefaultRun) { +

Class Periods

+
+
+ {{ period }} + +
-
-
- - Add new period - - - {{ addPeriodMessage }} - -

- For "Period 9", just enter the number 9. -

-
- -

Students Per Team

- - - Only 1 student - - - 1-3 students - - - +
+ + Add new period + + + {{ addPeriodMessage }} + +

+ For "Period 9", just enter the number 9. +

+
+ +

Students Per Team

+ + + Only 1 student + + + 1-3 students + + + + }

Schedule  - - (Last student login: {{ run.lastRun | date: 'short' }}) - + @if (run.lastRun) { + + (Last student login: {{ run.lastRun | date: 'short' }}) + + }

@@ -95,23 +99,25 @@

{{ endDateMessage }}

-
- - Lock After End Date   - help -
+ @if (isDefaultRun) { +
+ + Lock After End Date   + help +
+ }
diff --git a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts index 9bc4973b17e..abfdcee4150 100644 --- a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts +++ b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.spec.ts @@ -53,10 +53,9 @@ export class MockTeacherService { } } +let component: RunSettingsDialogComponent; +let fixture: ComponentFixture; describe('RunSettingsDialogComponent', () => { - let component: RunSettingsDialogComponent; - let fixture: ComponentFixture; - const getStartDateInput = () => { return fixture.debugElement.nativeElement.querySelectorAll('input')[1]; }; @@ -199,4 +198,20 @@ describe('RunSettingsDialogComponent', () => { const message = component.translateMessageCode('periodNameAlreadyExists'); expect(message).toEqual('There is already a period with that name.'); }); + + surveyRun(); }); + +function surveyRun() { + describe('Survey Run', () => { + beforeEach(() => { + component['isDefaultRun'] = false; + fixture.detectChanges(); + }); + + it('should hide Student Per Team section', () => { + const radioGroup = fixture.debugElement.nativeElement.querySelector('mat-radio-group'); + expect(radioGroup).toBeNull(); + }); + }); +} diff --git a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts index 719e371c141..306f6d4926e 100644 --- a/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts +++ b/src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts @@ -7,10 +7,10 @@ import { formatDate } from '@angular/common'; import { TeacherRun } from '../teacher-run'; @Component({ - selector: 'app-run-settings-dialog', - templateUrl: './run-settings-dialog.component.html', - styleUrls: ['./run-settings-dialog.component.scss'], - standalone: false + selector: 'app-run-settings-dialog', + templateUrl: './run-settings-dialog.component.html', + styleUrls: ['./run-settings-dialog.component.scss'], + standalone: false }) export class RunSettingsDialogComponent implements OnInit { run: TeacherRun; @@ -28,6 +28,7 @@ export class RunSettingsDialogComponent implements OnInit { startDateMessage: string = ''; endDateMessage: string = ''; isLockedAfterEndDateMessage: string = ''; + protected isDefaultRun: boolean = true; maxStartDate: Date; minEndDate: Date; targetEndDate: Date; @@ -53,6 +54,7 @@ export class RunSettingsDialogComponent implements OnInit { this.isLockedAfterEndDateCheckboxEnabled = true; } this.initializeMessageCodeToMessage(); + this.isDefaultRun = !this.run.isSurveyRun(); } initializeMessageCodeToMessage() { diff --git a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html index 98f38db818d..57087867790 100644 --- a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html +++ b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html @@ -1,60 +1,87 @@ -

Share with Students

+@if (run.isSurveyRun()) { +

Share with Participants

+} @else { +

Share with Students

+}

{{ run.project.name }} (Run ID: {{ run.id }})

-

Copy this link to share with your students:

- -

- Students with WISE accounts can also select Add Unit+ and type the Access - Code: -

- - -

Add as an assignment in Google Classroom:

+ @if (run.isSurveyRun()) { +

+ This is a survey unit. Participants complete survey units anonymously and do not need a WISE + account. +

+

Copy this link to share with participants:

+ + } @else { +

Copy this link to share with your students:

-
+

+ Students with WISE accounts can also select Add Unit+ and type the Access + Code: +

+ + +

Add as an assignment in Google Classroom:

+ +
+ }
diff --git a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts index fcae1fd4a53..2b0a592941b 100644 --- a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts +++ b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.spec.ts @@ -3,16 +3,21 @@ import { ShareRunCodeDialogComponent } from './share-run-code-dialog.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ConfigService } from '../../services/config.service'; import { TeacherService } from '../teacher.service'; -import { UserService } from '../../services/user.service'; import { TeacherRun } from '../teacher-run'; import { Project } from '../../domain/project'; +import { AccessLinkService } from '../../services/accessLinkService'; +import { MatIconModule } from '@angular/material/icon'; +import { ClipboardModule } from '@angular/cdk/clipboard'; +import { MockProviders } from 'ng-mocks'; +import { CommonModule } from '@angular/common'; +import { UserService } from '../../services/user.service'; const runObj = new TeacherRun(); runObj.id = 1; runObj.runCode = 'Dog123'; +runObj.isSurveyRun = () => false; const project = new Project(); project.id = 1; project.name = 'Photosynthesis'; @@ -32,47 +37,73 @@ export class MockConfigService { } } -export class MockTeacherService {} - -export class MockUserService { - isGoogleUser() { - return true; - } -} - +let component: ShareRunCodeDialogComponent; +let fixture: ComponentFixture; describe('ShareRunCodeDialogComponent', () => { - let component: ShareRunCodeDialogComponent; - let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ShareRunCodeDialogComponent], - imports: [BrowserAnimationsModule, MatDialogModule, MatSnackBarModule], + imports: [ + BrowserAnimationsModule, + CommonModule, + ClipboardModule, + MatDialogModule, + MatIconModule, + MatSnackBarModule + ], providers: [ + MockProviders(AccessLinkService, MatDialogRef, TeacherService, UserService), { provide: ConfigService, useClass: MockConfigService }, - { provide: TeacherService, useClass: MockTeacherService }, - { provide: UserService, useClass: MockUserService }, - { provide: MatDialogRef, useValue: {} }, { provide: MAT_DIALOG_DATA, useValue: runObj } - ], - schemas: [NO_ERRORS_SCHEMA] + ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ShareRunCodeDialogComponent); component = fixture.componentInstance; + component.run.isSurveyRun = () => false; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should show run info', () => { + expect(fixture.debugElement.nativeElement.textContent).toContain('Photosynthesis (Run ID: 1)'); }); - it('should show run info and sharing url', () => { - const compiled = fixture.debugElement.nativeElement; - expect(compiled.textContent).toContain('Photosynthesis (Run ID: 1)'); - const url = `http://localhost:8080/login?accessCode=${component.run.runCode}`; - expect(compiled.textContent).toContain(url); - }); + defaultRun(); + surveyRun(); }); + +function defaultRun() { + describe('when run is a default run', () => { + beforeEach(() => { + component.run.isSurveyRun = () => false; + fixture.detectChanges(); + }); + it('should show access links to share with students', () => { + const textContent = fixture.debugElement.nativeElement.textContent; + expect(textContent).toContain('Copy this link to share with your students:'); + expect(textContent).toContain( + `http://localhost:8080/login?accessCode=${component.run.runCode}` + ); + }); + }); +} + +function surveyRun() { + describe('when run is a survey run', () => { + beforeEach(() => { + component.run.isSurveyRun = () => true; + spyOn(TestBed.inject(AccessLinkService), 'getAccessLinks').and.returnValue([ + 'http://localhost:8080/run-survey/Dog123-1' + ]); + component.ngOnInit(); + fixture.detectChanges(); + }); + it('should show access links to share with participants', () => { + const textContent = fixture.debugElement.nativeElement.textContent; + expect(textContent).toContain('Copy this link to share with participants:'); + expect(textContent).toContain(`http://localhost:8080/run-survey/Dog123-1`); + }); + }); +} diff --git a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts index 927ef5ac4b8..775dbc21b3f 100644 --- a/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts +++ b/src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts @@ -6,19 +6,22 @@ import { UserService } from '../../services/user.service'; import { TeacherRun } from '../teacher-run'; import { TeacherService } from '../teacher.service'; import { ListClassroomCoursesDialogComponent } from '../list-classroom-courses-dialog/list-classroom-courses-dialog.component'; +import { AccessLinkService } from '../../services/accessLinkService'; @Component({ - selector: 'app-share-run-code-dialog', - templateUrl: './share-run-code-dialog.component.html', - styleUrls: ['./share-run-code-dialog.component.scss'], - standalone: false + selector: 'app-share-run-code-dialog', + templateUrl: './share-run-code-dialog.component.html', + styleUrls: ['./share-run-code-dialog.component.scss'], + standalone: false }) export class ShareRunCodeDialogComponent { - code: string; - link: string; + protected accessLinks: string[] = []; + protected code: string; + protected link: string; constructor( @Inject(MAT_DIALOG_DATA) public run: TeacherRun, + private accessLinkService: AccessLinkService, private dialog: MatDialog, private snackBar: MatSnackBar, private teacherService: TeacherService, @@ -26,25 +29,28 @@ export class ShareRunCodeDialogComponent { private configService: ConfigService ) {} - ngOnInit() { + ngOnInit(): void { this.code = this.run.runCode; const host = this.configService.getWISEHostname() + this.configService.getContextPath(); this.link = `${host}/login?accessCode=${this.code}`; + if (this.run.isSurveyRun()) { + this.accessLinks = this.accessLinkService.getAccessLinks(this.run.runCode, this.run.periods); + } } - copyMsg() { + protected copyMsg(): void { this.snackBar.open($localize`Copied to clipboard.`); } - isGoogleUser() { + protected isGoogleUser(): boolean { return this.userService.isGoogleUser(); } - isGoogleClassroomEnabled() { + protected isGoogleClassroomEnabled(): boolean { return this.configService.isGoogleClassroomEnabled(); } - checkClassroomAuthorization() { + protected checkClassroomAuthorization(): void { this.teacherService .getClassroomAuthorizationUrl(this.userService.getUser().getValue().username) .subscribe(({ authorizationUrl }) => { @@ -62,7 +68,7 @@ export class ShareRunCodeDialogComponent { }); } - getClassroomCourses() { + private getClassroomCourses(): void { this.teacherService .getClassroomCourses(this.userService.getUser().getValue().username) .subscribe((courses) => { @@ -73,4 +79,8 @@ export class ShareRunCodeDialogComponent { }); }); } + + protected getPeriodFromAccessLink(link: string): string { + return this.accessLinkService.getPeriodFromAccessLink(link); + } } diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html index 55ef1c3c828..567eccebc72 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html @@ -36,13 +36,19 @@
- Access Code: {{ run.runCode }} (Share with students) + @if (run.isSurveyRun()) { + Survey Unit (Share with participants) + } @else { + Access Code: {{ run.runCode }} (Share with students) + }
Shared by {{ run.owner.firstName }} {{ run.owner.lastName }} diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss index bc0dc87e74f..9b1d5b138a2 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.scss @@ -28,3 +28,12 @@ margin: -8px; display: block; } + +.access-link { + text-transform: lowercase; +} + +.toggle-show:hover { + cursor: pointer; + text-decoration: underline; +} \ No newline at end of file diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts index 276f1bd15b5..4b65661c4e8 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.spec.ts @@ -1,27 +1,27 @@ +import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; +import { ArchiveProjectService } from '../../services/archive-project.service'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TeacherRunListItemComponent } from './teacher-run-list-item.component'; -import { TeacherService } from '../teacher.service'; -import { TeacherRun } from '../teacher-run'; import { ConfigService } from '../../services/config.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { MatCardModule } from '@angular/material/card'; import { MatDialogModule } from '@angular/material/dialog'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; -import { TeacherRunListItemHarness } from './teacher-run-list-item.harness'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; -import { RunMenuComponent } from '../run-menu/run-menu.component'; -import { ArchiveProjectService } from '../../services/archive-project.service'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { UserService } from '../../services/user.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { User } from '../../domain/user'; -import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { of } from 'rxjs'; -import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { RunMenuComponent } from '../run-menu/run-menu.component'; +import { TeacherRun } from '../teacher-run'; +import { TeacherRunListItemComponent } from './teacher-run-list-item.component'; +import { TeacherRunListItemHarness } from './teacher-run-list-item.harness'; +import { TeacherService } from '../teacher.service'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { User } from '../../domain/user'; +import { UserService } from '../../services/user.service'; export class MockTeacherService {} @@ -52,16 +52,18 @@ let userService: UserService; describe('TeacherRunListItemComponent', () => { beforeEach(async () => { TestBed.configureTestingModule({ - declarations: [RunMenuComponent, TeacherRunListItemComponent], - schemas: [NO_ERRORS_SCHEMA], - imports: [BrowserAnimationsModule, + declarations: [RunMenuComponent, TeacherRunListItemComponent], + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, MatCardModule, MatDialogModule, MatIconModule, MatMenuModule, MatSnackBarModule, - RouterTestingModule], - providers: [ + RouterTestingModule + ], + providers: [ ArchiveProjectService, { provide: ConfigService, useClass: MockConfigService }, ProjectTagService, @@ -69,8 +71,8 @@ describe('TeacherRunListItemComponent', () => { UserService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting() - ] -}); + ] + }); http = TestBed.inject(HttpClient); userService = TestBed.inject(UserService); spyOn(userService, 'getUserId').and.returnValue(userId); diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts index 48bda33ff6e..9efa3ae6ae7 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts @@ -1,22 +1,22 @@ import { Component, OnInit, Input, ElementRef, Output, EventEmitter } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { SafeStyle } from '@angular/platform-browser'; -import { TeacherRun } from '../teacher-run'; import { ConfigService } from '../../services/config.service'; +import { DomSanitizer } from '@angular/platform-browser'; import { flash } from '../../animations'; -import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { Router } from '@angular/router'; +import { SafeStyle } from '@angular/platform-browser'; import { ShareRunCodeDialogComponent } from '../share-run-code-dialog/share-run-code-dialog.component'; import { Subscription } from 'rxjs'; -import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; import { Tag } from '../../domain/tag'; +import { TeacherRun } from '../teacher-run'; @Component({ - animations: [flash], - selector: 'app-teacher-run-list-item', - styleUrl: './teacher-run-list-item.component.scss', - templateUrl: './teacher-run-list-item.component.html', - standalone: false + animations: [flash], + selector: 'app-teacher-run-list-item', + standalone: false, + styleUrl: './teacher-run-list-item.component.scss', + templateUrl: './teacher-run-list-item.component.html' }) export class TeacherRunListItemComponent implements OnInit { protected animateDelay: string = '0s'; @@ -30,12 +30,12 @@ export class TeacherRunListItemComponent implements OnInit { protected thumbStyle: SafeStyle; constructor( - private sanitizer: DomSanitizer, private configService: ConfigService, - private router: Router, - private elRef: ElementRef, private dialog: MatDialog, - private projectTagService: ProjectTagService + private elRef: ElementRef, + private projectTagService: ProjectTagService, + private router: Router, + private sanitizer: DomSanitizer ) {} ngOnInit(): void { @@ -91,7 +91,7 @@ export class TeacherRunListItemComponent implements OnInit { this.subscriptions.unsubscribe(); } - private getThumbStyle() { + private getThumbStyle(): SafeStyle { const DEFAULT_THUMB = 'assets/img/default-picture.svg'; const STYLE = `url(${this.run.project.projectThumb}), url(${DEFAULT_THUMB})`; return this.sanitizer.bypassSecurityTrustStyle(STYLE); @@ -132,7 +132,7 @@ export class TeacherRunListItemComponent implements OnInit { return run.isCompleted(this.configService.getCurrentServerTime()); } - shareCode(event: Event): void { + protected shareCode(event: Event): void { event.preventDefault(); this.dialog.open(ShareRunCodeDialogComponent, { data: this.run, diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts index ff5bccb258b..6ce576f700b 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.spec.ts @@ -33,6 +33,8 @@ import { ArchiveProjectResponse } from '../../domain/archiveProjectResponse'; import { Tag } from '../../domain/tag'; import { provideRouter } from '@angular/router'; import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { MockProvider } from 'ng-mocks'; +import { AccessLinkService } from '../../services/accessLinkService'; class TeacherScheduleStubComponent {} @@ -103,6 +105,7 @@ describe('TeacherRunListComponent', () => { SelectRunsControlsModule ], providers: [ + MockProvider(AccessLinkService), ArchiveProjectService, ConfigService, ProjectTagService, diff --git a/src/app/teacher/teacher.module.ts b/src/app/teacher/teacher.module.ts index 09243099843..3bcfff3d4b9 100644 --- a/src/app/teacher/teacher.module.ts +++ b/src/app/teacher/teacher.module.ts @@ -48,6 +48,7 @@ import { SelectTagsComponent } from './select-tags/select-tags.component'; import { UnitTagsComponent } from './unit-tags/unit-tags.component'; import { ColorService } from '../../assets/wise5/services/colorService'; import { NgSelectModule } from '@ng-select/ng-select'; +import { AccessLinkService } from '../services/accessLinkService'; const materialModules = [ MatAutocompleteModule, @@ -103,7 +104,7 @@ const materialModules = [ TeacherRunListComponent, TeacherRunListItemComponent ], - providers: [AuthGuard, ColorService, ProjectTagService], + providers: [AccessLinkService, AuthGuard, ColorService, ProjectTagService], exports: [TeacherComponent, UnitTagsComponent, materialModules] }) export class TeacherModule {} diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index a691bad2e3d..68ee76b6284 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; +import { CopyProjectDialogComponent } from '../modules/library/copy-project-dialog/copy-project-dialog.component'; +import { Course } from '../domain/course'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { Observable, Subject } from 'rxjs'; import { Project } from '../domain/project'; -import { Teacher } from '../domain/teacher'; import { Run } from '../domain/run'; -import { Course } from '../domain/course'; -import { CopyProjectDialogComponent } from '../modules/library/copy-project-dialog/copy-project-dialog.component'; +import { Teacher } from '../domain/teacher'; import { TeacherRun } from './teacher-run'; @Injectable() @@ -82,6 +82,7 @@ export class TeacherService { createRun( projectId: number, periods: string, + isSurvey: boolean, maxStudentsPerTeam: number, startDate: number, endDate: number, @@ -91,6 +92,7 @@ export class TeacherService { let body = new HttpParams(); body = body.set('projectId', projectId + ''); body = body.set('periods', periods); + body = body.set('isSurvey', isSurvey); body = body.set('maxStudentsPerTeam', maxStudentsPerTeam + ''); body = body.set('startDate', startDate + ''); if (endDate) { diff --git a/src/assets/wise5/common/stepTools/step-tools.component.spec.ts b/src/assets/wise5/common/stepTools/step-tools.component.spec.ts index 320e0b7cafd..45e073c1275 100644 --- a/src/assets/wise5/common/stepTools/step-tools.component.spec.ts +++ b/src/assets/wise5/common/stepTools/step-tools.component.spec.ts @@ -1,13 +1,13 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ClassroomStatusService } from '../../services/classroomStatusService'; -import { TeacherDataService } from '../../services/teacherDataService'; -import { TeacherProjectService } from '../../services/teacherProjectService'; -import { TeacherWebSocketService } from '../../services/teacherWebSocketService'; -import { StepToolsComponent } from './step-tools.component'; -import { StudentTeacherCommonServicesModule } from '../../../../app/student-teacher-common-services.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { StepToolsComponent } from './step-tools.component'; import { StudentNodeService } from '../../services/studentNodeService'; +import { StudentTeacherCommonServicesModule } from '../../../../app/student-teacher-common-services.module'; +import { TeacherDataService } from '../../services/teacherDataService'; +import { TeacherProjectService } from '../../services/teacherProjectService'; +import { TeacherWebSocketService } from '../../services/teacherWebSocketService'; describe('StepTools', () => { let component: StepToolsComponent; diff --git a/src/assets/wise5/common/stepTools/step-tools.component.ts b/src/assets/wise5/common/stepTools/step-tools.component.ts index f38f5f98420..c3aab33b425 100644 --- a/src/assets/wise5/common/stepTools/step-tools.component.ts +++ b/src/assets/wise5/common/stepTools/step-tools.component.ts @@ -1,19 +1,19 @@ +import { CommonModule } from '@angular/common'; import { Component, ViewEncapsulation } from '@angular/core'; import { Directionality } from '@angular/cdk/bidi'; -import { Subscription } from 'rxjs'; -import { NodeService } from '../../services/nodeService'; -import { TeacherDataService } from '../../services/teacherDataService'; -import { TeacherProjectService } from '../../services/teacherProjectService'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { NodeIconComponent } from '../../vle/node-icon/node-icon.component'; -import { FlexLayoutModule } from '@angular/flex-layout'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { MatInputModule } from '@angular/material/input'; +import { NodeIconComponent } from '../../vle/node-icon/node-icon.component'; +import { NodeService } from '../../services/nodeService'; +import { Subscription } from 'rxjs'; +import { TeacherDataService } from '../../services/teacherDataService'; +import { TeacherProjectService } from '../../services/teacherProjectService'; @Component({ encapsulation: ViewEncapsulation.None, diff --git a/src/assets/wise5/services/sessionService.ts b/src/assets/wise5/services/sessionService.ts index c009b4dcfeb..a9a279d10fe 100644 --- a/src/assets/wise5/services/sessionService.ts +++ b/src/assets/wise5/services/sessionService.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { ConfigService } from './configService'; -import { Observable, Subject } from 'rxjs'; +import { map, Observable, Subject } from 'rxjs'; @Injectable() export class SessionService { @@ -61,6 +61,13 @@ export class SessionService { }); } + logOutWithoutHomeRedirect(): Observable { + this.broadcastExit(); + return this.http + .get(this.configService.getSessionLogOutURL()) + .pipe(map(() => (this.sessionActive = false))); + } + isSessionActive(): boolean { return this.sessionActive; } diff --git a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html index aff929afab4..078ce86a3be 100644 --- a/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html +++ b/src/assets/wise5/themes/default/themeComponents/stepTools/step-tools.component.html @@ -12,7 +12,7 @@ {{ icons.prev }}
- +
  @@ -49,7 +49,7 @@
- +
+ @if (isSurvey && !isBranchNode && !nextNodeId) { +
+ + +
+ }
diff --git a/src/assets/wise5/vle/node/node.component.spec.ts b/src/assets/wise5/vle/node/node.component.spec.ts index 870427abb14..8bd899d8ba4 100644 --- a/src/assets/wise5/vle/node/node.component.spec.ts +++ b/src/assets/wise5/vle/node/node.component.spec.ts @@ -6,6 +6,10 @@ import { NodeComponent } from './node.component'; import { StudentTeacherCommonServicesModule } from '../../../../app/student-teacher-common-services.module'; import { Node } from '../../common/Node'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { TransitionLogic } from '../../common/TransitionLogic'; +import { By } from '@angular/platform-browser'; +import { MockComponent } from 'ng-mocks'; +import { SubmitSurveyComponent } from '../submit-survey/submit-survey.component'; let component: NodeComponent; let fixture: ComponentFixture; @@ -13,6 +17,7 @@ let fixture: ComponentFixture; describe('NodeComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + declarations: [MockComponent(SubmitSurveyComponent)], imports: [NodeComponent, StudentTeacherCommonServicesModule], providers: [provideHttpClient(withInterceptorsFromDi())] }).compileComponents(); @@ -30,7 +35,9 @@ describe('NodeComponent', () => { spyOn(TestBed.inject(VLEProjectService), 'getNodeById').and.returnValue({ components: [] }); spyOn(TestBed.inject(VLEProjectService), 'getNodeTitle').and.returnValue(''); component = fixture.componentInstance; - component.node = new Node(); + const node = new Node(); + node.transitionLogic = { transitions: [] } as TransitionLogic; + component.node = node; fixture.detectChanges(); }); @@ -38,7 +45,45 @@ describe('NodeComponent', () => { fixture.destroy(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + isDefaultRun_ShouldNotShowSubmitSurveyButton(); + isSurveyRun(); }); + +function isDefaultRun_ShouldNotShowSubmitSurveyButton() { + describe('isDefaultRun', () => { + beforeEach(() => { + component['isSurvey'] = false; + fixture.detectChanges(); + }); + it('should not show submit survey button', () => { + expect(fixture.debugElement.query(By.css('submit-survey'))).toBeNull(); + }); + }); +} + +function isSurveyRun() { + describe('isSurveyRun', () => { + beforeEach(() => { + component['isSurvey'] = true; + component['isBranchNode'] = false; + }); + describe('is last step in the unit', () => { + beforeEach(() => { + component['nextNodeId'] = null; + fixture.detectChanges(); + }); + it('should show submit survey button', () => { + expect(fixture.debugElement.query(By.css('submit-survey'))).not.toBeNull(); + }); + }); + describe('is not the last step in the unit', () => { + beforeEach(() => { + component['nextNodeId'] = 'nextNodeId'; + fixture.detectChanges(); + }); + it('should not show submit survey button', () => { + expect(fixture.debugElement.query(By.css('submit-survey'))).toBeNull(); + }); + }); + }); +} diff --git a/src/assets/wise5/vle/node/node.component.ts b/src/assets/wise5/vle/node/node.component.ts index 394d4e33d92..cbe9bbec31a 100644 --- a/src/assets/wise5/vle/node/node.component.ts +++ b/src/assets/wise5/vle/node/node.component.ts @@ -19,19 +19,23 @@ import { MatButtonModule } from '@angular/material/button'; import { ComponentStateInfoComponent } from '../../common/component-state-info/component-state-info.component'; import { HelpIconComponent } from '../../themes/default/themeComponents/helpIcon/help-icon.component'; import { StudentNodeService } from '../../services/studentNodeService'; +import { SubmitSurveyComponent } from '../submit-survey/submit-survey.component'; +import { MatDividerModule } from '@angular/material/divider'; @Component({ - imports: [ - CommonModule, - ComponentComponent, - ComponentStateInfoComponent, - FlexLayoutModule, - HelpIconComponent, - MatButtonModule - ], - selector: 'node', - styleUrl: './node.component.scss', - templateUrl: './node.component.html' + imports: [ + CommonModule, + ComponentComponent, + ComponentStateInfoComponent, + FlexLayoutModule, + HelpIconComponent, + MatButtonModule, + MatDividerModule, + SubmitSurveyComponent + ], + selector: 'node', + styleUrl: './node.component.scss', + templateUrl: './node.component.html' }) export class NodeComponent implements OnInit { private autoSaveInterval: number = 60000; // in milliseconds; @@ -41,7 +45,11 @@ export class NodeComponent implements OnInit { protected dirtyComponentIds: any = []; protected dirtySubmitComponentIds: any = []; protected disabled: boolean; + protected isBranchNode: boolean = false; + protected isLastNode: boolean = false; + protected isSurvey: boolean; protected latestComponentState: ComponentState; + protected nextNodeId: string; @Input() node: Node; protected nodeStatus: any; protected showRubric: boolean; @@ -84,6 +92,8 @@ export class NodeComponent implements OnInit { ngOnInit(): void { this.workgroupId = this.configService.getWorkgroupId(); this.disabled = !this.configService.isRunActive(); + this.setIsLastNode(); + this.isSurvey = this.configService.getConfigParam('isSurvey'); this.initializeNode(); this.startAutoSaveInterval(); @@ -151,6 +161,7 @@ export class NodeComponent implements OnInit { if (this.node.isEvaluateTransitionLogicOn('exitNode')) { this.nodeService.evaluateTransitionLogic(); } + this.setIsLastNode(); this.initializeNode(); }) ); @@ -436,6 +447,14 @@ export class NodeComponent implements OnInit { } } + private setIsLastNode(): void { + this.nextNodeId = null; + this.nodeService.getNextNodeId(this.node.id).then((nextId) => { + this.nextNodeId = nextId; + }); + this.isBranchNode = this.node.transitionLogic.transitions.length > 1; + } + protected getComponentStateByComponentId(componentId: string): any { return this.studentDataService.getLatestComponentStateByNodeIdAndComponentId( this.node.id, diff --git a/src/assets/wise5/vle/submit-survey/submit-survey.component.html b/src/assets/wise5/vle/submit-survey/submit-survey.component.html new file mode 100644 index 00000000000..270188bc650 --- /dev/null +++ b/src/assets/wise5/vle/submit-survey/submit-survey.component.html @@ -0,0 +1,6 @@ +

Thank you for taking the time to provide your responses!

+

When you have finished answering all the questions, press the button below to exit.

+ diff --git a/src/assets/wise5/vle/submit-survey/submit-survey.component.spec.ts b/src/assets/wise5/vle/submit-survey/submit-survey.component.spec.ts new file mode 100644 index 00000000000..b9264d2258b --- /dev/null +++ b/src/assets/wise5/vle/submit-survey/submit-survey.component.spec.ts @@ -0,0 +1,76 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SubmitSurveyComponent } from './submit-survey.component'; +import { ProjectService } from '../../services/projectService'; +import { NodeStatusService } from '../../services/nodeStatusService'; +import { MockProviders } from 'ng-mocks'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatButtonHarness } from '@angular/material/button/testing'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { SessionService } from '../../services/sessionService'; + +let fixture: ComponentFixture; +let loader: HarnessLoader; +let nodeStatusService: NodeStatusService; +let projectService: ProjectService; +describe('SubmitSurveyComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubmitSurveyComponent], + providers: [ + MockProviders(NodeStatusService, ProjectService, SessionService), + provideHttpClient(withInterceptorsFromDi()) + ] + }).compileComponents(); + projectService = TestBed.inject(ProjectService); + projectService.idToOrder = { + node1: '0' + }; + nodeStatusService = TestBed.inject(NodeStatusService); + fixture = TestBed.createComponent(SubmitSurveyComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + submitSurvey_UnitIncomplete_ShowIncompleteSubmitWarning(); + submitSurvey_UnitComplete_ShowGenericSubmitWarning(); +}); + +function submitSurvey_UnitIncomplete_ShowIncompleteSubmitWarning() { + describe('Unit is incomplete', () => { + beforeEach(() => { + spyOn(projectService, 'isApplicationNode').and.returnValue(true); + spyOn(nodeStatusService, 'getNodeStatusByNodeId').and.returnValue({ + isCompleted: false + }); + spyOn(projectService, 'getNodePositionById').and.returnValue('1.1'); + }); + + it('submitSurvey() should show incomplete message', async () => { + spyOn(window, 'confirm'); + await (await loader.getHarness(MatButtonHarness)).click(); + expect(window.confirm).toHaveBeenCalledWith( + 'You have not completed the following steps: 1.1\n\nAre you sure you want to submit your final responses?' + ); + }); + }); +} + +function submitSurvey_UnitComplete_ShowGenericSubmitWarning() { + describe('Unit is complete', () => { + beforeEach(() => { + spyOn(projectService, 'isApplicationNode').and.returnValue(true); + spyOn(nodeStatusService, 'getNodeStatusByNodeId').and.returnValue({ + isCompleted: true + }); + }); + + it('submitSurvey() should show generic message', async () => { + spyOn(window, 'confirm'); + await (await loader.getHarness(MatButtonHarness)).click(); + expect(window.confirm).toHaveBeenCalledWith( + 'Are you sure you want to submit your final responses?' + ); + }); + }); +} diff --git a/src/assets/wise5/vle/submit-survey/submit-survey.component.ts b/src/assets/wise5/vle/submit-survey/submit-survey.component.ts new file mode 100644 index 00000000000..f1d5728bdde --- /dev/null +++ b/src/assets/wise5/vle/submit-survey/submit-survey.component.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { ProjectService } from '../../services/projectService'; +import { NodeStatusService } from '../../services/nodeStatusService'; +import { SessionService } from '../../services/sessionService'; + +@Component({ + imports: [MatButtonModule, MatIconModule], + selector: 'submit-survey', + templateUrl: './submit-survey.component.html' +}) +export class SubmitSurveyComponent { + private genericSubmitWarning = $localize`Are you sure you want to submit your final responses?`; + + constructor( + private nodeStatusService: NodeStatusService, + private projectService: ProjectService, + private sessionService: SessionService + ) {} + + protected submitSurvey(): void { + const incompleteNodeIds: string[] = this.getIncompleteNodeIds(); + if ( + confirm( + incompleteNodeIds.length > 0 + ? this.getIncompleteUnitSubmitWarning(incompleteNodeIds) + : this.genericSubmitWarning + ) + ) { + this.logOut(); + } + } + + private getIncompleteNodeIds(): string[] { + return Object.keys(this.projectService.idToOrder).filter( + (nodeId) => + this.projectService.isApplicationNode(nodeId) && + !this.nodeStatusService.getNodeStatusByNodeId(nodeId).isCompleted + ); + } + + private getIncompleteUnitSubmitWarning(incompleteNodeIds: string[]): string { + const incompleteNodePositions = incompleteNodeIds + .map((nodeId) => this.projectService.getNodePositionById(nodeId)) + .reduce((acc, nodePos) => `${acc} ${nodePos},`, '') + .slice(0, -1); + return ( + $localize`You have not completed the following steps: ` + + `${incompleteNodePositions}\n\n${this.genericSubmitWarning}` + ); + } + + private logOut(): void { + this.sessionService + .logOutWithoutHomeRedirect() + .subscribe(() => (window.location.href = `/survey/completed`)); + } +} diff --git a/src/assets/wise5/vle/vle.component.spec.ts b/src/assets/wise5/vle/vle.component.spec.ts index fb6d7197d61..502f2d37fcd 100644 --- a/src/assets/wise5/vle/vle.component.spec.ts +++ b/src/assets/wise5/vle/vle.component.spec.ts @@ -11,6 +11,7 @@ import { PauseScreenService } from '../services/pauseScreenService'; import { StudentNotificationService } from '../services/studentNotificationService'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { StudentService } from '../../../app/student/student.service'; let component: VLEComponent; let fixture: ComponentFixture; @@ -26,6 +27,7 @@ describe('VLEComponent', () => { PauseScreenService, provideRouter([]), StudentNotificationService, + StudentService, VLEProjectService, provideHttpClient(withInterceptorsFromDi()) ] diff --git a/src/assets/wise5/vle/vle.component.ts b/src/assets/wise5/vle/vle.component.ts index 5ad5e4698c2..d247d0d699b 100644 --- a/src/assets/wise5/vle/vle.component.ts +++ b/src/assets/wise5/vle/vle.component.ts @@ -1,34 +1,35 @@ +import { ActivatedRoute, Router } from '@angular/router'; import { AfterViewInit, Component, HostListener, TemplateRef, ViewChild } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { Subscription } from 'rxjs'; +import { AnnotationService } from '../services/annotationService'; +import { CommonModule } from '@angular/common'; import { ConfigService } from '../services/configService'; +import { convertToPNGFile } from '../common/canvas/canvas'; +import { DialogWithConfirmComponent } from '../directives/dialog-with-confirm/dialog-with-confirm.component'; +import { GroupTabsComponent } from '../directives/group-tabs/group-tabs.component'; import { InitializeVLEService } from '../services/initializeVLEService'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { NavigationComponent } from '../themes/default/navigation/navigation.component'; +import { Node } from '../common/Node'; +import { NodeComponent } from './node/node.component'; +import { NodeNavigationComponent } from '../directives/node-navigation/node-navigation.component'; +import { NodeStatusService } from '../services/nodeStatusService'; +import { NotebookLauncherComponent } from '../../../app/notebook/notebook-launcher/notebook-launcher.component'; +import { NotebookNotesComponent } from '../../../app/notebook/notebook-notes/notebook-notes.component'; +import { NotebookReportComponent } from '../../../app/notebook/notebook-report/notebook-report.component'; import { NotebookService } from '../services/notebookService'; import { NotificationService } from '../services/notificationService'; +import { RunEndedAndLockedMessageComponent } from './run-ended-and-locked-message/run-ended-and-locked-message.component'; +import { SafeResourceUrl } from '@angular/platform-browser'; +import { SafeUrl } from '../directives/safeUrl/safe-url.pipe'; import { SessionService } from '../services/sessionService'; +import { StepToolsComponent } from '../themes/default/themeComponents/stepTools/step-tools.component'; import { StudentDataService } from '../services/studentDataService'; +import { StudentService } from '../../../app/student/student.service'; +import { Subscription } from 'rxjs'; +import { TopBarComponent } from '../../../app/student/top-bar/top-bar.component'; import { VLEProjectService } from './vleProjectService'; -import { DialogWithConfirmComponent } from '../directives/dialog-with-confirm/dialog-with-confirm.component'; -import { AnnotationService } from '../services/annotationService'; -import { ActivatedRoute, Router } from '@angular/router'; import { WiseLinkService } from '../../../app/services/wiseLinkService'; -import { convertToPNGFile } from '../common/canvas/canvas'; -import { NodeStatusService } from '../services/nodeStatusService'; -import { Node } from '../common/Node'; -import { SafeResourceUrl } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; -import { NotebookNotesComponent } from '../../../app/notebook/notebook-notes/notebook-notes.component'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { SafeUrl } from '../directives/safeUrl/safe-url.pipe'; -import { NodeNavigationComponent } from '../directives/node-navigation/node-navigation.component'; -import { GroupTabsComponent } from '../directives/group-tabs/group-tabs.component'; -import { TopBarComponent } from '../../../app/student/top-bar/top-bar.component'; -import { NotebookReportComponent } from '../../../app/notebook/notebook-report/notebook-report.component'; -import { NotebookLauncherComponent } from '../../../app/notebook/notebook-launcher/notebook-launcher.component'; -import { StepToolsComponent } from '../themes/default/themeComponents/stepTools/step-tools.component'; -import { RunEndedAndLockedMessageComponent } from './run-ended-and-locked-message/run-ended-and-locked-message.component'; -import { NodeComponent } from './node/node.component'; -import { NavigationComponent } from '../themes/default/navigation/navigation.component'; @Component({ imports: [ @@ -55,6 +56,7 @@ export class VLEComponent implements AfterViewInit { @ViewChild('defaultVLETemplate') private defaultVLETemplate: TemplateRef; @ViewChild('drawer') public drawer: any; protected initialized: boolean; + private isSurvey: boolean; protected layoutState: string; protected notebookConfig: any; protected notesEnabled: boolean = false; @@ -81,11 +83,16 @@ export class VLEComponent implements AfterViewInit { private router: Router, private sessionService: SessionService, private studentDataService: StudentDataService, + private studentService: StudentService, private wiseLinkService: WiseLinkService ) {} - @HostListener('window:beforeunload') - beforeUnload(): void { + @HostListener('window:beforeunload', ['$event']) + beforeUnload($event: BeforeUnloadEvent): void { + if (this.isSurvey) { + // prevents the browser from showing a confirmation dialog + $event.stopImmediatePropagation(); + } if (this.sessionService.isSessionActive()) { this.saveNodeExitedEvent(); } @@ -140,6 +147,15 @@ export class VLEComponent implements AfterViewInit { this.setLayoutState(); this.initializeSubscriptions(); this.saveNodeEnteredEvent(); + + // Set isSurvey + if (!this.configService.isPreview()) { + this.studentService + .getRunInfoById(this.studentDataService.getRunStatus().runId) + .subscribe((runInfo) => { + this.isSurvey = runInfo.isSurvey; + }); + } } ngOnDestroy() { diff --git a/src/messages.xlf b/src/messages.xlf index 634a6932992..7b8e6ff583b 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -299,7 +299,7 @@ src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 61,63 + 88,90 src/assets/wise5/authoringTool/components/component-info-dialog/component-info-dialog.component.html @@ -433,7 +433,7 @@ src/app/teacher/create-run-dialog/create-run-dialog.component.html - 81,85 + 109,113 src/app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html @@ -1148,7 +1148,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/modules/header/header-account-menu/header-account-menu.component.html - 80,83 + 81,85 src/assets/wise5/authoringTool/components/top-bar/top-bar.component.html @@ -2248,6 +2248,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/contact/contact-form/contact-form.component.html 141,145 + + src/app/student/survey/survey-completed/survey-completed.component.html + 19,23 + Submit for Review @@ -5480,6 +5484,14 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/register/register.component.html 12,17 + + src/app/student/survey/survey-completed/survey-completed.component.html + 12,16 + + + src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html + 12,16 + src/assets/wise5/authoringTool/components/top-bar/top-bar.component.html 10,14 @@ -5624,25 +5636,25 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Account avatar src/app/modules/header/header-account-menu/header-account-menu.component.html - 13,16 + 14,17 src/app/modules/header/header-account-menu/header-account-menu.component.html - 17,19 + 18,20 Switch back to original user src/app/modules/header/header-account-menu/header-account-menu.component.html - 22,26 + 23,27 Student Home src/app/modules/header/header-account-menu/header-account-menu.component.html - 33,37 + 34,38 src/app/student/student-home/student-home.component.html @@ -5653,7 +5665,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Teacher Home src/app/modules/header/header-account-menu/header-account-menu.component.html - 43,47 + 44,48 src/app/teacher/teacher-home/teacher-home.component.html @@ -5664,11 +5676,11 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Edit Profile src/app/modules/header/header-account-menu/header-account-menu.component.html - 53,57 + 54,58 src/app/modules/header/header-account-menu/header-account-menu.component.html - 63,65 + 64,66 src/app/student/account/edit/edit.component.html @@ -5683,21 +5695,21 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Researcher Tools src/app/modules/header/header-account-menu/header-account-menu.component.html - 67,69 + 68,70 Admin Tools src/app/modules/header/header-account-menu/header-account-menu.component.html - 71,76 + 72,76 Sign Out src/app/modules/header/header-account-menu/header-account-menu.component.html - 85,89 + 87,91 @@ -5762,7 +5774,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/create-run-dialog/create-run-dialog.component.html - 5,7 + 7,8 @@ -5962,7 +5974,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 83,86 + 89,92 @@ -6543,7 +6555,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/create-run-dialog/create-run-dialog.component.html - 121,124 + 171,174 src/app/teacher/list-classroom-courses-dialog/list-classroom-courses-dialog.component.html @@ -6551,7 +6563,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 118,120 + 124,126 src/app/teacher/share-run-dialog/share-run-dialog.component.html @@ -8217,7 +8229,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 115,116 + 121,122 @@ -8291,6 +8303,20 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.33,38 + + Your responses have been submitted. You can close this window. + + src/app/student/survey/survey-completed/survey-completed.component.html + 18,22 + + + + Sorry, this unit has reached the maximum number of submissions. + + src/app/student/survey/workgroup-limit-reached/workgroup-limit-reached.component.html + 18,22 + + Team Sign In @@ -8541,157 +8567,239 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.87 - - 1. Choose Periods + + 1. Choose Run Type src/app/teacher/create-run-dialog/create-run-dialog.component.html - 7,9 + 9,11 + + + + Default + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 12,13 + + + src/assets/wise5/classroomMonitor/dataExport/export-raw-data/export-raw-data.component.html + 15,16 + + + + Survey + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 15,17 + + + + Participants complete survey units anonymously without a WISE account. + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 20,23 + + + + 2. Choose Periods + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 28,30 Add your own periods src/app/teacher/create-run-dialog/create-run-dialog.component.html - 16,17 + 39,40 For "Period 9", just enter the number 9. Separate periods with commas (e.g. "Section 1, Section 2"). Manually named periods should be no more than 16 characters long. src/app/teacher/create-run-dialog/create-run-dialog.component.html - 20,24 + 43,47 - - 2. Choose Students Per Team + + 3. Choose Students Per Team src/app/teacher/create-run-dialog/create-run-dialog.component.html - 24,25 + 47,48 Only 1 student src/app/teacher/create-run-dialog/create-run-dialog.component.html - 26,27 + 49,50 1-3 students src/app/teacher/create-run-dialog/create-run-dialog.component.html - 27,29 + 50,52 - - 3. Set Schedule + + 4. Set Schedule src/app/teacher/create-run-dialog/create-run-dialog.component.html - 30,32 + 53,57 + + + + 2. Set Schedule + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 55,57 Start date src/app/teacher/create-run-dialog/create-run-dialog.component.html - 34,37 + 60,63 src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 69,72 + 73,76 Start date is required src/app/teacher/create-run-dialog/create-run-dialog.component.html - 44,48 + 70,74 End date src/app/teacher/create-run-dialog/create-run-dialog.component.html - 49,52 + 75,78 src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 85,88 + 89,92 - - Lock After End Date + + Lock After End Date src/app/teacher/create-run-dialog/create-run-dialog.component.html - 63,66 - - - src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 105,109 + 90,93 If the End Date has passed and the unit is locked, students will no longer be able to save new work src/app/teacher/create-run-dialog/create-run-dialog.component.html - 68,71 + 95,98 src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 109,112 + 114,117 Note: These dates can be changed at any time from your Class Schedule. Just select "Edit Settings" from the unit's dropdown menu. src/app/teacher/create-run-dialog/create-run-dialog.component.html - 76,80 + 104,108 Create Run src/app/teacher/create-run-dialog/create-run-dialog.component.html - 90,94 + 120,124 Success! This unit has been added to your Class Schedule. src/app/teacher/create-run-dialog/create-run-dialog.component.html - 97,99 + 127,129 Access Code: src/app/teacher/create-run-dialog/create-run-dialog.component.html - 99,100 + 130,131 src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 39,40 + 44,45 Important: Every classroom unit has a unique Access Code. Students use this code to register for a unit. Give the code to your students when they first sign up for a WISE account. If they already have WISE accounts, have them log in and then select "Add Unit" from the student home page. src/app/teacher/create-run-dialog/create-run-dialog.component.html - 101,107 + 132,138 You can always find the Access Code for each classroom unit in your Class Schedule. src/app/teacher/create-run-dialog/create-run-dialog.component.html - 107,111 + 138,141 + + + + Access Link: + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 142,145 + + + + Copy link + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 148,150 + + + src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html + 23,25 + + + src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html + 39,41 + + + + Important: Every survey unit has a unique Access Link. Participants can use this link to complete the unit without a WISE account. + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 158,161 + + + + You can always find the Access Link for each survey unit in your Class Schedule. + + src/app/teacher/create-run-dialog/create-run-dialog.component.html + 161,165 Share to Google Classroom src/app/teacher/create-run-dialog/create-run-dialog.component.html - 119,121 + 168,171 + + + + Copied to clipboard. + + src/app/teacher/create-run-dialog/create-run-dialog.component.ts + 272 + + + src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts + 42 @@ -8735,7 +8843,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 5,7 + 9,11 src/app/teacher/share-run-dialog/share-run-dialog.component.html @@ -8915,7 +9023,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 53,57 + 79,83 @@ -9124,196 +9232,203 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Class Periods src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 6,8 + 7,9 Delete period src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 15,20 + 16,20 Add new period src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 23,26 + 24,27 Add period src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 39,43 + 40,44 For "Period 9", just enter the number 9. src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 46,50 + 47,51 Students Per Team src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 50,51 + 51,52 Only 1 student src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 53,55 + 54,56 1-3 students src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 56,59 + 57,60 Schedule src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 61,62 + 63,65 (Last student login: ) src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 63,65 + 66,68 Start date is required. src/app/teacher/run-settings-dialog/run-settings-dialog.component.html - 79,80 + 83,84 + + + + Lock After End Date + + src/app/teacher/run-settings-dialog/run-settings-dialog.component.html + 110,113 There is already a period with that name. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 60 + 62 You do not have permission to add periods to this unit. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 61 + 63 You are not allowed to delete a period that contains students. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 62 + 64 You do not have permission to delete periods from this unit. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 63 + 65 You do not have permission to change the number of students per team for this unit. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 64 + 66 You are not allowed to decrease the number of students per team because this unit already has teams with more than 1 student. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 65 + 67 You do not have permission to change the dates for this unit. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 66 + 68 End date can't be before start date. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 67 + 69 Start date can't be after end date. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 68 + 70 You do not have permission to change whether unit is locked after end date. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 69 + 71 Please enter a new period name. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 89 + 91 1-3 src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 130 + 132 Are you sure you want to change the students per team to ? src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 134 + 136 Are you sure you want to change the start date to ? src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 165 + 167 Are you sure you want to change the end date to ? src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 221 + 223 Are you sure you want to remove the end date? src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 223 + 225 Unit settings updated. src/app/teacher/run-settings-dialog/run-settings-dialog.component.ts - 306 + 308 @@ -9324,7 +9439,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 63,65 + 69,71 @@ -9427,53 +9542,60 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.6,9 + + Share with Participants + + src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html + 2,6 + + Share with Students src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 1,4 + 4,8 - - Copy this link to share with your students: + + This is a survey unit. Participants complete survey units anonymously and do not need a WISE account. src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 7,11 + 13,16 - - Copy link + + Copy this link to share with participants: src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 13,16 + 16,19 + + + + Copy this link to share with your students: + + src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html + 33,37 Students with WISE accounts can also select Add Unit+ and type the Access Code: src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 23,26 + 49,52 Copy Access Code src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 31,33 + 57,59 Add as an assignment in Google Classroom: src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html - 43,47 - - - - Copied to clipboard. - - src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.ts - 36 + 69,73 @@ -9654,39 +9776,53 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.33,34 + + Survey Unit + + src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html + 40,41 + + + + Share with participants + + src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html + 41,44 + + Share with students src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 44,48 + 49,54 Shared by src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 48,50 + 54,56 (Legacy Unit) src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 86,89 + 92,95 Last student login: src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 112,114 + 118,120 Teacher Tools src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html - 124,128 + 130,134 @@ -10367,7 +10503,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/vle/vle.component.ts - 195 + 211 @@ -10382,7 +10518,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/assets/wise5/vle/vle.component.ts - 196 + 212 @@ -15119,13 +15255,6 @@ Are you sure you want to proceed? 33,38 - - Default - - src/assets/wise5/classroomMonitor/dataExport/export-raw-data/export-raw-data.component.html - 15,16 - - Everything @@ -22407,6 +22536,41 @@ If this problem continues, let your teacher know and move on to the next activit 27,29 + + Thank you for taking the time to provide your responses! + + src/assets/wise5/vle/submit-survey/submit-survey.component.html + 1,2 + + + + When you have finished answering all the questions, press the button below to exit. + + src/assets/wise5/vle/submit-survey/submit-survey.component.html + 2,3 + + + + Complete Survey + + src/assets/wise5/vle/submit-survey/submit-survey.component.html + 5,7 + + + + Are you sure you want to submit your final responses? + + src/assets/wise5/vle/submit-survey/submit-survey.component.ts + 14 + + + + You have not completed the following steps: + + src/assets/wise5/vle/submit-survey/submit-survey.component.ts + 49 + +