Skip to content
Snippets Groups Projects
Commit 6a828f92 authored by Samuel's avatar Samuel
Browse files

Edit Community - Assign Roles/Groups (Angular)

parent 9adafac6
Branches
No related merge requests found
Showing
with 523 additions and 11 deletions
......@@ -761,6 +761,12 @@
"community.edit.tabs.roles.title": "Community Edit - Roles",
"community.edit.tabs.roles.none": "None",
"community.edit.tabs.roles.admin.name": "Administrators",
"community.edit.tabs.roles.admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).",
"community.form.abstract": "Short Description",
......
......@@ -3,19 +3,27 @@ import { RouterModule } from '@angular/router';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getAccessControlModulePath } from '../admin-routing.module';
const GROUP_EDIT_PATH = 'groups';
export function getGroupEditPath(id: string) {
return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();
}
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } },
{ path: 'groups', component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } },
{ path: GROUP_EDIT_PATH, component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } },
{
path: 'groups/:groupId',
path: `${GROUP_EDIT_PATH}/:groupId`,
component: GroupFormComponent,
data: {title: 'admin.registries.schema.title'}
},
{
path: 'groups/newGroup',
path: `${GROUP_EDIT_PATH}/newGroup`,
component: GroupFormComponent,
data: {title: 'admin.registries.schema.title'}
},
......
......@@ -12,6 +12,10 @@ export function getRegistriesModulePath() {
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
}
export function getAccessControlModulePath() {
return new URLCombiner(getAdminModulePath(), ACCESS_CONTROL_MODULE_PATH).toString();
}
@NgModule({
imports: [
RouterModule.forChild([
......
<ds-comcol-role
*ngFor="let comcolRole of getComcolRoles()"
[dso]="community$ | async"
[comcolRole]="comcolRole"
class="card {{comcolRole.name}}"
>
</ds-comcol-role>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { ActivatedRoute } from '@angular/router';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { CommunityRolesComponent } from './community-roles.component';
import { Community } from '../../../core/shared/community.model';
import { By } from '@angular/platform-browser';
import { RemoteData } from '../../../core/data/remote-data';
describe('CommunityRolesComponent', () => {
let fixture: ComponentFixture<CommunityRolesComponent>;
let comp: CommunityRolesComponent;
let de: DebugElement;
beforeEach(() => {
const route = {
parent: {
data: observableOf({
dso: new RemoteData(
false,
false,
true,
undefined,
new Community(),
)
})
}
};
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
],
declarations: [
CommunityRolesComponent,
],
providers: [
{ provide: ActivatedRoute, useValue: route },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(CommunityRolesComponent);
comp = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
});
it('should display a community admin role component', () => {
expect(de.query(By.css('ds-comcol-role.admin'))).toBeDefined();
});
});
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { Community } from '../../../core/shared/community.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role';
import { RemoteData } from '../../../core/data/remote-data';
/**
* Component for managing a community's roles
......@@ -7,6 +14,38 @@ import { Component } from '@angular/core';
selector: 'ds-community-roles',
templateUrl: './community-roles.component.html',
})
export class CommunityRolesComponent {
/* TODO: Implement Community Edit - Roles */
export class CommunityRolesComponent implements OnInit {
dsoRD$: Observable<RemoteData<Community>>;
/**
* The community to manage, as an observable.
*/
get community$(): Observable<Community> {
return this.dsoRD$.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
)
}
/**
* The different roles for the community.
*/
getComcolRoles(): ComcolRole[] {
return [
ComcolRole.ADMIN,
];
}
constructor(
protected route: ActivatedRoute,
) {
}
ngOnInit(): void {
this.dsoRD$ = this.route.parent.data.pipe(
first(),
map((data) => data.dso),
);
}
}
......@@ -82,7 +82,8 @@ describe('GroupDataService', () => {
rdbService,
store,
null,
halService
halService,
null,
);
};
......
......@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { createSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
import {
GroupRegistryCancelGroupAction,
GroupRegistryEditGroupAction
......@@ -21,16 +21,26 @@ import { DataService } from '../data/data.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import { DeleteRequest, FindListOptions, FindListRequest, PostRequest, RestRequest } from '../data/request.models';
import {
CreateRequest,
DeleteRequest,
FindListOptions,
FindListRequest,
PostRequest
} from '../data/request.models';
import { RequestService } from '../data/request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getResponseFromEntry } from '../shared/operators';
import { configureRequest, getResponseFromEntry} from '../shared/operators';
import { EPerson } from './models/eperson.model';
import { Group } from './models/group.model';
import { dataService } from '../cache/builders/build-decorators';
import { GROUP } from './models/group.resource-type';
import { DSONameService } from '../breadcrumbs/dso-name.service';
import { Community } from '../shared/community.model';
import { Collection } from '../shared/collection.model';
import { ComcolRole } from '../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role';
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
......@@ -56,7 +66,8 @@ export class GroupDataService extends DataService<Group> {
protected rdbService: RemoteDataBuildService,
protected store: Store<any>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService
protected halService: HALEndpointService,
protected nameService: DSONameService,
) {
super();
}
......@@ -309,4 +320,67 @@ export class GroupDataService extends DataService<Group> {
return foundUUID;
}
/**
* Create a group for a given role for a given community or collection.
*
* @param dso The community or collection for which to create a group
* @param comcolRole The role for which to create a group
*/
createComcolGroup(dso: Community|Collection, comcolRole: ComcolRole): Observable<RestResponse> {
const requestId = this.requestService.generateRequestId();
const link = comcolRole.getEndpoint(dso);
const group = Object.assign(new Group(), {
metadata: {
'dc.description': [
{
value: `${this.nameService.getName(dso)} admin group`,
}
],
},
});
return this.halService.getEndpoint(link).pipe(
distinctUntilChanged(),
take(1),
map((endpoint: string) =>
new CreateRequest(
requestId,
endpoint,
JSON.stringify(group),
)
),
configureRequest(this.requestService),
tap(() => this.requestService.removeByHrefSubstring(link)),
switchMap((restRequest) => this.requestService.getByUUID(restRequest.uuid)),
getResponseFromEntry(),
);
}
/**
* Delete the group for a given role for a given community or collection.
*
* @param dso The community or collection for which to delete the group
* @param comcolRole The role for which to delete the group
*/
deleteComcolGroup(dso: Community|Collection, comcolRole: ComcolRole): Observable<RestResponse> {
const requestId = this.requestService.generateRequestId();
const link = comcolRole.getEndpoint(dso);
return this.halService.getEndpoint(link).pipe(
distinctUntilChanged(),
take(1),
map((endpoint: string) =>
new DeleteRequest(
requestId,
endpoint,
)
),
configureRequest(this.requestService),
tap(() => this.requestService.removeByHrefSubstring(link)),
switchMap((restRequest) => this.requestService.getByUUID(restRequest.uuid)),
getResponseFromEntry(),
);
}
}
......@@ -15,6 +15,8 @@ import { RESOURCE_POLICY } from './resource-policy.resource-type';
import { COMMUNITY } from './community.resource-type';
import { Community } from './community.model';
import { ChildHALResource } from './child-hal-resource.model';
import { GROUP } from '../eperson/models/group.resource-type';
import { Group } from '../eperson/models/group.model';
@typedObject
@inheritSerialization(DSpaceObject)
......@@ -70,6 +72,12 @@ export class Collection extends DSpaceObject implements ChildHALResource {
@link(COMMUNITY, false)
parentCommunity?: Observable<RemoteData<Community>>;
/**
* The administrators group of this community.
*/
@link(GROUP)
adminGroup?: Observable<RemoteData<Group>>;
/**
* The introductory text of this Collection
* Corresponds to the metadata field dc.description
......
......@@ -3,6 +3,8 @@ import { Observable } from 'rxjs';
import { link, typedObject } from '../cache/builders/build-decorators';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import { Group } from '../eperson/models/group.model';
import { GROUP } from '../eperson/models/group.resource-type';
import { Bitstream } from './bitstream.model';
import { BITSTREAM } from './bitstream.resource-type';
import { Collection } from './collection.model';
......@@ -32,6 +34,7 @@ export class Community extends DSpaceObject implements ChildHALResource {
logo: HALLink;
subcommunities: HALLink;
parentCommunity: HALLink;
adminGroup: HALLink;
self: HALLink;
};
......@@ -63,6 +66,12 @@ export class Community extends DSpaceObject implements ChildHALResource {
@link(COMMUNITY, false)
parentCommunity?: Observable<RemoteData<Community>>;
/**
* The administrators group of this community.
*/
@link(GROUP)
adminGroup?: Observable<RemoteData<Group>>;
/**
* The introductory text of this Community
* Corresponds to the metadata field dc.description
......
<div class="card p-2">
<div class="card-body d-flex flex-column">
<div class="d-flex flex-row justify-content-between">
<div>
<p>{{'community.edit.tabs.roles.' + comcolRole.name + '.name' | translate}}</p>
</div>
<ng-container *ngVar="group$ | async as group">
<div *ngIf="!group">
{{'community.edit.tabs.roles.none' | translate}}
</div>
<a *ngIf="group"
href="{{editGroupLink$ | async}}">
{{group.name}}
</a>
<div *ngIf="!group"
class="btn btn-outline-dark create"
(click)="create()">create
</div>
<div *ngIf="group"
class="btn btn-outline-dark delete"
(click)="delete()">delete
</div>
</ng-container>
</div>
<div class="mt-2">
{{'community.edit.tabs.roles.' + comcolRole.name + '.description' | translate}}
</div>
</div>
</div>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComcolRoleComponent } from './comcol-role.component';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { By } from '@angular/platform-browser';
import { SharedModule } from '../../../shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { RequestService } from '../../../../core/data/request.service';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { Community } from '../../../../core/shared/community.model';
import { ComcolRole } from './comcol-role';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { RemoteData } from '../../../../core/data/remote-data';
import { Group } from '../../../../core/eperson/models/group.model';
describe('ComcolRoleComponent', () => {
let fixture: ComponentFixture<ComcolRoleComponent>;
let comp: ComcolRoleComponent;
let de: DebugElement;
let groupService;
let linkService;
beforeEach(() => {
groupService = jasmine.createSpyObj('groupService', {
createComcolGroup: undefined,
deleteComcolGroup: undefined,
});
linkService = {
resolveLink: () => undefined,
};
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
SharedModule,
],
declarations: [
],
providers: [
{ provide: GroupDataService, useValue: groupService },
{ provide: LinkService, useValue: linkService },
{ provide: ChangeDetectorRef, useValue: {} },
{ provide: RequestService, useValue: {} },
], schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
fixture = TestBed.createComponent(ComcolRoleComponent);
comp = fixture.componentInstance;
de = fixture.debugElement;
comp.comcolRole = new ComcolRole(
'test name',
'test link name',
);
comp.dso = new Community();
fixture.detectChanges();
});
describe('when there is no group yet', () => {
it('should have a create button but no delete button', () => {
expect(de.query(By.css('.btn.create'))).toBeDefined();
expect(de.query(By.css('.btn.delete'))).toBeNull();
});
describe('when the create button is pressed', () => {
beforeEach(() => {
de.query(By.css('.btn.create')).nativeElement.click();
});
it('should call the groupService create method', () => {
expect(groupService.createComcolGroup).toHaveBeenCalled();
});
});
});
describe('when there is a group yet', () => {
beforeEach(() => {
Object.assign(comp.dso, {
'test link name': observableOf(new RemoteData(
false,
false,
true,
undefined,
new Group(),
)),
});
fixture.detectChanges();
});
it('should have a delete button but no create button', () => {
expect(de.query(By.css('.btn.delete'))).toBeDefined();
expect(de.query(By.css('.btn.create'))).toBeNull();
});
describe('when the delete button is pressed', () => {
beforeEach(() => {
de.query(By.css('.btn.delete')).nativeElement.click();
});
it('should call the groupService delete method', () => {
expect(groupService.deleteComcolGroup).toHaveBeenCalled();
});
});
});
});
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Group } from '../../../../core/eperson/models/group.model';
import { Community } from '../../../../core/shared/community.model';
import { EMPTY, Observable } from 'rxjs';
import { getGroupEditPath } from '../../../../+admin/admin-access-control/admin-access-control-routing.module';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Collection } from '../../../../core/shared/collection.model';
import { map } from 'rxjs/operators';
import { followLink } from '../../../utils/follow-link-config.model';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { ComcolRole } from './comcol-role';
import { RequestService } from '../../../../core/data/request.service';
/**
* Component for managing a community or collection role.
*/
@Component({
selector: 'ds-comcol-role',
styleUrls: ['./comcol-role.component.scss'],
templateUrl: './comcol-role.component.html'
})
export class ComcolRoleComponent implements OnInit {
/**
* The community or collection to manage.
*/
@Input()
dso: Community|Collection;
/**
* The role to manage
*/
@Input()
comcolRole: ComcolRole;
constructor(
protected groupService: GroupDataService,
protected linkService: LinkService,
protected cdr: ChangeDetectorRef,
protected requestService: RequestService,
) {
}
/**
* The group for this role as an observable.
*/
get group$(): Observable<Group> {
if (!this.dso[this.comcolRole.linkName]) {
return EMPTY;
}
return this.dso[this.comcolRole.linkName].pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
);
}
/**
* The link to the group edit page as an observable.
*/
get editGroupLink$(): Observable<string> {
return this.group$.pipe(
map((group) => getGroupEditPath(group.id)),
);
}
/**
* Create a group for this community or collection role.
*/
create() {
this.groupService.createComcolGroup(this.dso, this.comcolRole)
.subscribe(() => {
this.linkService.resolveLink(this.dso, followLink(this.comcolRole.linkName));
this.cdr.detectChanges();
});
}
/**
* Delete the group for this community or collection role.
*/
delete() {
this.groupService.deleteComcolGroup(this.dso, this.comcolRole)
.subscribe(() => {
this.cdr.detectChanges();
})
}
ngOnInit(): void {
this.linkService.resolveLink(this.dso, followLink(this.comcolRole.linkName));
}
}
import { Community } from '../../../../core/shared/community.model';
import { Collection } from '../../../../core/shared/collection.model';
/**
* Class representing a community or collection role.
*/
export class ComcolRole {
/**
* The admin role.
*/
public static ADMIN = new ComcolRole(
'admin',
'adminGroup',
);
/**
* @param name The name for this community or collection role.
* @param linkName The path linking to this community or collection role.
*/
constructor(
public name,
public linkName,
) {
}
/**
* Get the REST endpoint for managing this role for a given community or collection.
* @param dso
*/
public getEndpoint(dso: Community | Collection) {
let linkPath;
switch (dso.type + '') {
case 'community':
linkPath = 'communities';
break;
case 'collection':
linkPath = 'collections';
break;
}
return `${linkPath}/${dso.uuid}/${this.linkName}`;
}
}
......@@ -9,6 +9,7 @@ import { NgbDatepickerModule, NgbModule, NgbTimepickerModule, NgbTypeaheadModule
import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core';
import { NgxPaginationModule } from 'ngx-pagination';
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
import { FileUploadModule } from 'ng2-file-upload';
......@@ -252,6 +253,7 @@ const COMPONENTS = [
EditComColPageComponent,
DeleteComColPageComponent,
ComcolPageBrowseByComponent,
ComcolRoleComponent,
DsDynamicFormComponent,
DsDynamicFormControlContainerComponent,
DsDynamicListComponent,
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment