From 208cac15c4babfde926ab67d20b6253df053b1ca Mon Sep 17 00:00:00 2001 From: Kyle Huckins Date: Thu, 7 Nov 2019 18:38:04 +0000 Subject: [PATCH] LP1849212: tidying up course reserves UIs - Remove RowFlair and ClassCallback from Course Reserves Grid. - Implement dialog to view course materials associated with a particular course in the course list admin UI. - Implement actions to associate and disassociate materials with a specific course. - Optionally apply temporary Call Number, Circ Modifier, Item Status, and Shelving Location when associating an item with a course. - Reapply original values of the above-mentioned fields when disassociating an item from a course. - Move disassociation code into Course Service. - Automatically disassociate items and return them to their original state when deleting a course. Signed-off-by: Kyle Huckins Signed-off-by: Jane Sandberg Signed-off-by: Michele Morgan Signed-off-by: Galen Charlton --- Open-ILS/examples/fm_IDL.xml | 8 + .../course-associate-material.component.html | 160 +++++++++++++++++++ .../course-associate-material.component.ts | 174 +++++++++++++++++++++ .../course-reserves/course-list.component.html | 7 +- .../course-reserves/course-list.component.ts | 95 +++++++---- .../course-reserves/course-reserves.module.ts | 9 +- .../admin/server/course-reserves/course.service.ts | 100 ++++++++++++ Open-ILS/src/sql/Pg/040.schema.asset.sql | 6 +- .../XXXX.schema.course-materials-module.sql | 6 +- 9 files changed, 531 insertions(+), 34 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course.service.ts diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 146f0a8152..4e678b4793 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3169,10 +3169,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html new file mode 100644 index 0000000000..488eea7191 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + {{entry.barcode()}} + + + + + + + {{entry._title}} + + + + + {{r.label}} + + \ No newline at end of file diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts new file mode 100644 index 0000000000..cb8eeeb317 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-associate-material.component.ts @@ -0,0 +1,174 @@ +import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core'; +import {Observable, Observer, of} from 'rxjs'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {AuthService} from '@eg/core/auth.service'; +import {NetService} from '@eg/core/net.service'; +import {EventService} from '@eg/core/event.service'; +import {OrgService} from '@eg/core/org.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {Pager} from '@eg/share/util/pager'; +import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {IdlObject, IdlService} from '@eg/core/idl.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {CourseService} from './course.service'; + +@Component({ + selector: 'eg-course-associate-material-dialog', + templateUrl: './course-associate-material.component.html' +}) + +export class CourseAssociateMaterialComponent extends DialogComponent { + + @ViewChild('materialsGrid', {static: true}) materialsGrid: GridComponent; + @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; + @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; + @ViewChild('successString', { static: true }) successString: StringComponent; + @ViewChild('failedString', { static: true }) failedString: StringComponent; + @ViewChild('differentLibraryString', { static: true }) differentLibraryString: StringComponent; + @Input() table_name = "Course Materials"; + @Input() barcodeInput: String; + @Input() relationshipInput: String; + @Input() tempCallNumber: String; + @Input() tempStatus: Number; + @Input() tempLocation: Number; + @Input() tempCircMod: String; + @Input() isModifyingStatus: Boolean; + @Input() isModifyingCircMod: Boolean; + @Input() isModifyingCallNumber: Boolean; + @Input() isModifyingLocation: Boolean; + currentCourse: IdlObject; + materials: any[]; + gridDataSource: GridDataSource; + + constructor( + private auth: AuthService, + private idl: IdlService, + private net: NetService, + private pcrud: PcrudService, + private org: OrgService, + private evt: EventService, + private modal: NgbModal, + private toast: ToastService, + private courseSvc: CourseService + ) { + super(modal); + this.gridDataSource = new GridDataSource(); + } + + ngOnInit() { + this.gridDataSource.getRows = (pager: Pager, sort: any[]) => { + return this.fetchMaterials(pager); + } + } + + deleteSelected(items) { + let item_ids = []; + items.forEach(item => { + this.gridDataSource.data.splice(this.gridDataSource.data.indexOf(item, 0), 1); + item_ids.push(item.id()) + }); + this.pcrud.search('acmcm', {course: this.currentCourse.id(), item: item_ids}).subscribe(material => { + material.isdeleted(true); + this.pcrud.autoApply(material).subscribe( + val => { + this.courseSvc.resetItemFields(material, this.currentCourse.owning_lib()); + console.debug('deleted: ' + val); + this.deleteSuccessString.current().then(str => this.toast.success(str)); + }, + err => { + this.deleteFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + }); + } + + associateItem(barcode, relationship) { + if (barcode) { + this.pcrud.search('acp', {barcode: barcode}, + {flesh: 3, flesh_fields: {acp: ['call_number']}}).subscribe(item => { + let material = this.idl.create('acmcm'); + material.item(item.id()); + material.course(this.currentCourse.id()); + if (relationship) material.relationship(relationship); + if (this.isModifyingStatus && this.tempStatus) { + material.original_status(item.status()); + item.status(this.tempStatus); + } + if (this.isModifyingLocation && this.tempLocation) { + material.original_location(item.location()); + item.location(this.tempLocation); + } + if (this.isModifyingCircMod) { + material.original_circ_modifier(item.circ_modifier()); + item.circ_modifier(this.tempCircMod); + if (!this.tempCircMod) item.circ_modifier(null); + } + if (this.isModifyingCallNumber) { + material.original_callnumber(item.call_number()); + } + this.pcrud.create(material).subscribe( + val => { + console.debug('created: ' + val); + let new_cn = item.call_number().label(); + if (this.tempCallNumber) new_cn = this.tempCallNumber; + this.courseSvc.updateItem(item, this.currentCourse.owning_lib(), new_cn, this.isModifyingCallNumber).then(res => { + this.fetchItem(item.id(), relationship); + if (item.circ_lib() != this.currentCourse.owning_lib()) { + this.differentLibraryString.current().then(str => this.toast.warning(str)); + } else { + this.successString.current().then(str => this.toast.success(str)); + } + }); + + // Cleaning up inputs + this.barcodeInput = ""; + this.relationshipInput = ""; + this.tempStatus = null; + this.tempCircMod = null; + this.tempCallNumber = null; + this.tempLocation = null; + this.isModifyingCallNumber = false; + this.isModifyingCircMod = false; + this.isModifyingLocation = false; + this.isModifyingStatus = false; + }, err => { + this.failedString.current().then(str => this.toast.danger(str)); + }); + }); + } + } + + fetchMaterials(pager: Pager): Observable { + return new Observable(observer => { + this.materials.forEach(material => { + this.fetchItem(material.item, material.relationship); + }); + observer.complete(); + }); + } + + fetchItem(itemId, relationship): Promise { + return new Promise((resolve, reject) => { + this.net.request( + 'open-ils.circ', + 'open-ils.circ.copy_details.retrieve', + this.auth.token(), itemId + ).subscribe(res => { + if (res) { + let item = res.copy; + item.call_number(res.volume); + item.circ_lib(this.org.get(item.circ_lib())); + item._title = res.mvr.title(); + item._relationship = relationship; + this.gridDataSource.data.push(item); + } + }, err => { + reject(err); + }, () => resolve(this.gridDataSource.data)); + }); + } +} \ No newline at end of file diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.html index 471a94e02f..eb01af8112 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.html @@ -7,16 +7,17 @@ + +
+ + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.ts index 9e4bb40878..ea5dc2fc3f 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-list.component.ts @@ -1,13 +1,20 @@ import {Component, Input, ViewChild, OnInit} from '@angular/core'; import {IdlObject} from '@eg/core/idl.service'; import {PcrudService} from '@eg/core/pcrud.service'; +import {AuthService} from '@eg/core/auth.service'; +import {CourseService} from './course.service'; +import {NetService} from '@eg/core/net.service'; +import {OrgService} from '@eg/core/org.service'; import {GridComponent} from '@eg/share/grid/grid.component'; import {Pager} from '@eg/share/util/pager'; -import {GridDataSource, GridColumn, GridRowFlairEntry} from '@eg/share/grid/grid'; +import {GridDataSource, GridColumn} from '@eg/share/grid/grid'; import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; import {StringComponent} from '@eg/share/string/string.component'; import {ToastService} from '@eg/share/toast/toast.service'; +import {CourseAssociateMaterialComponent + } from './course-associate-material.component'; + @Component({ templateUrl: './course-list.component.html' }) @@ -22,23 +29,27 @@ export class CourseListComponent implements OnInit { @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent; @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent; @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent; - @ViewChild('flairTooltip', { static: true }) private flairTooltip: StringComponent; - rowFlairCallback: (row: any) => GridRowFlairEntry; + @ViewChild('courseMaterialDialog', {static: true}) + private courseMaterialDialog: CourseAssociateMaterialComponent; @Input() sort_field: string; @Input() idl_class = "acmc"; @Input() dialog_size: 'sm' | 'lg' = 'lg'; @Input() table_name = "Course"; grid_source: GridDataSource = new GridDataSource(); + currentMaterials: any[] = []; search_value = ''; constructor( - private pcrud: PcrudService, - private toast: ToastService, + private auth: AuthService, + private courseSvc: CourseService, + private net: NetService, + private org: OrgService, + private pcrud: PcrudService, + private toast: ToastService, ){} ngOnInit() { this.getSource(); - this.rowFlair(); } /** @@ -63,24 +74,6 @@ export class CourseListComponent implements OnInit { }; } - rowFlair() { - this.rowFlairCallback = (row: any): GridRowFlairEntry => { - const flair = {icon: null, title: null}; - if (row.id() < 100) { - flair.icon = 'not_interested'; - flair.title = this.flairTooltip.text; - } - return flair; - }; - } - - gridCellClassCallback = (row: any, col: GridColumn): string => { - if (col.name === 'id' && row.a[0] < 100) { - return 'text-danger'; - } - return ''; - } - showEditDialog(standingPenalty: IdlObject): Promise { this.editDialog.mode = 'update'; this.editDialog.recordId = standingPenalty['id'](); @@ -131,7 +124,11 @@ export class CourseListComponent implements OnInit { } deleteSelected(idl_object: IdlObject[]) { - idl_object.forEach(idl_object => idl_object.isdeleted(true)); + this.courseSvc.disassociateMaterials(idl_object).then(res => { + console.log(res); + idl_object.forEach(idl_object => { + idl_object.isdeleted(true) + }); this.pcrud.autoApply(idl_object).subscribe( val => { console.debug('deleted: ' + val); @@ -142,8 +139,52 @@ export class CourseListComponent implements OnInit { this.deleteFailedString.current() .then(str => this.toast.danger(str)); }, - () => this.grid.reload() + () => this.grid.reload() ); - }; + }); + }; + + fetchCourseMaterials(course, currentMaterials): Promise { + return new Promise((resolve, reject) => { + this.pcrud.search('acmcm', {course: course}).subscribe(res => { + if (res) this.fleshItemDetails(res.item(), res.relationship()); + }, err => { + reject(err); + }, () => resolve(this.courseMaterialDialog.gridDataSource.data)); + }); + } + + fleshItemDetails(itemId, relationship): Promise { + return new Promise((resolve, reject) => { + this.net.request( + 'open-ils.circ', + 'open-ils.circ.copy_details.retrieve', + this.auth.token(), itemId + ).subscribe(res => { + if (res) { + let item = res.copy; + item.call_number(res.volume); + item._title = res.mvr.title(); + item.circ_lib(this.org.get(item.circ_lib())); + item._relationship = relationship; + this.courseMaterialDialog.gridDataSource.data.push(item); + } + }, err => { + reject(err); + }, () => resolve(this.courseMaterialDialog.gridDataSource.data)); + }); + } + + openMaterialsDialog(course) { + let currentMaterials = [] + this.courseMaterialDialog.gridDataSource.data = []; + this.fetchCourseMaterials(course[0].id(), currentMaterials).then(res => { + this.courseMaterialDialog.currentCourse = course[0]; + this.courseMaterialDialog.materials = currentMaterials; + this.courseMaterialDialog.open({size: 'lg'}).subscribe(res => { + console.log(res); + }); + }); + } } diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-reserves.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-reserves.module.ts index 1702ba861e..6f6eed594a 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-reserves.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course-reserves.module.ts @@ -2,20 +2,25 @@ import {NgModule} from '@angular/core'; import {TreeModule} from '@eg/share/tree/tree.module'; import {AdminCommonModule} from '@eg/staff/admin/common.module'; import {CourseListComponent} from './course-list.component'; +import {CourseAssociateMaterialComponent} from './course-associate-material.component'; import {CourseReservesRoutingModule} from './routing.module'; - +import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-location-select.module'; +import {CourseService} from './course.service' @NgModule({ declarations: [ - CourseListComponent + CourseListComponent, + CourseAssociateMaterialComponent ], imports: [ AdminCommonModule, CourseReservesRoutingModule, + ItemLocationSelectModule, TreeModule ], exports: [ ], providers: [ + CourseService ] }) diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course.service.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course.service.ts new file mode 100644 index 0000000000..c8b60cc15d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/course-reserves/course.service.ts @@ -0,0 +1,100 @@ +import {AuthService} from '@eg/core/auth.service'; +import {EventService} from '@eg/core/event.service'; +import {IdlObject, IdlService} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {PcrudService} from '@eg/core/pcrud.service'; + +export class CourseService { + + constructor( + private auth: AuthService, + private evt: EventService, + private idl: IdlService, + private net: NetService, + private pcrud: PcrudService + ) {} + + disassociateMaterials(courses) { + return new Promise((resolve, reject) => { + let course_ids = []; + let course_library_hash = {}; + courses.forEach(course => { + course_ids.push(course.id()); + course_library_hash[course.id()] = course.owning_lib(); + }); + this.pcrud.search('acmcm', {course: course_ids}).subscribe(material => { + material.isdeleted(true); + this.resetItemFields(material, course_library_hash[material.course()]); + this.pcrud.autoApply(material).subscribe(res => { + console.log(res); + }, err => { + reject(err); + }, () => { + resolve(material); + }); + }, err => { + reject(err) + }, () => { + resolve(courses); + }); + }); + } + + resetItemFields(material, course_lib) { + this.pcrud.retrieve('acp', material.item(), + {flesh: 3, flesh_fields: {acp: ['call_number']}}).subscribe(copy => { + if (material.original_status()) { + copy.status(material.original_status()); + } + if (copy.circ_modifier() != material.original_circ_modifier()) { + copy.circ_modifier(material.original_circ_modifier()); + } + if (material.original_location()) { + copy.location(material.original_location()); + } + if (material.original_callnumber()) { + this.pcrud.retrieve('acn', material.original_callnumber()).subscribe(cn => { + this.updateItem(copy, course_lib, cn.label(), true); + }); + } else { + this.updateItem(copy, course_lib, copy.call_number().label(), false); + } + }); + } + + updateItem(item: IdlObject, course_lib, call_number, updatingVolume) { + return new Promise((resolve, reject) => { + this.pcrud.update(item).subscribe(item_id => { + if (updatingVolume) { + let cn = item.call_number(); + return this.net.request( + 'open-ils.cat', 'open-ils.cat.call_number.find_or_create', + this.auth.token(), call_number, cn.record(), + course_lib, cn.prefix(), cn.suffix(), + cn.label_class() + ).subscribe(res => { + let event = this.evt.parse(res); + if (event) return; + return this.net.request( + 'open-ils.cat', 'open-ils.cat.transfer_copies_to_volume', + this.auth.token(), res.acn_id, [item.id()] + ).subscribe(transfered_res => { + console.debug("Copy transferred to volume with code " + transfered_res); + }, err => { + reject(err); + }, () => { + resolve(item); + }); + }, err => { + reject(err); + }, () => { + resolve(item); + }); + } else { + return this.pcrud.update(item); + } + }); + }); + } + +} \ No newline at end of file diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql index 2d21799560..2091a693fc 100644 --- a/Open-ILS/src/sql/Pg/040.schema.asset.sql +++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql @@ -1124,7 +1124,11 @@ CREATE TABLE asset.course_module_course_materials ( id SERIAL PRIMARY KEY, course INT NOT NULL REFERENCES asset.course_module_course (id), item INT NOT NULL REFERENCES asset.copy (id), - relationship TEXT + relationship TEXT, + original_location INT REFERENCES asset.copy_location, + original_status INT REFERENCES config.copy_status, + original_circ_modifier INT REFERENCES config.circ_modifier, + original_callnumber INT REFERENCES asset.call_number ); CREATE TABLE asset.course_module_non_cat_course_materials ( diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.course-materials-module.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.course-materials-module.sql index 23dd2729f7..8f813f2be4 100644 --- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.course-materials-module.sql +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.course-materials-module.sql @@ -21,7 +21,11 @@ CREATE TABLE asset.course_module_course_materials ( id SERIAL PRIMARY KEY, course INT NOT NULL REFERENCES asset.course_module_course (id), item INT NOT NULL REFERENCES asset.copy (id), - relationship TEXT + relationship TEXT, + original_location INT REFERENCES asset.copy_location, + original_status INT REFERENCES config.copy_status, + original_circ_modifier TEXT REFERENCES config.circ_modifier, + original_callnumber INT REFERENCES asset.call_number ); CREATE TABLE asset.course_module_non_cat_course_materials ( -- 2.11.0