From 5e070cb9ab069ce9cfbe35fbc168e0411040896a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Marie=CC=81thoz?= Date: Wed, 4 Nov 2020 08:43:19 +0100 Subject: [PATCH] patron: a professional can change a user password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds missing license in some files. * Fixes some spelling. * Adds a button in the circulation patron profile to update the password. * Adds a edit button in the circulation patron profile. * Moves roles in the patron informations in the patron circulation profile and the patron detail view. * Searches a patron in every fields in the checkout view. Co-Authored-by: Johnny MariƩthoz --- .../circulation/checkin/checkin.component.ts | 19 ++- .../src/app/circulation/circulation.module.ts | 13 +- .../change-password-form.component.html | 36 +++++ .../change-password-form.component.spec.ts | 53 +++++++ .../change-password-form.component.ts | 136 ++++++++++++++++++ .../circulation/patron/loan/loan.component.ts | 7 +- ...tron-transaction-event-form.component.html | 16 +++ .../patron-transactions.component.html | 16 +++ .../patron/profile/profile.component.html | 36 +++-- .../patron/profile/profile.component.ts | 72 ++++++++-- .../patron-detail-view.component.html | 27 ++-- .../admin/src/app/service/user.service.ts | 40 ++++-- 12 files changed, 416 insertions(+), 55 deletions(-) create mode 100644 projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.html create mode 100644 projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.spec.ts create mode 100644 projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts diff --git a/projects/admin/src/app/circulation/checkin/checkin.component.ts b/projects/admin/src/app/circulation/checkin/checkin.component.ts index d793cf091..a78d9a402 100644 --- a/projects/admin/src/app/circulation/checkin/checkin.component.ts +++ b/projects/admin/src/app/circulation/checkin/checkin.component.ts @@ -179,12 +179,21 @@ export class CheckinComponent implements OnInit { if (barcode) { this.isLoading = true; this._recordService - .getRecords('patrons', `barcode:${barcode}`, 1, 1) + .getRecords('patrons', `${barcode}`, 1, 2, [], {simple: 1}) .pipe( - map((response: any) => (this._recordService.totalHits(response.hits.total) === 0) - ? null - : response.hits.hits[0].metadata - ) + map((response: any) => { + const total = this._recordService.totalHits(response.hits.total); + if (total === 0) { + return null; + } + if (total > 1) { + this._toastService.warning( + this._translate.instant('Found more than one patron.'), + this._translate.instant('Checkin') + ); + } + return response.hits.hits[0].metadata; + }) ).subscribe( patron => { if ( diff --git a/projects/admin/src/app/circulation/circulation.module.ts b/projects/admin/src/app/circulation/circulation.module.ts index 4bb522cc0..5a1ee9075 100644 --- a/projects/admin/src/app/circulation/circulation.module.ts +++ b/projects/admin/src/app/circulation/circulation.module.ts @@ -21,8 +21,8 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormlyModule } from '@ngx-formly/core'; import { RecordModule } from '@rero/ng-core'; -import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { CollapseModule } from 'ngx-bootstrap/collapse'; +import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { SharedPipesModule } from '../shared/shared-pipes.module'; import { CheckinComponent } from './checkin/checkin.component'; import { CirculationRoutingModule } from './circulation-routing.module'; @@ -30,6 +30,7 @@ import { ItemComponent } from './item/item.component'; import { ItemsListComponent } from './items-list/items-list.component'; import { MainRequestComponent } from './main-request/main-request.component'; import { CardComponent } from './patron/card/card.component'; +import { ChangePasswordFormComponent } from './patron/change-password-form/change-password-form.component'; import { HistoryItemComponent } from './patron/history/history-item/history-item.component'; import { HistoryComponent } from './patron/history/history.component'; import { LoanComponent } from './patron/loan/loan.component'; @@ -46,11 +47,11 @@ import { } from './patron/patron-transactions/patron-transaction/overdue-transaction/overdue-transaction.component'; import { PatronTransactionComponent } from './patron/patron-transactions/patron-transaction/patron-transaction.component'; import { PatronTransactionsComponent } from './patron/patron-transactions/patron-transactions.component'; +import { PendingItemComponent } from './patron/pending/pending-item/pending-item.component'; +import { PendingComponent } from './patron/pending/pending.component'; import { PickupItemComponent } from './patron/pickup/pickup-item/pickup-item.component'; import { PickupComponent } from './patron/pickup/pickup.component'; import { ProfileComponent } from './patron/profile/profile.component'; -import { PendingItemComponent } from './patron/pending/pending-item/pending-item.component'; -import { PendingComponent } from './patron/pending/pending.component'; import { RequestedItemsListComponent } from './requested-items-list/requested-items-list.component'; @@ -77,7 +78,8 @@ import { RequestedItemsListComponent } from './requested-items-list/requested-it DefaultTransactionComponent, PatronTransactionEventFormComponent, HistoryComponent, - HistoryItemComponent + HistoryItemComponent, + ChangePasswordFormComponent ], imports: [ CommonModule, @@ -91,7 +93,8 @@ import { RequestedItemsListComponent } from './requested-items-list/requested-it SharedPipesModule ], entryComponents: [ - PatronTransactionEventFormComponent + PatronTransactionEventFormComponent, + ChangePasswordFormComponent ] }) export class CirculationModule { } diff --git a/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.html b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.html new file mode 100644 index 000000000..cd08d8180 --- /dev/null +++ b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.html @@ -0,0 +1,36 @@ + + diff --git a/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.spec.ts b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.spec.ts new file mode 100644 index 000000000..f13fb07a9 --- /dev/null +++ b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.spec.ts @@ -0,0 +1,53 @@ +/* + * RERO ILS UI + * Copyright (C) 2020 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { CirculationModule } from '../../circulation.module'; +import { ChangePasswordFormComponent } from './change-password-form.component'; + + +describe('ChangePasswordFormComponent', () => { + let component: ChangePasswordFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + HttpClientTestingModule, + CirculationModule + ], + providers: [ + BsModalRef + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChangePasswordFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts new file mode 100644 index 000000000..80f89d52a --- /dev/null +++ b/projects/admin/src/app/circulation/patron/change-password-form/change-password-form.component.ts @@ -0,0 +1,136 @@ +/* + * RERO ILS UI + * Copyright (C) 2020 RERO + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { TranslateService } from '@ngx-translate/core'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { ToastrService } from 'ngx-toastr'; +import { User } from '../../../class/user'; +import { UserService } from '../../../service/user.service'; + +@Component({ + selector: 'admin-change-password-form', + templateUrl: './change-password-form.component.html' +}) +export class ChangePasswordFormComponent implements OnInit { + + /** patron to change the password */ + patron: User; + + /** form */ + form: FormGroup = new FormGroup({}); + + /** model */ + model = {}; + + /** form fields */ + formFields: FormlyFieldConfig[]; + + /** + * Constructor + * @param _modalService - BsModalService + * @param _bsModalRef - BsModalRef + * @param _translateService - TranslateService + * @param _toastr - ToastrService + * @param _userService - UserService + */ + constructor( + private _modalService: BsModalService, + private _bsModalRef: BsModalRef, + private _translateService: TranslateService, + private _toastr: ToastrService, + private _userService: UserService + ) { } + + /** + * Component initialization. + */ + ngOnInit() { + const initialState: any = this._modalService.config.initialState; + if (initialState.hasOwnProperty('patron')) { + this.closeModal(); + } + this.patron = initialState.patron; + this._initForm(); + } + + /** + * Submit form + * @param model - Object + */ + submit(patron, model) { + this._userService.changePassword(patron.username, model.password).subscribe( + () => { + this._toastr.success( + this._translateService.instant('The patron password has been changed.'), + ); + this.closeModal(); + }, + (resp) => { + console.log('Error: Update Patron Password', resp); + let error = this._translateService.instant('An error has occurred.'); + if (resp.error && resp.error.message) { + error = `${error}: (${resp.error.message})`; + } + this._toastr.error( + error, + this._translateService.instant('Update Patron Password'), + { disableTimeOut: true } + ); + this.closeModal(); + } + ); + } + + /** + * Initialize formly form. + */ + private _initForm() { + if (this.patron) { + this.formFields = [ + { + key: 'password', + type: 'input', + focus: true, + templateOptions: { + type: 'password', + label: this._translateService.instant('New password'), + required: true, + // same as Invenio + minLength: 6, + maxLength: 128, + keydown: (field, event) => { + if (event.key === 'Enter') { + event.preventDefault(); + } + } + } + } + ]; + } + } + + /** + * Close modal dialog + * @param event - Event + */ + closeModal() { + this._bsModalRef.hide(); + } +} diff --git a/projects/admin/src/app/circulation/patron/loan/loan.component.ts b/projects/admin/src/app/circulation/patron/loan/loan.component.ts index 43c74c135..8533757fe 100644 --- a/projects/admin/src/app/circulation/patron/loan/loan.component.ts +++ b/projects/admin/src/app/circulation/patron/loan/loan.component.ts @@ -56,7 +56,8 @@ export class LoanComponent implements OnInit, OnDestroy { /** Library PID of the logged user */ currentLibraryPid: string; - private _subcription = new Subscription(); + /** Observable subscription */ + private _subscription = new Subscription(); /** * Constructor @@ -77,7 +78,7 @@ export class LoanComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this._subcription.add(this._patronService.currentPatron$.subscribe(patron => { + this._subscription.add(this._patronService.currentPatron$.subscribe(patron => { this.patron = patron; if (patron) { this.isLoading = true; @@ -101,7 +102,7 @@ export class LoanComponent implements OnInit, OnDestroy { } ngOnDestroy() { - this._subcription.unsubscribe(); + this._subscription.unsubscribe(); } diff --git a/projects/admin/src/app/circulation/patron/patron-transactions/patron-transaction-event-form/patron-transaction-event-form.component.html b/projects/admin/src/app/circulation/patron/patron-transactions/patron-transaction-event-form/patron-transaction-event-form.component.html index 781b77cd2..a4ce20dd3 100644 --- a/projects/admin/src/app/circulation/patron/patron-transactions/patron-transaction-event-form/patron-transaction-event-form.component.html +++ b/projects/admin/src/app/circulation/patron/patron-transactions/patron-transaction-event-form/patron-transaction-event-form.component.html @@ -1,3 +1,19 @@ +
diff --git a/projects/admin/src/app/circulation/patron/profile/profile.component.ts b/projects/admin/src/app/circulation/patron/profile/profile.component.ts index d1b2f1bf6..bd1c8dd87 100644 --- a/projects/admin/src/app/circulation/patron/profile/profile.component.ts +++ b/projects/admin/src/app/circulation/patron/profile/profile.component.ts @@ -14,26 +14,82 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { BsModalService } from 'ngx-bootstrap/modal'; +import { Subscription } from 'rxjs'; import { PatronService } from '../../../service/patron.service'; +import { RecordPermissionService } from '../../../service/record-permission.service'; +import { ChangePasswordFormComponent } from '../change-password-form/change-password-form.component'; @Component({ selector: 'admin-profile', templateUrl: './profile.component.html' }) -export class ProfileComponent { +export class ProfileComponent implements OnInit, OnDestroy { /** - * Get current patron - * @return Observable + * Current patron */ - get currentPatron$() { - return this._patronService.currentPatron$; - } + currentPatron$; + + /** Observable subscription */ + private _subscription = new Subscription(); + + /** Patron permission */ + private _permissions; /** * Constructor * @param _patronService - PatronService + * @param _modalService - BsModalService + * @param _recordPermission - RecordPermissionService + */ + constructor( + private _patronService: PatronService, + private _modalService: BsModalService, + private _recordPermission: RecordPermissionService + ) { + } + + /** + * Component initialization. + */ + ngOnInit() { + this.currentPatron$ = this._patronService.currentPatron$; + this._subscription = this.currentPatron$.subscribe(patron => { + if (patron && patron.pid) { + this._recordPermission.getPermission('patrons', patron.pid).subscribe( + perm => { + this._permissions = perm; + } + ); + } + }); + } + + /** + * Check the update permission. + * + * @returns True if the logged user can edit the current patron. */ - constructor(private _patronService: PatronService) { } + canUpdate() { + return this._permissions && this._permissions.update && this._permissions.update.can === true; + } + + /** Component destroy */ + ngOnDestroy() { + this._subscription.unsubscribe(); + } + + /** + * Open a modal dialog with the new password. + * + * @param patron - Patron the patron to update the password. + */ + updatePatronPassword(patron) { + const initialState = { + patron + }; + this._modalService.show(ChangePasswordFormComponent, { initialState }); + } } diff --git a/projects/admin/src/app/record/detail-view/patron-detail-view/patron-detail-view.component.html b/projects/admin/src/app/record/detail-view/patron-detail-view/patron-detail-view.component.html index d2ccb0536..e66808fb5 100644 --- a/projects/admin/src/app/record/detail-view/patron-detail-view/patron-detail-view.component.html +++ b/projects/admin/src/app/record/detail-view/patron-detail-view/patron-detail-view.component.html @@ -77,18 +77,6 @@

{{ patron.last_name }} {{ patron.first_name }}

{{ patron.email }} - -
-
- Role - Roles: -
-
- - {{ role | translate }}{{ last ? '' : ', ' }} - -
-
Librarian Information
@@ -113,6 +101,18 @@
Patron Information
{{ patron.patron.barcode }} + +
+
+ Role + Roles: +
+
+ + {{ role | translate }}{{ last ? '' : ', ' }} + +
+
@@ -134,7 +134,8 @@
Patron Information
{{ 'Affiliation libraries' | translate }}:
-
{{library.pid | getRecord: 'libraries' : 'field' : 'name' | async}}
+
+ {{library.pid | getRecord: 'libraries' : 'field' : 'name' | async}}
diff --git a/projects/admin/src/app/service/user.service.ts b/projects/admin/src/app/service/user.service.ts index c8cc9e9d9..8f8733346 100644 --- a/projects/admin/src/app/service/user.service.ts +++ b/projects/admin/src/app/service/user.service.ts @@ -17,7 +17,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RecordService } from '@rero/ng-core'; +import { ApiService, RecordService } from '@rero/ng-core'; import { forkJoin, of, Subject } from 'rxjs'; import { concatAll, map } from 'rxjs/operators'; import { User } from '../class/user'; @@ -46,11 +46,17 @@ export class UserService { return this._allowInterfaceAccess; } + /** API Prefix, i.e. /api. */ + private _apiPrefix = ''; + constructor( - private http: HttpClient, - private recordService: RecordService, - private _appConfigService: AppConfigService - ) { } + private _http: HttpClient, + private _recordService: RecordService, + private _appConfigService: AppConfigService, + private _apiService: ApiService + ) { + this._apiPrefix = this._apiService.endpointPrefix; + } getCurrentUser() { return this.user; @@ -65,7 +71,7 @@ export class UserService { } public loadLoggedUser() { - this.http.get(User.LOGGED_URL).subscribe(data => { + this._http.get(User.LOGGED_URL).subscribe(data => { const user = data.metadata; if (user && user.library) { user.currentLibrary = user.library.pid; @@ -77,14 +83,32 @@ export class UserService { }); } + /** + * Update a patron password. + * + * @param username - patron username + * @param password - new password + */ + public changePassword(username, password) { + const data = { + username, + new_password: password + }; + const url = `${this._apiPrefix}/change-password`; + return this._http.post(url, data).pipe( + map(result => console.log(result)) + ); + } + + getUser(pid: string) { - return this.recordService.getRecord('patrons', pid, 1).pipe( + return this._recordService.getRecord('patrons', pid, 1).pipe( map(data => { if (data) { const patron = new User(data.metadata); return forkJoin( of(patron), - this.recordService.getRecord('patron_types', patron.patron.type.pid) + this._recordService.getRecord('patron_types', patron.patron.type.pid) ).pipe( map(patronAndType => { const newPatron = patronAndType[0];