Merge pull request #1634 from 4Science/CST-5738

Add special groups list to the profile page
This commit is contained in:
Tim Donohue
2022-05-19 15:24:49 -05:00
committed by GitHub
10 changed files with 175 additions and 16 deletions

View File

@@ -12,13 +12,13 @@ import { AuthStatus } from './models/auth-status.model';
import { ShortLivedToken } from './models/short-lived-token.model';
import { URLCombiner } from '../url-combiner/url-combiner';
import { RestRequest } from '../data/rest-request.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/**
* Abstract service to send authentication requests
*/
export abstract class AuthRequestService {
protected linkName = 'authn';
protected browseEndpoint = '';
protected shortlivedtokensEndpoint = 'shortlivedtokens';
constructor(protected halService: HALEndpointService,
@@ -27,14 +27,21 @@ export abstract class AuthRequestService {
) {
}
protected fetchRequest(request: RestRequest): Observable<RemoteData<AuthStatus>> {
return this.rdbService.buildFromRequestUUID<AuthStatus>(request.uuid).pipe(
protected fetchRequest(request: RestRequest, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): Observable<RemoteData<AuthStatus>> {
return this.rdbService.buildFromRequestUUID<AuthStatus>(request.uuid, ...linksToFollow).pipe(
getFirstCompletedRemoteData(),
);
}
protected getEndpointByMethod(endpoint: string, method: string): string {
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
protected getEndpointByMethod(endpoint: string, method: string, ...linksToFollow: FollowLinkConfig<AuthStatus>[]): string {
let url = isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
if (linksToFollow?.length > 0) {
linksToFollow.forEach((link: FollowLinkConfig<AuthStatus>, index: number) => {
url += ((index === 0) ? '?' : '&') + `embed=${link.name}`;
});
}
return url;
}
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
@@ -48,14 +55,14 @@ export abstract class AuthRequestService {
distinctUntilChanged());
}
public getRequest(method: string, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
return this.halService.getEndpoint(this.linkName).pipe(
filter((href: string) => isNotEmpty(href)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
distinctUntilChanged(),
map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)),
tap((request: GetRequest) => this.requestService.send(request)),
mergeMap((request: GetRequest) => this.fetchRequest(request)),
mergeMap((request: GetRequest) => this.fetchRequest(request, ...linksToFollow)),
distinctUntilChanged());
}

View File

@@ -32,6 +32,8 @@ import { TranslateService } from '@ngx-translate/core';
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions';
import { SpecialGroupDataMock, SpecialGroupDataMock$ } from '../../shared/testing/special-group.mock';
import { cold } from 'jasmine-marbles';
describe('AuthService test', () => {
@@ -56,6 +58,13 @@ describe('AuthService test', () => {
let linkService;
let hardRedirectService;
const AuthStatusWithSpecialGroups = Object.assign(new AuthStatus(), {
uuid: 'test',
authenticated: true,
okay: true,
specialGroups: SpecialGroupDataMock$
});
function init() {
mockStore = jasmine.createSpyObj('store', {
dispatch: {},
@@ -511,6 +520,19 @@ describe('AuthService test', () => {
expect((authService as any).navigateToRedirectUrl).toHaveBeenCalled();
});
});
describe('getSpecialGroupsFromAuthStatus', () => {
beforeEach(() => {
spyOn(authRequest, 'getRequest').and.returnValue(createSuccessfulRemoteDataObject$(AuthStatusWithSpecialGroups));
});
it('should call navigateToRedirectUrl with no url', () => {
const expectRes = cold('(a|)', {
a: SpecialGroupDataMock
});
expect(authService.getSpecialGroupsFromAuthStatus()).toBeObservable(expectRes);
});
});
});
describe('when user is not logged in', () => {

View File

@@ -44,13 +44,18 @@ import {
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { RouteService } from '../services/route.service';
import { EPersonDataService } from '../eperson/eperson-data.service';
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators';
import { AuthMethod } from './models/auth.method';
import { HardRedirectService } from '../services/hard-redirect.service';
import { RemoteData } from '../data/remote-data';
import { environment } from '../../../environments/environment';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model';
import { Group } from '../eperson/models/group.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PageInfo } from '../shared/page-info.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout';
@@ -211,6 +216,22 @@ export class AuthService {
this.store.dispatch(new CheckAuthenticationTokenAction());
}
/**
* Return the special groups list embedded in the AuthStatus model
*/
public getSpecialGroupsFromAuthStatus(): Observable<RemoteData<PaginatedList<Group>>> {
return this.authRequestService.getRequest('status', null, followLink('specialGroups')).pipe(
getFirstCompletedRemoteData(),
switchMap((status: RemoteData<AuthStatus>) => {
if (status.hasSucceeded) {
return status.payload.specialGroups;
} else {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(),[]));
}
})
);
}
/**
* Checks if token is present into storage and is not expired
*/

View File

@@ -5,6 +5,8 @@ import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
import { RemoteData } from '../../data/remote-data';
import { EPerson } from '../../eperson/models/eperson.model';
import { EPERSON } from '../../eperson/models/eperson.resource-type';
import { Group } from '../../eperson/models/group.model';
import { GROUP } from '../../eperson/models/group.resource-type';
import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
@@ -13,6 +15,7 @@ import { AUTH_STATUS } from './auth-status.resource-type';
import { AuthTokenInfo } from './auth-token-info.model';
import { AuthMethod } from './auth.method';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { PaginatedList } from '../../data/paginated-list.model';
/**
* Object that represents the authenticated status of a user
@@ -61,6 +64,7 @@ export class AuthStatus implements CacheableObject {
_links: {
self: HALLink;
eperson: HALLink;
specialGroups: HALLink;
};
/**
@@ -70,6 +74,13 @@ export class AuthStatus implements CacheableObject {
@link(EPERSON)
eperson?: Observable<RemoteData<EPerson>>;
/**
* The SpecialGroup of this auth status
* Will be undefined unless the SpecialGroup {@link HALLink} has been resolved.
*/
@link(GROUP, true)
specialGroups?: Observable<RemoteData<PaginatedList<Group>>>;
/**
* True if the token is valid, false if there was no token or the token wasn't valid
*/

View File

@@ -22,12 +22,21 @@
</div>
<ng-container *ngVar="(groupsRD$ | async)?.payload?.page as groups">
<div *ngIf="groups">
<div *ngIf="groups?.length > 0">
<h3 class="mt-4">{{'profile.groups.head' | translate}}</h3>
<ul class="list-group list-group-flush">
<li *ngFor="let group of groups" class="list-group-item">{{group.name}}</li>
</ul>
</div>
</ng-container>
<ng-container *ngVar="(specialGroupsRD$ | async)?.payload?.page as specialGroups">
<div *ngIf="specialGroups?.length > 0" data-test="specialGroups">
<h3 class="mt-4">{{'profile.special.groups.head' | translate}}</h3>
<ul class="list-group list-group-flush">
<li *ngFor="let specialGroup of specialGroups" class="list-group-item">{{specialGroup.name}}</li>
</ul>
</div>
</ng-container>
</div>
</ng-container>

View File

@@ -20,6 +20,7 @@ import { provideMockStore } from '@ngrx/store/testing';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { getTestScheduler } from 'jasmine-marbles';
import { By } from '@angular/platform-browser';
import { EmptySpecialGroupDataMock$, SpecialGroupDataMock$ } from '../shared/testing/special-group.mock';
describe('ProfilePageComponent', () => {
let component: ProfilePageComponent;
@@ -54,7 +55,8 @@ describe('ProfilePageComponent', () => {
};
authService = jasmine.createSpyObj('authService', {
getAuthenticatedUserFromStore: observableOf(user)
getAuthenticatedUserFromStore: observableOf(user),
getSpecialGroupsFromAuthStatus: SpecialGroupDataMock$
});
epersonService = jasmine.createSpyObj('epersonService', {
findById: createSuccessfulRemoteDataObject$(user),
@@ -235,4 +237,25 @@ describe('ProfilePageComponent', () => {
});
});
});
describe('check for specialGroups', () => {
it('should contains specialGroups list', () => {
const specialGroupsEle = fixture.debugElement.query(By.css('[data-test="specialGroups"]'));
expect(specialGroupsEle).toBeTruthy();
});
it('should not contains specialGroups list', () => {
component.specialGroupsRD$ = null;
fixture.detectChanges();
const specialGroupsEle = fixture.debugElement.query(By.css('[data-test="specialGroups"]'));
expect(specialGroupsEle).toBeFalsy();
});
it('should not contains specialGroups list', () => {
component.specialGroupsRD$ = EmptySpecialGroupDataMock$;
fixture.detectChanges();
const specialGroupsEle = fixture.debugElement.query(By.css('[data-test="specialGroups"]'));
expect(specialGroupsEle).toBeFalsy();
});
});
});

View File

@@ -9,11 +9,7 @@ import { RemoteData } from '../core/data/remote-data';
import { PaginatedList } from '../core/data/paginated-list.model';
import { filter, switchMap, tap } from 'rxjs/operators';
import { EPersonDataService } from '../core/eperson/eperson-data.service';
import {
getAllSucceededRemoteData,
getRemoteDataPayload,
getFirstCompletedRemoteData
} from '../core/shared/operators';
import { getAllSucceededRemoteData, getFirstCompletedRemoteData, getRemoteDataPayload } from '../core/shared/operators';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
import { AuthService } from '../core/auth/auth.service';
@@ -45,6 +41,11 @@ export class ProfilePageComponent implements OnInit {
*/
groupsRD$: Observable<RemoteData<PaginatedList<Group>>>;
/**
* The special groups the user belongs to
*/
specialGroupsRD$: Observable<RemoteData<PaginatedList<Group>>>;
/**
* Prefix for the notification messages of this component
*/
@@ -88,6 +89,7 @@ export class ProfilePageComponent implements OnInit {
);
this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups));
this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href)));
this.specialGroupsRD$ = this.authService.getSpecialGroupsFromAuthStatus();
}
/**

View File

@@ -34,6 +34,9 @@ export class AuthRequestServiceStub {
},
eperson: {
href: this.mockUser._links.self.href
},
specialGroups: {
href: this.mockUser._links.self.href
}
};
} else {
@@ -62,6 +65,9 @@ export class AuthRequestServiceStub {
},
eperson: {
href: this.mockUser._links.self.href
},
specialGroups: {
href: this.mockUser._links.self.href
}
};
} else {

View File

@@ -0,0 +1,56 @@
import { Observable } from 'rxjs';
import { EPersonMock } from './eperson.mock';
import { PageInfo } from '../../core/shared/page-info.model';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { Group } from '../../core/eperson/models/group.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { RemoteData } from '../../core/data/remote-data';
export const SpecialGroupMock2: Group = Object.assign(new Group(), {
handle: null,
subgroups: [],
epersons: [],
permanent: true,
selfRegistered: false,
_links: {
self: {
href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2',
},
subgroups: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/subgroups' },
object: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/object' },
epersons: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/epersons' }
},
_name: 'testgroupname2',
id: 'testgroupid2',
uuid: 'testgroupid2',
type: 'specialGroups',
// object: createSuccessfulRemoteDataObject$({ name: 'testspecialGroupsid2objectName'})
});
export const SpecialGroupMock: Group = Object.assign(new Group(), {
handle: null,
subgroups: [SpecialGroupMock2],
epersons: [EPersonMock],
selfRegistered: false,
permanent: false,
_links: {
self: {
href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid',
},
subgroups: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid/subgroups' },
object: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/object' },
epersons: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid/epersons' }
},
_name: 'testgroupname',
id: 'testgroupid',
uuid: 'testgroupid',
type: 'specialGroups',
});
export const SpecialGroupDataMock: RemoteData<PaginatedList<Group>> = createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [SpecialGroupMock2,SpecialGroupMock]));
export const SpecialGroupDataMock$: Observable<RemoteData<PaginatedList<Group>>> = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [SpecialGroupMock2,SpecialGroupMock]));
export const EmptySpecialGroupDataMock$: Observable<RemoteData<PaginatedList<Group>>> = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));

View File

@@ -2886,6 +2886,8 @@
"profile.groups.head": "Authorization groups you belong to",
"profile.special.groups.head": "Authorization special groups you belong to",
"profile.head": "Update Profile",
"profile.metadata.form.error.firstname.required": "First Name is required",