Merge pull request #862 from atmire/User-agreement

User agreement
This commit is contained in:
Tim Donohue
2020-09-17 11:17:43 -05:00
committed by GitHub
35 changed files with 1174 additions and 53 deletions

View File

@@ -60,3 +60,8 @@ export const UNAUTHORIZED_PATH = 'unauthorized';
export function getUnauthorizedRoute() {
return `/${UNAUTHORIZED_PATH}`;
}
export const INFO_MODULE_PATH = 'info';
export function getInfoModulePath() {
return `/${INFO_MODULE_PATH}`;
}

View File

@@ -13,12 +13,14 @@ import {
REGISTER_PATH,
PROFILE_MODULE_PATH,
ADMIN_MODULE_PATH,
BITSTREAM_MODULE_PATH
BITSTREAM_MODULE_PATH,
INFO_MODULE_PATH
} from './app-routing-paths';
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
import { ReloadGuard } from './core/reload/reload.guard';
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
@NgModule({
imports: [
@@ -27,41 +29,44 @@ import { ReloadGuard } from './core/reload/reload.guard';
children: [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'reload/:rnd', component: PageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false }, canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule' },
{ path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule' },
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule' },
{ path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{
path: 'mydspace',
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
canActivate: [AuthenticatedGuard]
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
},
{ path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard] },
{ path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard, EndUserAgreementCurrentUserGuard] },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
{ path: 'import-external', loadChildren: './+import-external-page/import-external-page.module#ImportExternalPageModule' },
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{ path: 'import-external', loadChildren: './+import-external-page/import-external-page.module#ImportExternalPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
{
path: 'workspaceitems',
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule',
canActivate: [EndUserAgreementCurrentUserGuard]
},
{
path: WORKFLOW_ITEM_MODULE_PATH,
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule',
canActivate: [EndUserAgreementCurrentUserGuard]
},
{
path: PROFILE_MODULE_PATH,
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
},
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] },
{ path: INFO_MODULE_PATH, loadChildren: './info/info.module#InfoModule' },
{ path: UNAUTHORIZED_PATH, component: UnauthorizedComponent },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
]}

View File

@@ -167,6 +167,9 @@ import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabul
import { ConfigurationDataService } from './data/configuration-data.service';
import { ConfigurationProperty } from './shared/configuration-property.model';
import { ReloadGuard } from './reload/reload.guard';
import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user-agreement-current-user.guard';
import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard';
import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -293,6 +296,9 @@ const PROVIDERS = [
MetadataFieldDataService,
TokenResponseParsingService,
ReloadGuard,
EndUserAgreementCurrentUserGuard,
EndUserAgreementCookieGuard,
EndUserAgreementService,
// register AuthInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,

View File

@@ -3,7 +3,7 @@ import { Store } from '@ngrx/store';
import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
@@ -376,6 +376,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
).subscribe();
return this.requestService.getByUUID(requestId).pipe(
hasValueOperator(),
find((request: RequestEntry) => request.completed),
map((request: RequestEntry) => request.response)
);

View File

@@ -0,0 +1,32 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/operators';
/**
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
* That condition is defined by abstract method hasAccepted
*/
export abstract class AbstractEndUserAgreementGuard implements CanActivate {
constructor(protected router: Router) {
}
/**
* True when the user agreement has been accepted
* The user will be redirected to the End User Agreement page if they haven't accepted it before
* A redirect URL will be provided with the navigation so the component can redirect the user back to the blocked route
* when they're finished accepting the agreement
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.hasAccepted().pipe(
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url)
);
}
/**
* This abstract method determines how the User Agreement has to be accepted before the user is allowed to visit
* the desired route
*/
abstract hasAccepted(): Observable<boolean>;
}

View File

@@ -0,0 +1,47 @@
import { EndUserAgreementService } from './end-user-agreement.service';
import { Router, UrlTree } from '@angular/router';
import { EndUserAgreementCookieGuard } from './end-user-agreement-cookie.guard';
describe('EndUserAgreementCookieGuard', () => {
let guard: EndUserAgreementCookieGuard;
let endUserAgreementService: EndUserAgreementService;
let router: Router;
beforeEach(() => {
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
isCookieAccepted: true
});
router = jasmine.createSpyObj('router', {
navigateByUrl: {},
parseUrl: new UrlTree(),
createUrlTree: new UrlTree()
});
guard = new EndUserAgreementCookieGuard(endUserAgreementService, router);
});
describe('canActivate', () => {
describe('when the cookie has been accepted', () => {
it('should return true', (done) => {
guard.canActivate(undefined, { url: Object.assign({ url: 'redirect' }) } as any).subscribe((result) => {
expect(result).toEqual(true);
done();
});
});
});
describe('when the cookie hasn\'t been accepted', () => {
beforeEach(() => {
(endUserAgreementService.isCookieAccepted as jasmine.Spy).and.returnValue(false);
});
it('should return a UrlTree', (done) => {
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
expect(result).toEqual(jasmine.any(UrlTree));
done();
});
});
});
});
});

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
import { Observable } from 'rxjs/internal/Observable';
import { of as observableOf } from 'rxjs';
import { EndUserAgreementService } from './end-user-agreement.service';
import { Router } from '@angular/router';
/**
* A guard redirecting users to the end agreement page when the user agreement cookie hasn't been accepted
*/
@Injectable()
export class EndUserAgreementCookieGuard extends AbstractEndUserAgreementGuard {
constructor(protected endUserAgreementService: EndUserAgreementService,
protected router: Router) {
super(router);
}
/**
* True when the user agreement cookie has been accepted
*/
hasAccepted(): Observable<boolean> {
return observableOf(this.endUserAgreementService.isCookieAccepted());
}
}

View File

@@ -0,0 +1,49 @@
import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-user.guard';
import { EndUserAgreementService } from './end-user-agreement.service';
import { Router, UrlTree } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../auth/auth.service';
describe('EndUserAgreementGuard', () => {
let guard: EndUserAgreementCurrentUserGuard;
let endUserAgreementService: EndUserAgreementService;
let router: Router;
beforeEach(() => {
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
hasCurrentUserAcceptedAgreement: observableOf(true)
});
router = jasmine.createSpyObj('router', {
navigateByUrl: {},
parseUrl: new UrlTree(),
createUrlTree: new UrlTree()
});
guard = new EndUserAgreementCurrentUserGuard(endUserAgreementService, router);
});
describe('canActivate', () => {
describe('when the user has accepted the agreement', () => {
it('should return true', (done) => {
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
expect(result).toEqual(true);
done();
});
});
});
describe('when the user hasn\'t accepted the agreement', () => {
beforeEach(() => {
(endUserAgreementService.hasCurrentUserAcceptedAgreement as jasmine.Spy).and.returnValue(observableOf(false));
});
it('should return a UrlTree', (done) => {
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
expect(result).toEqual(jasmine.any(UrlTree));
done();
});
});
});
});
});

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
import { EndUserAgreementService } from './end-user-agreement.service';
import { Router } from '@angular/router';
/**
* A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement
*/
@Injectable()
export class EndUserAgreementCurrentUserGuard extends AbstractEndUserAgreementGuard {
constructor(protected endUserAgreementService: EndUserAgreementService,
protected router: Router) {
super(router);
}
/**
* True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
*/
hasAccepted(): Observable<boolean> {
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
}
}

