mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #1479 from 4Science/CST-4981-statistics-are-always-accessible-even-if-are-restricted-to-administrator
Fix statistics page in order to show them only when user has authorizations
This commit is contained in:
@@ -30,7 +30,7 @@
|
|||||||
"clean:json": "rimraf *.records.json",
|
"clean:json": "rimraf *.records.json",
|
||||||
"clean:node": "rimraf node_modules",
|
"clean:node": "rimraf node_modules",
|
||||||
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
||||||
"clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:dev:config",
|
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node",
|
||||||
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||||
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||||
|
@@ -18,6 +18,8 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import createSpy = jasmine.createSpy;
|
import createSpy = jasmine.createSpy;
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
describe('AdminSidebarComponent', () => {
|
describe('AdminSidebarComponent', () => {
|
||||||
let comp: AdminSidebarComponent;
|
let comp: AdminSidebarComponent;
|
||||||
@@ -26,6 +28,28 @@ describe('AdminSidebarComponent', () => {
|
|||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let scriptService;
|
let scriptService;
|
||||||
|
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/items/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockItem)
|
||||||
|
}),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthorized: observableOf(true)
|
isAuthorized: observableOf(true)
|
||||||
@@ -42,6 +66,7 @@ describe('AdminSidebarComponent', () => {
|
|||||||
{ provide: ActivatedRoute, useValue: {} },
|
{ provide: ActivatedRoute, useValue: {} },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{ provide: ScriptDataService, useValue: scriptService },
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{
|
{
|
||||||
provide: NgbModal, useValue: {
|
provide: NgbModal, useValue: {
|
||||||
open: () => {/*comment*/
|
open: () => {/*comment*/
|
||||||
@@ -229,19 +254,19 @@ describe('AdminSidebarComponent', () => {
|
|||||||
|
|
||||||
it('should contain site admin section', () => {
|
it('should contain site admin section', () => {
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
id: 'admin_search', visible: true,
|
id: 'admin_search', visible: true,
|
||||||
}));
|
}));
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
id: 'registries', visible: true,
|
id: 'registries', visible: true,
|
||||||
}));
|
}));
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
parentID: 'registries', visible: true,
|
parentID: 'registries', visible: true,
|
||||||
}));
|
}));
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
id: 'curation_tasks', visible: true,
|
id: 'curation_tasks', visible: true,
|
||||||
}));
|
}));
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
id: 'workflow', visible: true,
|
id: 'workflow', visible: true,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -259,7 +284,7 @@ describe('AdminSidebarComponent', () => {
|
|||||||
|
|
||||||
it('should show edit_community', () => {
|
it('should show edit_community', () => {
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
id: 'edit_community', visible: true,
|
id: 'edit_community', visible: true,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -277,7 +302,7 @@ describe('AdminSidebarComponent', () => {
|
|||||||
|
|
||||||
it('should show edit_collection', () => {
|
it('should show edit_collection', () => {
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
id: 'edit_collection', visible: true,
|
id: 'edit_collection', visible: true,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -295,10 +320,10 @@ describe('AdminSidebarComponent', () => {
|
|||||||
|
|
||||||
it('should show access control section', () => {
|
it('should show access control section', () => {
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
id: 'access_control', visible: true,
|
id: 'access_control', visible: true,
|
||||||
}));
|
}));
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||||
parentID: 'access_control', visible: true,
|
parentID: 'access_control', visible: true,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -21,6 +21,7 @@ import { MenuService } from '../../shared/menu/menu.service';
|
|||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the admin sidebar
|
* Component representing the admin sidebar
|
||||||
@@ -63,14 +64,15 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
inFocus$: BehaviorSubject<boolean>;
|
inFocus$: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
constructor(protected menuService: MenuService,
|
constructor(protected menuService: MenuService,
|
||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
private variableService: CSSVariableService,
|
private variableService: CSSVariableService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private authorizationService: AuthorizationDataService,
|
public authorizationService: AuthorizationDataService,
|
||||||
private scriptDataService: ScriptDataService,
|
private scriptDataService: ScriptDataService,
|
||||||
|
public route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
super(menuService, injector);
|
super(menuService, injector, authorizationService, route);
|
||||||
this.inFocus$ = new BehaviorSubject(false);
|
this.inFocus$ = new BehaviorSubject(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +146,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
type: MenuItemType.TEXT,
|
type: MenuItemType.TEXT,
|
||||||
text: 'menu.section.new'
|
text: 'menu.section.new'
|
||||||
} as TextMenuItemModel,
|
} as TextMenuItemModel,
|
||||||
icon: 'plus',
|
icon: 'plus',
|
||||||
index: 0
|
index: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -103,20 +103,20 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig);
|
const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig);
|
||||||
|
|
||||||
this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe(
|
this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe(
|
||||||
switchMap(([currentPagination, currentSort ]) => this.collectionRD$.pipe(
|
switchMap(([currentPagination, currentSort]) => this.collectionRD$.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
map((rd) => rd.payload.id),
|
map((rd) => rd.payload.id),
|
||||||
switchMap((id: string) => {
|
switchMap((id: string) => {
|
||||||
return this.searchService.search(
|
return this.searchService.search(
|
||||||
new PaginatedSearchOptions({
|
new PaginatedSearchOptions({
|
||||||
scope: id,
|
scope: id,
|
||||||
pagination: currentPagination,
|
pagination: currentPagination,
|
||||||
sort: currentSort,
|
sort: currentSort,
|
||||||
dsoTypes: [DSpaceObjectType.ITEM]
|
dsoTypes: [DSpaceObjectType.ITEM]
|
||||||
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
||||||
}),
|
}),
|
||||||
startWith(undefined) // Make sure switching pages shows loading component
|
startWith(undefined) // Make sure switching pages shows loading component
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
||||||
|
* management rights
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||||
|
super(authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check group management rights
|
||||||
|
*/
|
||||||
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.CanViewUsageStatistics);
|
||||||
|
}
|
||||||
|
}
|
@@ -26,4 +26,5 @@ export enum FeatureID {
|
|||||||
CanEditVersion = 'canEditVersion',
|
CanEditVersion = 'canEditVersion',
|
||||||
CanDeleteVersion = 'canDeleteVersion',
|
CanDeleteVersion = 'canDeleteVersion',
|
||||||
CanCreateVersion = 'canCreateVersion',
|
CanCreateVersion = 'canCreateVersion',
|
||||||
|
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
<div class="nav-item navbar-section">
|
<div class="nav-item navbar-section">
|
||||||
<ng-container
|
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
</div>
|
||||||
</div>
|
|
@@ -1,18 +1,15 @@
|
|||||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}"
|
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||||
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
||||||
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container"
|
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
|
||||||
role="navigation" [attr.aria-label]="'nav.main.description' | translate"> <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="reset-padding-md w-100">
|
<div class="reset-padding-md w-100">
|
||||||
<div id="collapsingNav">
|
<div id="collapsingNav">
|
||||||
<ul class="navbar-nav mr-auto shadow-none">
|
<ul class="navbar-nav mr-auto shadow-none">
|
||||||
<ng-container *ngFor="let section of (sections | async)">
|
<ng-container *ngFor="let section of (sections | async)">
|
||||||
<ng-container
|
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@@ -14,14 +14,41 @@ import { MenuServiceStub } from '../shared/testing/menu-service.stub';
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { BrowseService } from '../core/browse/browse.service';
|
import { BrowseService } from '../core/browse/browse.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
import { buildPaginatedList } from '../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
||||||
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator';
|
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
let comp: NavbarComponent;
|
let comp: NavbarComponent;
|
||||||
let fixture: ComponentFixture<NavbarComponent>;
|
let fixture: ComponentFixture<NavbarComponent>;
|
||||||
|
|
||||||
|
const authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/items/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockItem)
|
||||||
|
}),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe('NavbarComponent', () => {
|
describe('NavbarComponent', () => {
|
||||||
const menuService = new MenuServiceStub();
|
const menuService = new MenuServiceStub();
|
||||||
let browseDefinitions;
|
let browseDefinitions;
|
||||||
@@ -66,8 +93,9 @@ describe('NavbarComponent', () => {
|
|||||||
Injector,
|
Injector,
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
{ provide: ActivatedRoute, useValue: {} },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } }
|
{ provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
@@ -76,7 +104,6 @@ describe('NavbarComponent', () => {
|
|||||||
|
|
||||||
// synchronous beforeEach
|
// synchronous beforeEach
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([]));
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NavbarComponent);
|
fixture = TestBed.createComponent(NavbarComponent);
|
||||||
|
|
||||||
@@ -87,4 +114,6 @@ describe('NavbarComponent', () => {
|
|||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(comp).toBeTruthy();
|
expect(comp).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -11,6 +11,8 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
|||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the public navbar
|
* Component representing the public navbar
|
||||||
@@ -29,11 +31,13 @@ export class NavbarComponent extends MenuComponent {
|
|||||||
menuID = MenuID.PUBLIC;
|
menuID = MenuID.PUBLIC;
|
||||||
|
|
||||||
constructor(protected menuService: MenuService,
|
constructor(protected menuService: MenuService,
|
||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
public windowService: HostWindowService,
|
public windowService: HostWindowService,
|
||||||
public browseService: BrowseService
|
public browseService: BrowseService,
|
||||||
|
public authorizationService: AuthorizationDataService,
|
||||||
|
public route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
super(menuService, injector);
|
super(menuService, injector, authorizationService, route);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@@ -7,9 +7,12 @@ import { MenuComponent } from './menu.component';
|
|||||||
import { MenuServiceStub } from '../testing/menu-service.stub';
|
import { MenuServiceStub } from '../testing/menu-service.stub';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { MenuSection } from './menu.reducer';
|
import { MenuSection } from './menu.reducer';
|
||||||
import { Router } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { MenuID } from './initial-menus-state';
|
import { MenuID } from './initial-menus-state';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||||
|
|
||||||
describe('MenuComponent', () => {
|
describe('MenuComponent', () => {
|
||||||
let comp: MenuComponent;
|
let comp: MenuComponent;
|
||||||
@@ -19,13 +22,44 @@ describe('MenuComponent', () => {
|
|||||||
|
|
||||||
const mockMenuID = 'mock-menuID' as MenuID;
|
const mockMenuID = 'mock-menuID' as MenuID;
|
||||||
|
|
||||||
|
const mockStatisticSection = { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } };
|
||||||
|
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/items/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockItem)
|
||||||
|
}),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(false)
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
||||||
declarations: [MenuComponent],
|
declarations: [MenuComponent],
|
||||||
providers: [
|
providers: [
|
||||||
Injector,
|
Injector,
|
||||||
{ provide: MenuService, useClass: MenuServiceStub }
|
{ provide: MenuService, useClass: MenuServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(MenuComponent, {
|
}).overrideComponent(MenuComponent, {
|
||||||
@@ -95,4 +129,35 @@ describe('MenuComponent', () => {
|
|||||||
expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID);
|
expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when unauthorized statistics', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService as any).isAuthorized.and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return observable of empty object', done => {
|
||||||
|
comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => {
|
||||||
|
expect(res).toEqual({});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get authorized statistics', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return observable of statistics section menu', done => {
|
||||||
|
comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => {
|
||||||
|
expect(res).toEqual(mockStatisticSection);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,14 +1,17 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Injector, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Injector, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { MenuService } from './menu.service';
|
import { MenuService } from './menu.service';
|
||||||
import { MenuID } from './initial-menus-state';
|
import { MenuID } from './initial-menus-state';
|
||||||
import { MenuSection } from './menu.reducer';
|
import { MenuSection } from './menu.reducer';
|
||||||
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
import { distinctUntilChanged, map, mergeMap, switchMap } from 'rxjs/operators';
|
||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { hasValue } from '../empty.util';
|
import { hasValue, isNotEmptyOperator } from '../empty.util';
|
||||||
import { MenuSectionComponent } from './menu-section/menu-section.component';
|
import { MenuSectionComponent } from './menu-section/menu-section.component';
|
||||||
import { getComponentForMenu } from './menu-section.decorator';
|
import { getComponentForMenu } from './menu-section.decorator';
|
||||||
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
|
import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic implementation of a MenuComponent
|
* A basic implementation of a MenuComponent
|
||||||
@@ -67,27 +70,38 @@ export class MenuComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
subs: Subscription[] = [];
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(protected menuService: MenuService, protected injector: Injector) {
|
private activatedRouteLastChild: ActivatedRoute;
|
||||||
|
|
||||||
|
constructor(protected menuService: MenuService, protected injector: Injector, public authorizationService: AuthorizationDataService, public route: ActivatedRoute) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets all instance variables to their initial values
|
* Sets all instance variables to their initial values
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.activatedRouteLastChild = this.getActivatedRoute(this.route);
|
||||||
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||||
this.menuVisible = this.menuService.isMenuVisible(this.menuID);
|
this.menuVisible = this.menuService.isMenuVisible(this.menuID);
|
||||||
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds()));
|
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds()));
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
this.sections.pipe(
|
this.sections.pipe(
|
||||||
// if you return an array from a switchMap it will emit each element as a separate event.
|
// if you return an array from a switchMap it will emit each element as a separate event.
|
||||||
// So this switchMap is equivalent to a subscribe with a forEach inside
|
// So this switchMap is equivalent to a subscribe with a forEach inside
|
||||||
switchMap((sections: MenuSection[]) => sections),
|
switchMap((sections: MenuSection[]) => sections),
|
||||||
|
mergeMap((section: MenuSection) => {
|
||||||
|
if (section.id.includes('statistics')) {
|
||||||
|
return this.getAuthorizedStatistics(section);
|
||||||
|
}
|
||||||
|
return observableOf(section);
|
||||||
|
}),
|
||||||
|
isNotEmptyOperator(),
|
||||||
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
|
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
|
||||||
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
|
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
|
||||||
)),
|
)),
|
||||||
distinctUntilChanged((x,y) => x.section.id === y.section.id)
|
distinctUntilChanged((x, y) => x.section.id === y.section.id)
|
||||||
).subscribe(({ section, component}) => {
|
).subscribe(({ section, component }) => {
|
||||||
const nextMap = this.sectionMap$.getValue();
|
const nextMap = this.sectionMap$.getValue();
|
||||||
nextMap.set(section.id, {
|
nextMap.set(section.id, {
|
||||||
injector: this.getSectionDataInjector(section),
|
injector: this.getSectionDataInjector(section),
|
||||||
@@ -98,6 +112,43 @@ export class MenuComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get activated route of the deepest activated route
|
||||||
|
*/
|
||||||
|
getActivatedRoute(route) {
|
||||||
|
if (route.children.length > 0) {
|
||||||
|
return this.getActivatedRoute(route.firstChild);
|
||||||
|
} else {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get section of statistics after checking authorization
|
||||||
|
*/
|
||||||
|
getAuthorizedStatistics(section) {
|
||||||
|
return this.activatedRouteLastChild.data.pipe(
|
||||||
|
switchMap((data) => {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.CanViewUsageStatistics, this.getObjectUrl(data)).pipe(
|
||||||
|
map((canViewUsageStatistics: boolean) => {
|
||||||
|
if (!canViewUsageStatistics) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics route dso data
|
||||||
|
*/
|
||||||
|
getObjectUrl(data) {
|
||||||
|
const object = data.site ? data.site : data.dso.payload;
|
||||||
|
return object._links.self.href;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collapse this menu when it's currently expanded, expand it when its currently collapsed
|
* Collapse this menu when it's currently expanded, expand it when its currently collapsed
|
||||||
* @param {Event} event The user event that triggered this method
|
* @param {Event} event The user event that triggered this method
|
||||||
@@ -164,8 +215,8 @@ export class MenuComponent implements OnInit, OnDestroy {
|
|||||||
private getSectionComponent(section: MenuSection): Observable<GenericConstructor<MenuSectionComponent>> {
|
private getSectionComponent(section: MenuSection): Observable<GenericConstructor<MenuSectionComponent>> {
|
||||||
return this.menuService.hasSubSections(this.menuID, section.id).pipe(
|
return this.menuService.hasSubSections(this.menuID, section.id).pipe(
|
||||||
map((expandable: boolean) => {
|
map((expandable: boolean) => {
|
||||||
return getComponentForMenu(this.menuID, expandable);
|
return getComponentForMenu(this.menuID, expandable);
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ export class MenuEffects {
|
|||||||
/**
|
/**
|
||||||
* On route change, build menu sections for every menu type depending on the current route data
|
* On route change, build menu sections for every menu type depending on the current route data
|
||||||
*/
|
*/
|
||||||
@Effect({dispatch: false})
|
@Effect({ dispatch: false })
|
||||||
public buildRouteMenuSections$: Observable<Action> = this.actions$
|
public buildRouteMenuSections$: Observable<Action> = this.actions$
|
||||||
.pipe(
|
.pipe(
|
||||||
ofType(ROUTER_NAVIGATED),
|
ofType(ROUTER_NAVIGATED),
|
||||||
@@ -31,8 +31,8 @@ export class MenuEffects {
|
|||||||
);
|
);
|
||||||
|
|
||||||
constructor(private actions$: Actions,
|
constructor(private actions$: Actions,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private route: ActivatedRoute) {
|
private route: ActivatedRoute) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +72,6 @@ export class MenuEffects {
|
|||||||
const last: boolean = hasNoValue(route.firstChild);
|
const last: boolean = hasNoValue(route.firstChild);
|
||||||
|
|
||||||
if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) {
|
if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) {
|
||||||
|
|
||||||
let menuSections: MenuSection[] | MenuSection = data.menu[menuID];
|
let menuSections: MenuSection[] | MenuSection = data.menu[menuID];
|
||||||
menuSections = this.resolveSubstitutions(menuSections, params);
|
menuSections = this.resolveSubstitutions(menuSections, params);
|
||||||
|
|
||||||
@@ -120,4 +119,5 @@ export class MenuEffects {
|
|||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -10,64 +10,69 @@ import { ThemedCommunityStatisticsPageComponent } from './community-statistics-p
|
|||||||
import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component';
|
import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component';
|
||||||
import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component';
|
import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component';
|
||||||
import { ItemResolver } from '../item-page/item.resolver';
|
import { ItemResolver } from '../item-page/item.resolver';
|
||||||
|
import { StatisticsAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
StatisticsPageModule,
|
StatisticsPageModule,
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title: 'statistics.title',
|
|
||||||
breadcrumbKey: 'statistics'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: ThemedSiteStatisticsPageComponent,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
data: {
|
||||||
path: `items/:id`,
|
title: 'statistics.title',
|
||||||
resolve: {
|
breadcrumbKey: 'statistics'
|
||||||
scope: ItemResolver,
|
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title: 'statistics.title',
|
|
||||||
breadcrumbKey: 'statistics'
|
|
||||||
},
|
|
||||||
component: ThemedItemStatisticsPageComponent,
|
|
||||||
},
|
},
|
||||||
{
|
children: [
|
||||||
path: `collections/:id`,
|
{
|
||||||
resolve: {
|
path: '',
|
||||||
scope: CollectionPageResolver,
|
component: ThemedSiteStatisticsPageComponent,
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
|
||||||
},
|
},
|
||||||
data: {
|
],
|
||||||
title: 'statistics.title',
|
canActivate: [StatisticsAdministratorGuard]
|
||||||
breadcrumbKey: 'statistics'
|
},
|
||||||
},
|
{
|
||||||
component: ThemedCollectionStatisticsPageComponent,
|
path: `items/:id`,
|
||||||
|
resolve: {
|
||||||
|
scope: ItemResolver,
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
{
|
data: {
|
||||||
path: `communities/:id`,
|
title: 'statistics.title',
|
||||||
resolve: {
|
breadcrumbKey: 'statistics'
|
||||||
scope: CommunityPageResolver,
|
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
title: 'statistics.title',
|
|
||||||
breadcrumbKey: 'statistics'
|
|
||||||
},
|
|
||||||
component: ThemedCommunityStatisticsPageComponent,
|
|
||||||
},
|
},
|
||||||
]
|
component: ThemedItemStatisticsPageComponent,
|
||||||
|
canActivate: [StatisticsAdministratorGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `collections/:id`,
|
||||||
|
resolve: {
|
||||||
|
scope: CollectionPageResolver,
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'statistics.title',
|
||||||
|
breadcrumbKey: 'statistics'
|
||||||
|
},
|
||||||
|
component: ThemedCollectionStatisticsPageComponent,
|
||||||
|
canActivate: [StatisticsAdministratorGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `communities/:id`,
|
||||||
|
resolve: {
|
||||||
|
scope: CommunityPageResolver,
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'statistics.title',
|
||||||
|
breadcrumbKey: 'statistics'
|
||||||
|
},
|
||||||
|
component: ThemedCommunityStatisticsPageComponent,
|
||||||
|
canActivate: [StatisticsAdministratorGuard]
|
||||||
|
},
|
||||||
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}"
|
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||||
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
class="navbar navbar-expand-md navbar-light p-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
||||||
class="navbar navbar-expand-md navbar-light p-0 navbar-container"
|
|
||||||
role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
|
||||||
<div class="container h-100">
|
<div class="container h-100">
|
||||||
<a class="navbar-brand my-2" routerLink="/home">
|
<a class="navbar-brand my-2" routerLink="/home">
|
||||||
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
|
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div id="collapsingNav" class="w-100 h-100">
|
<div id="collapsingNav" class="w-100 h-100">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100">
|
||||||
<ng-container *ngFor="let section of (sections | async)">
|
<ng-container *ngFor="let section of (sections | async)">
|
||||||
<ng-container
|
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,5 +17,4 @@
|
|||||||
<ds-auth-nav-menu class="navbar-collapsed"></ds-auth-nav-menu>
|
<ds-auth-nav-menu class="navbar-collapsed"></ds-auth-nav-menu>
|
||||||
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
|
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
Reference in New Issue
Block a user