mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-10-21 00:41:44 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3547ad19f6 | ||
|
|
d5fbd8654c | ||
|
|
c9ada93f87 | ||
|
|
6d782ecb10 | ||
|
|
1cc3ffedf8 | ||
|
|
05a0525320 | ||
|
|
3c08704345 |
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {BehaviorSubject, of} from 'rxjs';
|
||||||
|
import {SCAssessment} from '@openstapps/core';
|
||||||
|
import {AssessmentsProvider} from './assessments.provider';
|
||||||
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
|
import {AuthActions, AuthProvider, IAuthAction} from '../auth/auth.provider';
|
||||||
|
|
||||||
|
class FakeAuth {
|
||||||
|
events$ = new BehaviorSubject<IAuthAction>({action: AuthActions.Init});
|
||||||
|
|
||||||
|
accessToken = 'token-a';
|
||||||
|
|
||||||
|
getValidToken() {
|
||||||
|
return Promise.resolve({accessToken: this.accessToken});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradesA = [{uid: 'a', name: 'Grade of A'}] as unknown as SCAssessment[];
|
||||||
|
const gradesB = [{uid: 'b', name: 'Grade of B'}] as unknown as SCAssessment[];
|
||||||
|
|
||||||
|
describe('AssessmentsProvider', () => {
|
||||||
|
let provider: AssessmentsProvider;
|
||||||
|
let fakeAuth: FakeAuth;
|
||||||
|
let httpClient: jasmine.SpyObj<HttpClient>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fakeAuth = new FakeAuth();
|
||||||
|
httpClient = jasmine.createSpyObj<HttpClient>('HttpClient', ['get']);
|
||||||
|
httpClient.get.and.callFake(((_url: string, options: {headers: {Authorization: string}}) =>
|
||||||
|
of({data: options.headers.Authorization === 'Bearer token-a' ? gradesA : gradesB})) as never);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
AssessmentsProvider,
|
||||||
|
{
|
||||||
|
provide: ConfigProvider,
|
||||||
|
useValue: {config: {app: {features: {extern: {hisometry: {url: 'http://hisometry.local'}}}}}},
|
||||||
|
},
|
||||||
|
{provide: AuthProvider, useValue: fakeAuth},
|
||||||
|
{provide: HttpClient, useValue: httpClient},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
provider = TestBed.inject(AssessmentsProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the cache while the same user is logged in', async () => {
|
||||||
|
await provider.getAssessments();
|
||||||
|
fakeAuth.events$.next({action: AuthActions.RefreshSuccess});
|
||||||
|
|
||||||
|
expect(await provider.getAssessments()).toEqual(gradesA);
|
||||||
|
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const action of [AuthActions.SignOutSuccess, AuthActions.SessionExpired]) {
|
||||||
|
it(`should clear the cache on ${action}`, async () => {
|
||||||
|
await provider.getAssessments();
|
||||||
|
fakeAuth.events$.next({action});
|
||||||
|
|
||||||
|
expect(provider.cache).toBeUndefined();
|
||||||
|
expect(await provider.assessments).toEqual({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should not show the grades of the previous user after a user change', async () => {
|
||||||
|
expect(await provider.getAssessments()).toEqual(gradesA);
|
||||||
|
|
||||||
|
fakeAuth.events$.next({action: AuthActions.SignOutSuccess});
|
||||||
|
fakeAuth.accessToken = 'token-b';
|
||||||
|
fakeAuth.events$.next({action: AuthActions.SignInSuccess});
|
||||||
|
|
||||||
|
expect(await provider.getAssessments()).toEqual(gradesB);
|
||||||
|
expect(httpClient.get).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,7 +17,7 @@ import {ConfigProvider} from '../config/config.provider';
|
|||||||
import {SCAssessment, SCUuid} from '@openstapps/core';
|
import {SCAssessment, SCUuid} from '@openstapps/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {uniqBy, keyBy} from '@openstapps/collection-utils';
|
import {uniqBy, keyBy} from '@openstapps/collection-utils';
|
||||||
import {AuthProvider} from '../auth/auth.provider';
|
import {AuthActions, AuthProvider} from '../auth/auth.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -57,6 +57,24 @@ export class AssessmentsProvider {
|
|||||||
|
|
||||||
readonly http = inject(HttpClient);
|
readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
/*
|
||||||
|
* The grades belong to the logged-in user. The next user
|
||||||
|
* should never get the previous user's grades from the cache.
|
||||||
|
*/
|
||||||
|
this.authProvider.events$.subscribe(({action}) => {
|
||||||
|
switch (action) {
|
||||||
|
case AuthActions.SignInSuccess:
|
||||||
|
case AuthActions.SignOutSuccess:
|
||||||
|
case AuthActions.SignOutFailed:
|
||||||
|
case AuthActions.SessionExpired: {
|
||||||
|
this.clearCache();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
assessmentPath = 'assessments';
|
assessmentPath = 'assessments';
|
||||||
|
|
||||||
// usually this wouldn't be necessary, but the assessment service
|
// usually this wouldn't be necessary, but the assessment service
|
||||||
@@ -110,4 +128,13 @@ export class AssessmentsProvider {
|
|||||||
|
|
||||||
return this.cache;
|
return this.cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all cached grades.
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache = undefined;
|
||||||
|
this.assessments = Promise.resolve({});
|
||||||
|
this.cacheTimestamp = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {DataProvider, DataScope} from '../../data/data.provider';
|
|||||||
import {DataRoutingService} from '../../data/data-routing.service';
|
import {DataRoutingService} from '../../data/data-routing.service';
|
||||||
import {groupBy, mapValues} from '@openstapps/collection-utils';
|
import {groupBy, mapValues} from '@openstapps/collection-utils';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {environment} from '../../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-assessments-page',
|
selector: 'app-assessments-page',
|
||||||
@@ -62,6 +63,26 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
destroy$ = inject(DestroyRef);
|
destroy$ = inject(DestroyRef);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ?token=... (e.g. ?token=mock) is a development aid only.
|
||||||
|
* In production, no token is read from or passed on in URLs.
|
||||||
|
*/
|
||||||
|
private readonly urlTokenAllowed = !environment.production;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token from the URL (?token=...), development only.
|
||||||
|
* Always undefined in production, so no token is read from or passed on in URLs.
|
||||||
|
*/
|
||||||
|
get urlToken(): string | undefined {
|
||||||
|
if (!this.urlTokenAllowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.activatedRoute.snapshot.queryParamMap.get('token');
|
||||||
|
|
||||||
|
return token === null ? undefined : token;
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.segmentView.value = this.sharedAxisChoreographer.currentValue;
|
this.segmentView.value = this.sharedAxisChoreographer.currentValue;
|
||||||
}
|
}
|
||||||
@@ -73,7 +94,7 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
|
|||||||
.subscribe(thing => {
|
.subscribe(thing => {
|
||||||
void this.router.navigate(['assessments', 'detail', thing.uid], {
|
void this.router.navigate(['assessments', 'detail', thing.uid], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
|
token: this.urlToken,
|
||||||
},
|
},
|
||||||
state: {item: thing},
|
state: {item: thing},
|
||||||
});
|
});
|
||||||
@@ -82,7 +103,7 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
|
|||||||
this.activatedRoute.queryParams.subscribe(parameters => {
|
this.activatedRoute.queryParams.subscribe(parameters => {
|
||||||
try {
|
try {
|
||||||
this.assessments = this.assessmentsProvider
|
this.assessments = this.assessmentsProvider
|
||||||
.getAssessments(parameters.token)
|
.getAssessments(this.urlTokenAllowed ? parameters.token : undefined)
|
||||||
.then(assessments => groupBy(assessments, it => it.courseOfStudy?.uid ?? 'unknown'))
|
.then(assessments => groupBy(assessments, it => it.courseOfStudy?.uid ?? 'unknown'))
|
||||||
.then(it => {
|
.then(it => {
|
||||||
this.assessmentKeys = Object.keys(it);
|
this.assessmentKeys = Object.keys(it);
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
[displayValue]="setting | settingValueTranslate | titlecase"
|
[displayValue]="setting | settingValueTranslate | titlecase"
|
||||||
[value]="setting"
|
[value]="setting"
|
||||||
[active]="!!filtersMap.get($any(setting.name))"
|
[active]="!!filtersMap.get($any(setting.name))"
|
||||||
(toggle)="stateChanged($any($event))"
|
(toggled)="stateChanged($any($event))"
|
||||||
>
|
>
|
||||||
</stapps-chip-filter>
|
</stapps-chip-filter>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import {ChangeDetectionStrategy, Component, ElementRef, Input, inject} from '@angular/core';
|
import {ChangeDetectionStrategy, Component, ElementRef, Input, inject} from '@angular/core';
|
||||||
import {SCIdCard} from '@openstapps/core';
|
import {SCIdCard} from '@openstapps/core';
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {AsyncPipe, TitleCasePipe} from '@angular/common';
|
|
||||||
import {IntervalIsNowPipe, ToDateIntervalPipe} from '../../util/in-range.pipe';
|
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
|
||||||
import {AnimationController, ModalController} from '@ionic/angular/standalone';
|
import {AnimationController, ModalController} from '@ionic/angular/standalone';
|
||||||
import {ScreenBrightness} from '@capacitor-community/screen-brightness';
|
import {ScreenBrightness} from '@capacitor-community/screen-brightness';
|
||||||
import {ScreenOrientation} from '@capacitor/screen-orientation';
|
import {ScreenOrientation} from '@capacitor/screen-orientation';
|
||||||
@@ -15,14 +12,7 @@ import {iosDuration, iosEasing, mdDuration, mdEasing} from 'src/app/animation/ea
|
|||||||
templateUrl: 'id-card.html',
|
templateUrl: 'id-card.html',
|
||||||
styleUrls: ['id-card.scss'],
|
styleUrls: ['id-card.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [ThingTranslateModule],
|
||||||
ThingTranslateModule,
|
|
||||||
IntervalIsNowPipe,
|
|
||||||
ToDateIntervalPipe,
|
|
||||||
AsyncPipe,
|
|
||||||
TranslateModule,
|
|
||||||
TitleCasePipe,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class IdCardComponent {
|
export class IdCardComponent {
|
||||||
private modalController = inject(ModalController);
|
private modalController = inject(ModalController);
|
||||||
|
|||||||
@@ -4,6 +4,3 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
(click)="presentFullscreen()"
|
(click)="presentFullscreen()"
|
||||||
/>
|
/>
|
||||||
@if (item.validity && (item.validity | rangeToDateInterval | dfnsIntervalIsNow | async) === false) {
|
|
||||||
<div class="expired">{{ 'profile.userInfo.expired' | translate | titlecase }}</div>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,24 +6,3 @@
|
|||||||
img {
|
img {
|
||||||
border-radius: 3mm;
|
border-radius: 3mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expired {
|
|
||||||
position: absolute;
|
|
||||||
top: 48px;
|
|
||||||
left: 48px;
|
|
||||||
transform-origin: center;
|
|
||||||
translate: -50% -50%;
|
|
||||||
rotate: -45deg;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 256px;
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--ion-color-danger-contrast);
|
|
||||||
|
|
||||||
background: var(--ion-color-danger);
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -405,7 +405,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userInfo": {
|
"userInfo": {
|
||||||
"expired": "Abgelaufen",
|
|
||||||
"studentId": "Matrikelnr.",
|
"studentId": "Matrikelnr.",
|
||||||
"username": "Nutzername",
|
"username": "Nutzername",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -405,7 +405,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userInfo": {
|
"userInfo": {
|
||||||
"expired": "expired",
|
|
||||||
"studentId": "Matriculation Nr.",
|
"studentId": "Matriculation Nr.",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const environment = {
|
|||||||
backend_url: 'https://mobile.server.uni-frankfurt.de',
|
backend_url: 'https://mobile.server.uni-frankfurt.de',
|
||||||
app_host: 'mobile.app.uni-frankfurt.de',
|
app_host: 'mobile.app.uni-frankfurt.de',
|
||||||
custom_url_scheme: 'de.anyschool.app',
|
custom_url_scheme: 'de.anyschool.app',
|
||||||
backend_version: '4.0.0',
|
backend_version: '4.1.0',
|
||||||
production: true,
|
production: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {defineConfig} from 'tsup';
|
import {defineConfig} from 'tsup';
|
||||||
import packageJson from './package.json' assert {type: 'json'};
|
import packageJson from './package.json' with {type: 'json'};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: ['src/index.ts'],
|
entry: ['src/index.ts'],
|
||||||
|
|||||||
Reference in New Issue
Block a user