View File

@@ -0,0 +1,138 @@
import {
END_USER_AGREEMENT_COOKIE,
END_USER_AGREEMENT_METADATA_FIELD,
EndUserAgreementService
} from './end-user-agreement.service';
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
import { of as observableOf } from 'rxjs';
import { EPerson } from '../eperson/models/eperson.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { RestResponse } from '../cache/response.models';
describe('EndUserAgreementService', () => {
let service: EndUserAgreementService;
let userWithMetadata: EPerson;
let userWithoutMetadata: EPerson;
let cookie;
let authService;
let ePersonService;
beforeEach(() => {
userWithMetadata = Object.assign(new EPerson(), {
metadata: {
[END_USER_AGREEMENT_METADATA_FIELD]: [
{
value: 'true'
}
]
}
});
userWithoutMetadata = Object.assign(new EPerson());
cookie = new CookieServiceMock();
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true),
getAuthenticatedUserFromStore: observableOf(userWithMetadata)
});
ePersonService = jasmine.createSpyObj('ePersonService', {
update: createSuccessfulRemoteDataObject$(userWithMetadata),
patch: observableOf(new RestResponse(true, 200, 'OK'))
});
service = new EndUserAgreementService(cookie, authService, ePersonService);
});
describe('when the cookie is set to true', () => {
beforeEach(() => {
cookie.set(END_USER_AGREEMENT_COOKIE, true);
});
it('hasCurrentUserOrCookieAcceptedAgreement should return true', (done) => {
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
expect(result).toEqual(true);
done();
});
});
it('isCookieAccepted should return true', () => {
expect(service.isCookieAccepted()).toEqual(true);
});
it('removeCookieAccepted should remove the cookie', () => {
service.removeCookieAccepted();
expect(cookie.get(END_USER_AGREEMENT_COOKIE)).toBeUndefined();
});
});
describe('when the cookie isn\'t set', () => {
describe('and the user is authenticated', () => {
beforeEach(() => {
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
});
describe('and the user contains agreement metadata', () => {
beforeEach(() => {
(authService.getAuthenticatedUserFromStore as jasmine.Spy).and.returnValue(observableOf(userWithMetadata));
});
it('hasCurrentUserOrCookieAcceptedAgreement should return true', (done) => {
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
expect(result).toEqual(true);
done();
});
});
});
describe('and the user doesn\'t contain agreement metadata', () => {
beforeEach(() => {
(authService.getAuthenticatedUserFromStore as jasmine.Spy).and.returnValue(observableOf(userWithoutMetadata));
});
it('hasCurrentUserOrCookieAcceptedAgreement should return false', (done) => {
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
expect(result).toEqual(false);
done();
});
});
});
it('setUserAcceptedAgreement should update the user with new metadata', (done) => {
service.setUserAcceptedAgreement(true).subscribe(() => {
expect(ePersonService.patch).toHaveBeenCalled();
done();
});
});
});
describe('and the user is not authenticated', () => {
beforeEach(() => {
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
});
it('hasCurrentUserOrCookieAcceptedAgreement should return false', (done) => {
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
expect(result).toEqual(false);
done();
});
});
it('setUserAcceptedAgreement should set the cookie to true', (done) => {
service.setUserAcceptedAgreement(true).subscribe(() => {
expect(cookie.get(END_USER_AGREEMENT_COOKIE)).toEqual(true);
done();
});
});
});
it('isCookieAccepted should return false', () => {
expect(service.isCookieAccepted()).toEqual(false);
});
it('setCookieAccepted should set the cookie', () => {
service.setCookieAccepted(true);
expect(cookie.get(END_USER_AGREEMENT_COOKIE)).toEqual(true);
});
});
});

View File

@@ -0,0 +1,111 @@
import { Injectable } from '@angular/core';
import { AuthService } from '../auth/auth.service';
import { CookieService } from '../services/cookie.service';
import { Observable } from 'rxjs/internal/Observable';
import { of as observableOf } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
import { EPersonDataService } from '../eperson/eperson-data.service';
export const END_USER_AGREEMENT_COOKIE = 'hasAgreedEndUser';
export const END_USER_AGREEMENT_METADATA_FIELD = 'dspace.agreements.end-user';
/**
* Service for checking and managing the status of the current end user agreement
*/
@Injectable()
export class EndUserAgreementService {
constructor(protected cookie: CookieService,
protected authService: AuthService,
protected ePersonService: EPersonDataService) {
}
/**
* Whether or not either the cookie was accepted or the current user has accepted the End User Agreement
* @param acceptedWhenAnonymous Whether or not the user agreement should be considered accepted if the user is
* currently not authenticated (anonymous)
*/
hasCurrentUserOrCookieAcceptedAgreement(acceptedWhenAnonymous: boolean): Observable<boolean> {
if (this.isCookieAccepted()) {
return observableOf(true);
} else {
return this.hasCurrentUserAcceptedAgreement(acceptedWhenAnonymous);
}
}
/**
* Whether or not the current user has accepted the End User Agreement
* @param acceptedWhenAnonymous Whether or not the user agreement should be considered accepted if the user is
* currently not authenticated (anonymous)
*/
hasCurrentUserAcceptedAgreement(acceptedWhenAnonymous: boolean): Observable<boolean> {
return this.authService.isAuthenticated().pipe(
switchMap((authenticated) => {
if (authenticated) {
return this.authService.getAuthenticatedUserFromStore().pipe(
map((user) => hasValue(user) && user.hasMetadata(END_USER_AGREEMENT_METADATA_FIELD) && user.firstMetadata(END_USER_AGREEMENT_METADATA_FIELD).value === 'true')
);
} else {
return observableOf(acceptedWhenAnonymous);
}
})
);
}
/**
* Set the current user's accepted agreement status
* When a user is authenticated, set his/her metadata to the provided value
* When no user is authenticated, set the cookie to the provided value
* @param accepted
*/
setUserAcceptedAgreement(accepted: boolean): Observable<boolean> {
return this.authService.isAuthenticated().pipe(
switchMap((authenticated) => {
if (authenticated) {
return this.authService.getAuthenticatedUserFromStore().pipe(
take(1),
switchMap((user) => {
const newValue = { value: String(accepted) };
let operation;
if (user.hasMetadata(END_USER_AGREEMENT_METADATA_FIELD)) {
operation = { op: 'replace', path: `/metadata/${END_USER_AGREEMENT_METADATA_FIELD}/0`, value: newValue };
} else {
operation = { op: 'add', path: `/metadata/${END_USER_AGREEMENT_METADATA_FIELD}`, value: [ newValue ] };
}
return this.ePersonService.patch(user, [operation]);
}),
map((response) => response.isSuccessful)
);
} else {
this.setCookieAccepted(accepted);
return observableOf(true);
}
}),
take(1)
);
}
/**
* Is the End User Agreement accepted in the cookie?
*/
isCookieAccepted(): boolean {
return this.cookie.get(END_USER_AGREEMENT_COOKIE) === true;
}
/**
* Set the cookie's End User Agreement accepted state
* @param accepted
*/
setCookieAccepted(accepted: boolean) {
this.cookie.set(END_USER_AGREEMENT_COOKIE, accepted);
}
/**
* Remove the End User Agreement cookie
*/
removeCookieAccepted() {
this.cookie.remove(END_USER_AGREEMENT_COOKIE);
}
}

View File

@@ -12,6 +12,7 @@ import { RequestService } from '../data/request.service';
import { BrowseDefinition } from './browse-definition.model';
import { DSpaceObject } from './dspace-object.model';
import { getUnauthorizedRoute } from '../../app-routing-paths';
import { getEndUserAgreementPath } from '../../info/info-routing.module';
/**
* This file contains custom RxJS operators that can be used in multiple places
@@ -192,6 +193,20 @@ export const returnUnauthorizedUrlTreeOnFalse = (router: Router) =>
return authorized ? authorized : router.parseUrl(getUnauthorizedRoute())
}));
/**
* Operator that returns a UrlTree to the unauthorized page when the boolean received is false
* @param router Router
* @param redirect Redirect URL to add to the UrlTree. This is used to redirect back to the original route after the
* user accepts the agreement.
*/
export const returnEndUserAgreementUrlTreeOnFalse = (router: Router, redirect: string) =>
(source: Observable<boolean>): Observable<boolean | UrlTree> =>
source.pipe(
map((hasAgreed: boolean) => {
const queryParams = { redirect: encodeURIComponent(redirect) };
return hasAgreed ? hasAgreed : router.createUrlTree([getEndUserAgreementPath()], { queryParams });
}));
export const getFinishedRemoteData = () =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));

View File

@@ -0,0 +1,37 @@
<h2>{{ 'info.end-user-agreement.head' | translate }}</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu. In ante metus dictum at tempor. Diam phasellus vestibulum lorem sed risus. Sed cras ornare arcu dui vivamus. Sit amet consectetur adipiscing elit pellentesque. Id velit ut tortor pretium viverra suspendisse potenti. Sed euismod nisi porta lorem mollis aliquam ut. Justo laoreet sit amet cursus sit amet dictum sit. Ullamcorper morbi tincidunt ornare massa eget egestas.
</p>
<p>
In iaculis nunc sed augue lacus. Curabitur vitae nunc sed velit dignissim sodales ut eu sem. Tellus id interdum velit laoreet id donec ultrices tincidunt arcu. Quis vel eros donec ac odio tempor. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Varius quam quisque id diam vel quam. Nisl tincidunt eget nullam non nisi est sit. Nunc aliquet bibendum enim facilisis. Aenean sed adipiscing diam donec adipiscing. Convallis tellus id interdum velit laoreet. Massa placerat duis ultricies lacus sed turpis tincidunt. Sed cras ornare arcu dui vivamus arcu. Egestas integer eget aliquet nibh praesent tristique. Sit amet purus gravida quis blandit turpis cursus in hac. Porta non pulvinar neque laoreet suspendisse. Quis risus sed vulputate odio ut. Dignissim enim sit amet venenatis urna cursus.
</p>
<p>
Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. Massa sapien faucibus et molestie. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Metus dictum at tempor commodo ullamcorper. Tincidunt lobortis feugiat vivamus at augue eget. Non diam phasellus vestibulum lorem sed risus ultricies. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi. Euismod lacinia at quis risus sed. Lorem mollis aliquam ut porttitor leo a diam. Ipsum dolor sit amet consectetur. Ante in nibh mauris cursus mattis molestie a iaculis at. Commodo ullamcorper a lacus vestibulum. Pellentesque elit eget gravida cum sociis. Sit amet commodo nulla facilisi nullam vehicula. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean.
</p>
<p>
Ac turpis egestas maecenas pharetra convallis. Lacus sed viverra tellus in. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Id aliquet risus feugiat in ante metus dictum at. Quis enim lobortis scelerisque fermentum dui faucibus. Eu volutpat odio facilisis mauris sit amet massa vitae tortor. Tellus elementum sagittis vitae et leo. Cras sed felis eget velit aliquet sagittis. Proin fermentum leo vel orci porta non pulvinar neque laoreet. Dui sapien eget mi proin sed libero enim. Ultrices mi tempus imperdiet nulla malesuada. Mattis molestie a iaculis at. Turpis massa sed elementum tempus egestas.
</p>
<p>
Dui faucibus in ornare quam viverra orci sagittis eu volutpat. Cras adipiscing enim eu turpis. Ac felis donec et odio pellentesque. Iaculis nunc sed augue lacus viverra vitae congue eu consequat. Posuere lorem ipsum dolor sit amet consectetur adipiscing elit duis. Elit eget gravida cum sociis natoque penatibus. Id faucibus nisl tincidunt eget nullam non. Sagittis aliquam malesuada bibendum arcu vitae. Fermentum leo vel orci porta. Aliquam ultrices sagittis orci a scelerisque purus semper. Diam maecenas sed enim ut sem viverra aliquet eget sit. Et ultrices neque ornare aenean euismod. Eu mi bibendum neque egestas congue quisque egestas diam. Eget lorem dolor sed viverra. Ut lectus arcu bibendum at. Rutrum tellus pellentesque eu tincidunt tortor. Vitae congue eu consequat ac. Elit ullamcorper dignissim cras tincidunt. Sit amet volutpat consequat mauris nunc congue nisi.
</p>
<p>
Cursus in hac habitasse platea dictumst quisque sagittis purus. Placerat duis ultricies lacus sed turpis tincidunt. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Non nisi est sit amet facilisis magna. In massa tempor nec feugiat nisl pretium fusce. Pulvinar neque laoreet suspendisse interdum consectetur. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan. Fringilla urna porttitor rhoncus dolor purus non enim. Mauris nunc congue nisi vitae suscipit. Commodo elit at imperdiet dui accumsan sit amet nulla. Tempor id eu nisl nunc mi ipsum faucibus. Porta non pulvinar neque laoreet suspendisse. Nec nam aliquam sem et tortor consequat.
</p>
<p>
Eget nunc lobortis mattis aliquam faucibus purus. Odio tempor orci dapibus ultrices. Sed nisi lacus sed viverra tellus. Elit ullamcorper dignissim cras tincidunt. Porttitor rhoncus dolor purus non enim praesent elementum facilisis. Viverra orci sagittis eu volutpat odio. Pharetra massa massa ultricies mi quis. Lectus vestibulum mattis ullamcorper velit sed ullamcorper. Pulvinar neque laoreet suspendisse interdum consectetur. Vitae auctor eu augue ut. Arcu dictum varius duis at consectetur lorem donec. Massa sed elementum tempus egestas sed sed. Risus viverra adipiscing at in tellus integer. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Pharetra massa massa ultricies mi. Elementum eu facilisis sed odio morbi quis commodo odio. Tincidunt lobortis feugiat vivamus at. Felis donec et odio pellentesque diam volutpat commodo sed. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate.
</p>
<p>
Lectus proin nibh nisl condimentum id venenatis a condimentum. Id consectetur purus ut faucibus pulvinar elementum integer enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Suscipit tellus mauris a diam maecenas sed enim ut sem. Dolor purus non enim praesent elementum facilisis. Non enim praesent elementum facilisis leo vel. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Nulla porttitor massa id neque aliquam vestibulum. Erat velit scelerisque in dictum non consectetur. Amet cursus sit amet dictum. Nec tincidunt praesent semper feugiat nibh. Rutrum quisque non tellus orci ac auctor. Sagittis aliquam malesuada bibendum arcu vitae elementum. Massa tincidunt dui ut ornare lectus sit amet est. Aliquet porttitor lacus luctus accumsan tortor posuere ac. Quis hendrerit dolor magna eget est lorem ipsum dolor sit. Lectus mauris ultrices eros in.
</p>
<p>
Massa massa ultricies mi quis hendrerit dolor magna. Est ullamcorper eget nulla facilisi etiam dignissim diam. Vulputate sapien nec sagittis aliquam malesuada. Nisi porta lorem mollis aliquam ut porttitor leo a diam. Tempus quam pellentesque nec nam. Faucibus vitae aliquet nec ullamcorper sit amet risus nullam eget. Gravida arcu ac tortor dignissim convallis aenean et tortor. A scelerisque purus semper eget duis at tellus at. Viverra ipsum nunc aliquet bibendum enim. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Et ultrices neque ornare aenean euismod. Consequat semper viverra nam libero justo laoreet. Nunc mattis enim ut tellus elementum sagittis. Consectetur lorem donec massa sapien faucibus et. Vel risus commodo viverra maecenas accumsan lacus vel facilisis. Diam sollicitudin tempor id eu nisl nunc. Dolor magna eget est lorem ipsum dolor. Adipiscing elit pellentesque habitant morbi tristique.
</p>
<p>
Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien. Porttitor leo a diam sollicitudin tempor. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. Leo urna molestie at elementum. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Libero id faucibus nisl tincidunt eget nullam. Tellus elementum sagittis vitae et leo duis ut diam. Sodales ut etiam sit amet nisl purus in mollis. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Aliquam malesuada bibendum arcu vitae elementum. Leo vel orci porta non pulvinar neque laoreet. Ipsum suspendisse ultrices gravida dictum fusce.
</p>
<p>
Egestas erat imperdiet sed euismod nisi porta lorem. Venenatis a condimentum vitae sapien pellentesque habitant. Sit amet luctus venenatis lectus magna fringilla urna porttitor. Orci sagittis eu volutpat odio facilisis mauris sit amet massa. Ut enim blandit volutpat maecenas volutpat blandit aliquam. Libero volutpat sed cras ornare. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. Diam quis enim lobortis scelerisque fermentum dui. Pellentesque habitant morbi tristique senectus et netus. Auctor urna nunc id cursus metus aliquam eleifend. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Sed risus ultricies tristique nulla aliquet enim tortor. Tincidunt arcu non sodales neque sodales ut. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt.
</p>
<p>
Pulvinar etiam non quam lacus suspendisse faucibus. Eu mi bibendum neque egestas congue. Egestas purus viverra accumsan in nisl nisi scelerisque eu. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Eu non diam phasellus vestibulum. Semper feugiat nibh sed pulvinar. Ante in nibh mauris cursus mattis molestie a. Maecenas accumsan lacus vel facilisis volutpat. Non quam lacus suspendisse faucibus. Quis commodo odio aenean sed adipiscing. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Sed cras ornare arcu dui vivamus arcu felis. Tortor vitae purus faucibus ornare suspendisse sed. Morbi tincidunt ornare massa eget egestas purus viverra. Nibh cras pulvinar mattis nunc. Luctus venenatis lectus magna fringilla urna porttitor. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Felis eget nunc lobortis mattis aliquam faucibus purus in. Vivamus arcu felis bibendum ut.
</p>

View File

@@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EndUserAgreementContentComponent } from './end-user-agreement-content.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('EndUserAgreementContentComponent', () => {
let component: EndUserAgreementContentComponent;
let fixture: ComponentFixture<EndUserAgreementContentComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ TranslateModule.forRoot() ],
declarations: [ EndUserAgreementContentComponent ],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EndUserAgreementContentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-end-user-agreement-content',
templateUrl: './end-user-agreement-content.component.html',
styleUrls: ['./end-user-agreement-content.component.scss']
})
/**
* Component displaying the contents of the End User Agreement
*/
export class EndUserAgreementContentComponent {
}

View File

@@ -0,0 +1,13 @@
<div class="container">
<ds-end-user-agreement-content></ds-end-user-agreement-content>
<form class="form-user-agreement-accept mt-4" (ngSubmit)="submit()" novalidate>
<input class="ml-1 mr-2" type="checkbox" id="user-agreement-accept" [(ngModel)]="accepted" [ngModelOptions]="{standalone: true}">
<label class="col-form-label-lg" for="user-agreement-accept">{{ 'info.end-user-agreement.accept' | translate }}</label>
<div class="d-flex mt-4">
<button id="button-cancel" type="button" (click)="cancel()" class="btn btn-outline-secondary mr-auto">{{ 'info.end-user-agreement.buttons.cancel' | translate }}</button>
<button id="button-save" type="submit" class="btn btn-primary" [disabled]="!accepted">{{ 'info.end-user-agreement.buttons.save' | translate }}</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,8 @@
input#user-agreement-accept {
/* Large-sized Checkboxes */
-ms-transform: scale(1.6); /* IE */
-moz-transform: scale(1.6); /* FF */
-webkit-transform: scale(1.6); /* Safari and Chrome */
-o-transform: scale(1.6); /* Opera */
padding: 10px;
}

View File

@@ -0,0 +1,156 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EndUserAgreementComponent } from './end-user-agreement.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from '../../core/auth/auth.service';
import { ActivatedRoute, Router } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { Store } from '@ngrx/store';
import { By } from '@angular/platform-browser';
import { LogOutAction } from '../../core/auth/auth.actions';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
describe('EndUserAgreementComponent', () => {
let component: EndUserAgreementComponent;
let fixture: ComponentFixture<EndUserAgreementComponent>;
let endUserAgreementService: EndUserAgreementService;
let notificationsService: NotificationsService;
let authService: AuthService;
let store;
let router: Router;
let route: ActivatedRoute;
let redirectUrl;
function init() {
redirectUrl = 'redirect/url';
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
hasCurrentUserOrCookieAcceptedAgreement : observableOf(false),
setUserAcceptedAgreement: observableOf(true)
});
notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']);
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true)
});
store = jasmine.createSpyObj('store', ['dispatch']);
router = jasmine.createSpyObj('router', ['navigate', 'navigateByUrl']);
route = Object.assign(new ActivatedRouteStub(), {
queryParams: observableOf({
redirect: redirectUrl
})
}) as any;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [ TranslateModule.forRoot() ],
declarations: [ EndUserAgreementComponent ],
providers: [
{ provide: EndUserAgreementService, useValue: endUserAgreementService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: AuthService, useValue: authService },
{ provide: Store, useValue: store },
{ provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: route }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EndUserAgreementComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('when the user hasn\'t accepted the agreement', () => {
beforeEach(() => {
(endUserAgreementService.hasCurrentUserOrCookieAcceptedAgreement as jasmine.Spy).and.returnValue(observableOf(false));
component.ngOnInit();
fixture.detectChanges();
});
it('should initialize the accepted property', () => {
expect(component.accepted).toEqual(false);
});
it('should disable the save button', () => {
const button = fixture.debugElement.query(By.css('#button-save')).nativeElement;
expect(button.disabled).toBeTruthy();
});
});
describe('when the user has accepted the agreement', () => {
beforeEach(() => {
(endUserAgreementService.hasCurrentUserOrCookieAcceptedAgreement as jasmine.Spy).and.returnValue(observableOf(true));
component.ngOnInit();
fixture.detectChanges();
});
it('should initialize the accepted property', () => {
expect(component.accepted).toEqual(true);
});
it('should enable the save button', () => {
const button = fixture.debugElement.query(By.css('#button-save')).nativeElement;
expect(button.disabled).toBeFalsy();
});
describe('submit', () => {
describe('when accepting the agreement was successful', () => {
beforeEach(() => {
(endUserAgreementService.setUserAcceptedAgreement as jasmine.Spy).and.returnValue(observableOf(true));
component.submit();
});
it('should display a success notification', () => {
expect(notificationsService.success).toHaveBeenCalled();
});
it('should navigate the user to the redirect url', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith(redirectUrl);
});
});
describe('when accepting the agreement was unsuccessful', () => {
beforeEach(() => {
(endUserAgreementService.setUserAcceptedAgreement as jasmine.Spy).and.returnValue(observableOf(false));
component.submit();
});
it('should display an error notification', () => {
expect(notificationsService.error).toHaveBeenCalled();
});
});
});
});
describe('cancel', () => {
describe('when the user is authenticated', () => {
beforeEach(() => {
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
component.cancel();
});
it('should logout the user', () => {
expect(store.dispatch).toHaveBeenCalledWith(new LogOutAction());
});
});
describe('when the user is not authenticated', () => {
beforeEach(() => {
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
component.cancel();
});
it('should navigate the user to the homepage', () => {
expect(router.navigate).toHaveBeenCalledWith(['home']);
});
});
});
});

View File

@@ -0,0 +1,92 @@
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../core/auth/auth.service';
import { map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { LogOutAction } from '../../core/auth/auth.actions';
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { isNotEmpty } from '../../shared/empty.util';
@Component({
selector: 'ds-end-user-agreement',
templateUrl: './end-user-agreement.component.html',
styleUrls: ['./end-user-agreement.component.scss']
})
/**
* Component displaying the End User Agreement and an option to accept it
*/
export class EndUserAgreementComponent implements OnInit {
/**
* Whether or not the user agreement has been accepted
*/
accepted = false;
constructor(protected endUserAgreementService: EndUserAgreementService,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected authService: AuthService,
protected store: Store<AppState>,
protected router: Router,
protected route: ActivatedRoute) {
}
/**
* Initialize the component
*/
ngOnInit(): void {
this.initAccepted();
}
/**
* Initialize the "accepted" property of this component by checking if the current user has accepted it before
*/
initAccepted() {
this.endUserAgreementService.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((accepted) => {
this.accepted = accepted;
});
}
/**
* Submit the form
* Set the End User Agreement, display a notification and (optionally) redirect the user back to their original destination
*/
submit() {
this.endUserAgreementService.setUserAcceptedAgreement(this.accepted).pipe(
switchMap((success) => {
if (success) {
this.notificationsService.success(this.translate.instant('info.end-user-agreement.accept.success'));
return this.route.queryParams.pipe(map((params) => params.redirect));
} else {
this.notificationsService.error(this.translate.instant('info.end-user-agreement.accept.error'));
return observableOf(undefined);
}
}),
take(1)
).subscribe((redirectUrl) => {
if (isNotEmpty(redirectUrl)) {
this.router.navigateByUrl(decodeURIComponent(redirectUrl));
}
});
}
/**
* Cancel the agreement
* If the user is logged in, this will log him/her out
* If the user is not logged in, they will be redirected to the homepage
*/
cancel() {
this.authService.isAuthenticated().pipe(take(1)).subscribe((authenticated) => {
if (authenticated) {
this.store.dispatch(new LogOutAction());
} else {
this.router.navigate(['home']);
}
});
}
}

View File

@@ -0,0 +1,47 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EndUserAgreementComponent } from './end-user-agreement/end-user-agreement.component';
import { getInfoModulePath } from '../app-routing-paths';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { PrivacyComponent } from './privacy/privacy.component';
const END_USER_AGREEMENT_PATH = 'end-user-agreement';
const PRIVACY_PATH = 'privacy';
export function getEndUserAgreementPath() {
return getSubPath(END_USER_AGREEMENT_PATH);
}
export function getPrivacyPath() {
return getSubPath(PRIVACY_PATH);
}
function getSubPath(path: string) {
return `${getInfoModulePath()}/${path}`;
}
@NgModule({
imports: [
RouterModule.forChild([
{
path: END_USER_AGREEMENT_PATH,
component: EndUserAgreementComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }
}
]),
RouterModule.forChild([
{
path: PRIVACY_PATH,
component: PrivacyComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
}
])
]
})
/**
* Module for navigating to components within the info module
*/
export class InfoRoutingModule {
}

View File

@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../shared/shared.module';
import { EndUserAgreementComponent } from './end-user-agreement/end-user-agreement.component';
import { InfoRoutingModule } from './info-routing.module';
import { EndUserAgreementContentComponent } from './end-user-agreement/end-user-agreement-content/end-user-agreement-content.component';
import { PrivacyComponent } from './privacy/privacy.component';
import { PrivacyContentComponent } from './privacy/privacy-content/privacy-content.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
InfoRoutingModule
],
declarations: [
EndUserAgreementComponent,
EndUserAgreementContentComponent,
PrivacyComponent,
PrivacyContentComponent
]
})
export class InfoModule {
}

View File

@@ -0,0 +1,37 @@
<h2>{{ 'info.privacy.head' | translate }}</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu. In ante metus dictum at tempor. Diam phasellus vestibulum lorem sed risus. Sed cras ornare arcu dui vivamus. Sit amet consectetur adipiscing elit pellentesque. Id velit ut tortor pretium viverra suspendisse potenti. Sed euismod nisi porta lorem mollis aliquam ut. Justo laoreet sit amet cursus sit amet dictum sit. Ullamcorper morbi tincidunt ornare massa eget egestas.
</p>
<p>
In iaculis nunc sed augue lacus. Curabitur vitae nunc sed velit dignissim sodales ut eu sem. Tellus id interdum velit laoreet id donec ultrices tincidunt arcu. Quis vel eros donec ac odio tempor. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Varius quam quisque id diam vel quam. Nisl tincidunt eget nullam non nisi est sit. Nunc aliquet bibendum enim facilisis. Aenean sed adipiscing diam donec adipiscing. Convallis tellus id interdum velit laoreet. Massa placerat duis ultricies lacus sed turpis tincidunt. Sed cras ornare arcu dui vivamus arcu. Egestas integer eget aliquet nibh praesent tristique. Sit amet purus gravida quis blandit turpis cursus in hac. Porta non pulvinar neque laoreet suspendisse. Quis risus sed vulputate odio ut. Dignissim enim sit amet venenatis urna cursus.
</p>
<p>
Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. Massa sapien faucibus et molestie. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Metus dictum at tempor commodo ullamcorper. Tincidunt lobortis feugiat vivamus at augue eget. Non diam phasellus vestibulum lorem sed risus ultricies. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi. Euismod lacinia at quis risus sed. Lorem mollis aliquam ut porttitor leo a diam. Ipsum dolor sit amet consectetur. Ante in nibh mauris cursus mattis molestie a iaculis at. Commodo ullamcorper a lacus vestibulum. Pellentesque elit eget gravida cum sociis. Sit amet commodo nulla facilisi nullam vehicula. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean.
</p>
<p>
Ac turpis egestas maecenas pharetra convallis. Lacus sed viverra tellus in. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Id aliquet risus feugiat in ante metus dictum at. Quis enim lobortis scelerisque fermentum dui faucibus. Eu volutpat odio facilisis mauris sit amet massa vitae tortor. Tellus elementum sagittis vitae et leo. Cras sed felis eget velit aliquet sagittis. Proin fermentum leo vel orci porta non pulvinar neque laoreet. Dui sapien eget mi proin sed libero enim. Ultrices mi tempus imperdiet nulla malesuada. Mattis molestie a iaculis at. Turpis massa sed elementum tempus egestas.
</p>
<p>
Dui faucibus in ornare quam viverra orci sagittis eu volutpat. Cras adipiscing enim eu turpis. Ac felis donec et odio pellentesque. Iaculis nunc sed augue lacus viverra vitae congue eu consequat. Posuere lorem ipsum dolor sit amet consectetur adipiscing elit duis. Elit eget gravida cum sociis natoque penatibus. Id faucibus nisl tincidunt eget nullam non. Sagittis aliquam malesuada bibendum arcu vitae. Fermentum leo vel orci porta. Aliquam ultrices sagittis orci a scelerisque purus semper. Diam maecenas sed enim ut sem viverra aliquet eget sit. Et ultrices neque ornare aenean euismod. Eu mi bibendum neque egestas congue quisque egestas diam. Eget lorem dolor sed viverra. Ut lectus arcu bibendum at. Rutrum tellus pellentesque eu tincidunt tortor. Vitae congue eu consequat ac. Elit ullamcorper dignissim cras tincidunt. Sit amet volutpat consequat mauris nunc congue nisi.
</p>
<p>
Cursus in hac habitasse platea dictumst quisque sagittis purus. Placerat duis ultricies lacus sed turpis tincidunt. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Non nisi est sit amet facilisis magna. In massa tempor nec feugiat nisl pretium fusce. Pulvinar neque laoreet suspendisse interdum consectetur. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan. Fringilla urna porttitor rhoncus dolor purus non enim. Mauris nunc congue nisi vitae suscipit. Commodo elit at imperdiet dui accumsan sit amet nulla. Tempor id eu nisl nunc mi ipsum faucibus. Porta non pulvinar neque laoreet suspendisse. Nec nam aliquam sem et tortor consequat.
</p>
<p>
Eget nunc lobortis mattis aliquam faucibus purus. Odio tempor orci dapibus ultrices. Sed nisi lacus sed viverra tellus. Elit ullamcorper dignissim cras tincidunt. Porttitor rhoncus dolor purus non enim praesent elementum facilisis. Viverra orci sagittis eu volutpat odio. Pharetra massa massa ultricies mi quis. Lectus vestibulum mattis ullamcorper velit sed ullamcorper. Pulvinar neque laoreet suspendisse interdum consectetur. Vitae auctor eu augue ut. Arcu dictum varius duis at consectetur lorem donec. Massa sed elementum tempus egestas sed sed. Risus viverra adipiscing at in tellus integer. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Pharetra massa massa ultricies mi. Elementum eu facilisis sed odio morbi quis commodo odio. Tincidunt lobortis feugiat vivamus at. Felis donec et odio pellentesque diam volutpat commodo sed. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate.
</p>
<p>
Lectus proin nibh nisl condimentum id venenatis a condimentum. Id consectetur purus ut faucibus pulvinar elementum integer enim. Non pulvinar neque laoreet suspendisse interdum consectetur. Est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Suscipit tellus mauris a diam maecenas sed enim ut sem. Dolor purus non enim praesent elementum facilisis. Non enim praesent elementum facilisis leo vel. Ultricies leo integer malesuada nunc vel risus commodo viverra maecenas. Nulla porttitor massa id neque aliquam vestibulum. Erat velit scelerisque in dictum non consectetur. Amet cursus sit amet dictum. Nec tincidunt praesent semper feugiat nibh. Rutrum quisque non tellus orci ac auctor. Sagittis aliquam malesuada bibendum arcu vitae elementum. Massa tincidunt dui ut ornare lectus sit amet est. Aliquet porttitor lacus luctus accumsan tortor posuere ac. Quis hendrerit dolor magna eget est lorem ipsum dolor sit. Lectus mauris ultrices eros in.
</p>
<p>
Massa massa ultricies mi quis hendrerit dolor magna. Est ullamcorper eget nulla facilisi etiam dignissim diam. Vulputate sapien nec sagittis aliquam malesuada. Nisi porta lorem mollis aliquam ut porttitor leo a diam. Tempus quam pellentesque nec nam. Faucibus vitae aliquet nec ullamcorper sit amet risus nullam eget. Gravida arcu ac tortor dignissim convallis aenean et tortor. A scelerisque purus semper eget duis at tellus at. Viverra ipsum nunc aliquet bibendum enim. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Et ultrices neque ornare aenean euismod. Consequat semper viverra nam libero justo laoreet. Nunc mattis enim ut tellus elementum sagittis. Consectetur lorem donec massa sapien faucibus et. Vel risus commodo viverra maecenas accumsan lacus vel facilisis. Diam sollicitudin tempor id eu nisl nunc. Dolor magna eget est lorem ipsum dolor. Adipiscing elit pellentesque habitant morbi tristique.
</p>
<p>
Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Egestas fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien. Porttitor leo a diam sollicitudin tempor. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. Leo urna molestie at elementum. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi. Libero id faucibus nisl tincidunt eget nullam. Tellus elementum sagittis vitae et leo duis ut diam. Sodales ut etiam sit amet nisl purus in mollis. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Aliquam malesuada bibendum arcu vitae elementum. Leo vel orci porta non pulvinar neque laoreet. Ipsum suspendisse ultrices gravida dictum fusce.
</p>
<p>
Egestas erat imperdiet sed euismod nisi porta lorem. Venenatis a condimentum vitae sapien pellentesque habitant. Sit amet luctus venenatis lectus magna fringilla urna porttitor. Orci sagittis eu volutpat odio facilisis mauris sit amet massa. Ut enim blandit volutpat maecenas volutpat blandit aliquam. Libero volutpat sed cras ornare. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. Diam quis enim lobortis scelerisque fermentum dui. Pellentesque habitant morbi tristique senectus et netus. Auctor urna nunc id cursus metus aliquam eleifend. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Sed risus ultricies tristique nulla aliquet enim tortor. Tincidunt arcu non sodales neque sodales ut. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt.
</p>
<p>
Pulvinar etiam non quam lacus suspendisse faucibus. Eu mi bibendum neque egestas congue. Egestas purus viverra accumsan in nisl nisi scelerisque eu. Vulputate enim nulla aliquet porttitor lacus luctus accumsan. Eu non diam phasellus vestibulum. Semper feugiat nibh sed pulvinar. Ante in nibh mauris cursus mattis molestie a. Maecenas accumsan lacus vel facilisis volutpat. Non quam lacus suspendisse faucibus. Quis commodo odio aenean sed adipiscing. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Sed cras ornare arcu dui vivamus arcu felis. Tortor vitae purus faucibus ornare suspendisse sed. Morbi tincidunt ornare massa eget egestas purus viverra. Nibh cras pulvinar mattis nunc. Luctus venenatis lectus magna fringilla urna porttitor. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Felis eget nunc lobortis mattis aliquam faucibus purus in. Vivamus arcu felis bibendum ut.
</p>

View File

@@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PrivacyContentComponent } from './privacy-content.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('PrivacyContentComponent', () => {
let component: PrivacyContentComponent;
let fixture: ComponentFixture<PrivacyContentComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ TranslateModule.forRoot() ],
declarations: [ PrivacyContentComponent ],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PrivacyContentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-privacy-content',
templateUrl: './privacy-content.component.html',
styleUrls: ['./privacy-content.component.scss']
})
/**
* Component displaying the contents of the Privacy Statement
*/
export class PrivacyContentComponent {
}

View File

@@ -0,0 +1,3 @@
<div class="container">
<ds-privacy-content></ds-privacy-content>
</div>

View File

@@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PrivacyComponent } from './privacy.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('PrivacyComponent', () => {
let component: PrivacyComponent;
let fixture: ComponentFixture<PrivacyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ TranslateModule.forRoot() ],
declarations: [ PrivacyComponent ],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PrivacyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-privacy',
templateUrl: './privacy.component.html',
styleUrls: ['./privacy.component.scss']
})
/**
* Component displaying the Privacy Statement
*/
export class PrivacyComponent {
}

View File

@@ -18,6 +18,10 @@ import { EPerson } from '../../core/eperson/models/eperson.model';
import { AuthenticateAction } from '../../core/auth/auth.actions';
import { RouterStub } from '../../shared/testing/router.stub';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import {
END_USER_AGREEMENT_METADATA_FIELD,
EndUserAgreementService
} from '../../core/end-user-agreement/end-user-agreement.service';
describe('CreateProfileComponent', () => {
let comp: CreateProfileComponent;
@@ -28,40 +32,80 @@ describe('CreateProfileComponent', () => {
let ePersonDataService: EPersonDataService;
let notificationsService;
let store: Store<CoreState>;
let endUserAgreementService: EndUserAgreementService;
const registration = Object.assign(new Registration(), {email: 'test@email.org', token: 'test-token'});
const values = {
metadata: {
'eperson.firstname': [
{
value: 'First'
}
],
'eperson.lastname': [
{
value: 'Last'
},
],
'eperson.phone': [
{
value: 'Phone'
}
],
'eperson.language': [
{
value: 'en'
}
]
},
email: 'test@email.org',
password: 'password',
canLogIn: true,
requireCertificate: false
};
const eperson = Object.assign(new EPerson(), values);
let values;
let eperson: EPerson;
let valuesWithAgreement;
let epersonWithAgreement: EPerson;
beforeEach(async(() => {
values = {
metadata: {
'eperson.firstname': [
{
value: 'First'
}
],
'eperson.lastname': [
{
value: 'Last'
},
],
'eperson.phone': [
{
value: 'Phone'
}
],
'eperson.language': [
{
value: 'en'
}
]
},
email: 'test@email.org',
password: 'password',
canLogIn: true,
requireCertificate: false
};
eperson = Object.assign(new EPerson(), values);
valuesWithAgreement = {
metadata: {
'eperson.firstname': [
{
value: 'First'
}
],
'eperson.lastname': [
{
value: 'Last'
},
],
'eperson.phone': [
{
value: 'Phone'
}
],
'eperson.language': [
{
value: 'en'
}
],
[END_USER_AGREEMENT_METADATA_FIELD]: [
{
value: 'true'
}
]
},
email: 'test@email.org',
password: 'password',
canLogIn: true,
requireCertificate: false
};
epersonWithAgreement = Object.assign(new EPerson(), valuesWithAgreement);
route = {data: observableOf({registration: registration})};
router = new RouterStub();
notificationsService = new NotificationsServiceStub();
@@ -74,6 +118,11 @@ describe('CreateProfileComponent', () => {
dispatch: {},
});
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
isCookieAccepted: false,
removeCookieAccepted: {}
});
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule],
declarations: [CreateProfileComponent],
@@ -84,6 +133,7 @@ describe('CreateProfileComponent', () => {
{provide: EPersonDataService, useValue: ePersonDataService},
{provide: FormBuilder, useValue: new FormBuilder()},
{provide: NotificationsService, useValue: notificationsService},
{provide: EndUserAgreementService, useValue: endUserAgreementService},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
@@ -131,6 +181,41 @@ describe('CreateProfileComponent', () => {
expect(notificationsService.success).toHaveBeenCalled();
});
describe('when the end-user-agreement cookie is accepted', () => {
beforeEach(() => {
(endUserAgreementService.isCookieAccepted as jasmine.Spy).and.returnValue(true);
});
it('should submit an eperson with agreement metadata for creation and log in on success', () => {
comp.firstName.patchValue('First');
comp.lastName.patchValue('Last');
comp.contactPhone.patchValue('Phone');
comp.language.patchValue('en');
comp.password = 'password';
comp.isInValidPassword = false;
comp.submitEperson();
expect(ePersonDataService.createEPersonForToken).toHaveBeenCalledWith(epersonWithAgreement, 'test-token');
expect(store.dispatch).toHaveBeenCalledWith(new AuthenticateAction('test@email.org', 'password'));
expect(router.navigate).toHaveBeenCalledWith(['/home']);
expect(notificationsService.success).toHaveBeenCalled();
});
it('should remove the cookie', () => {
comp.firstName.patchValue('First');
comp.lastName.patchValue('Last');
comp.contactPhone.patchValue('Phone');
comp.language.patchValue('en');
comp.password = 'password';
comp.isInValidPassword = false;
comp.submitEperson();
expect(endUserAgreementService.removeCookieAccepted).toHaveBeenCalled();
});
});
it('should submit an eperson for creation and stay on page on error', () => {
(ePersonDataService.createEPersonForToken as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 500, 'Error')));

View File

@@ -14,6 +14,10 @@ import { AuthenticateAction } from '../../core/auth/auth.actions';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { environment } from '../../../environments/environment';
import { isEmpty } from '../../shared/empty.util';
import {
END_USER_AGREEMENT_METADATA_FIELD,
EndUserAgreementService
} from '../../core/end-user-agreement/end-user-agreement.service';
/**
* Component that renders the create profile page to be used by a user registering through a token
@@ -41,7 +45,8 @@ export class CreateProfileComponent implements OnInit {
private router: Router,
private route: ActivatedRoute,
private formBuilder: FormBuilder,
private notificationsService: NotificationsService
private notificationsService: NotificationsService,
private endUserAgreementService: EndUserAgreementService
) {
}
@@ -137,6 +142,16 @@ export class CreateProfileComponent implements OnInit {
requireCertificate: false
};
// If the End User Agreement cookie is accepted, add end-user agreement metadata to the user
if (this.endUserAgreementService.isCookieAccepted()) {
values.metadata[END_USER_AGREEMENT_METADATA_FIELD] = [
{
value: String(true)
}
];
this.endUserAgreementService.removeCookieAccepted();
}
const eperson = Object.assign(new EPerson(), values);
this.ePersonDataService.createEPersonForToken(eperson, this.token).subscribe((response) => {
if (response.isSuccessful) {

View File

@@ -4,6 +4,7 @@ import { RegisterEmailComponent } from './register-email/register-email.componen
import { CreateProfileComponent } from './create-profile/create-profile.component';
import { ItemPageResolver } from '../+item-page/item-page.resolver';
import { RegistrationResolver } from '../register-email-form/registration.resolver';
import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard';
@NgModule({
imports: [
@@ -16,7 +17,8 @@ import { RegistrationResolver } from '../register-email-form/registration.resolv
{
path: ':token',
component: CreateProfileComponent,
resolve: {registration: RegistrationResolver}
resolve: {registration: RegistrationResolver},
canActivate: [EndUserAgreementCookieGuard]
}
])
],

View File

@@ -16,7 +16,8 @@ export class CookieServiceMock {
return this.cookies.get(name);
}
remove() {
remove(name) {
this.cookies.delete(name);
return jasmine.createSpy('remove');
}

View File

@@ -1170,6 +1170,30 @@
"info.end-user-agreement.accept": "I have read and I agree to the End User Agreement",
"info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement",
"info.end-user-agreement.accept.success": "Successfully updated the End User Agreement",
"info.end-user-agreement.breadcrumbs": "End User Agreement",
"info.end-user-agreement.buttons.cancel": "Cancel",
"info.end-user-agreement.buttons.save": "Save",
"info.end-user-agreement.head": "End User Agreement",
"info.end-user-agreement.title": "End User Agreement",
"info.privacy.breadcrumbs": "Privacy Statement",
"info.privacy.head": "Privacy Statement",
"info.privacy.title": "Privacy Statement",
"item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.",
"item.edit.authorizations.title": "Edit item's Policies",