From 93873c15178a9e4c43e525d565e209922d0ba9c2 Mon Sep 17 00:00:00 2001 From: Jane Sandberg Date: Wed, 15 Jul 2020 00:45:59 -0700 Subject: [PATCH] LP1849212: Users can attach brief bib records and e-resources to courses Signed-off-by: Jane Sandberg Signed-off-by: Michele Morgan Signed-off-by: Galen Charlton --- Open-ILS/examples/fm_IDL.xml | 28 +- Open-ILS/examples/opensrf.xml.example | 21 + Open-ILS/examples/opensrf_core.xml.example | 1 + .../course-associate-material.component.html | 383 ++++++++++-------- .../course-associate-material.component.ts | 90 ++-- .../course-reserves/course-reserves.module.ts | 4 +- .../app/staff/share/marc-edit/marcrecord.ts | 1 + .../simplified-editor-field.component.ts | 42 ++ .../simplified-editor.component.html | 21 + .../simplified-editor.component.ts | 72 ++++ .../simplified-editor.module.ts | 27 ++ .../staff/share/marc-edit/tagtable.service.ts | 7 + .../perlmods/lib/OpenILS/Application/Circ.pm | 155 ------- .../lib/OpenILS/Application/Courses.pm | 235 +++++++++++ .../lib/OpenILS/WWW/EGCatLoader/Course.pm | 14 +- .../lib/OpenILS/WWW/EGCatLoader/Record.pm | 8 +- .../lib/OpenILS/WWW/EGCatLoader/Search.pm | 10 +- Open-ILS/src/perlmods/live_t/30-courses.t | 23 ++ Open-ILS/src/sql/Pg/040.schema.asset.sql | 13 +- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 19 + .../XXXX.schema.course-materials-module.sql | 41 +- .../src/templates/opac/parts/course/body.tt2 | 44 +- 22 files changed, 830 insertions(+), 429 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor-field.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.module.ts create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/Courses.pm create mode 100644 Open-ILS/src/perlmods/live_t/30-courses.t diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 9e896c9765..8c76cab9b2 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3127,7 +3127,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - @@ -3171,6 +3170,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -3201,32 +3201,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index eb875b1116..05496433d4 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -1270,6 +1270,26 @@ vim:et:ts=4:sw=4: 60 + + + 5 + 1 + perl + OpenILS::Application::Courses + 100 + + courses_unix.sock + courses_unix.pid + courses_unix.log + 100 + 1 + 15 + 1 + 5 + + + + @@ -1317,6 +1337,7 @@ vim:et:ts=4:sw=4: open-ils.serial open-ils.hold-targeter open-ils.ebook_api + open-ils.courses diff --git a/Open-ILS/examples/opensrf_core.xml.example b/Open-ILS/examples/opensrf_core.xml.example index 45e5f5a4f8..e801db910f 100644 --- a/Open-ILS/examples/opensrf_core.xml.example +++ b/Open-ILS/examples/opensrf_core.xml.example @@ -27,6 +27,7 @@ Example OpenSRF bootstrap configuration file for Evergreen open-ils.cat open-ils.circ open-ils.collections + open-ils.courses open-ils.fielder open-ils.pcrud open-ils.permacrud diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.html index a34e757679..724545a030 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.html @@ -1,196 +1,255 @@ - - + + - + + - -
-
-
-
-
-
-
- Barcode -
- -
-
-
-
-
- Relationship + +
+
+ + + +
+
+
+
+ Barcode +
+ +
+
+
+
+
+ Relationship +
+ +
+
- -
-
-
-
-
- -
-
-
-
-
The following fields will be applied to the material - added, and reverted once the course is no longer associated - with the material.
-
-
-
-
-
-
-
- Call Number +
+
+
- -
-
- +
+
+
The following fields will be applied to the material + added, and reverted once the course is no longer associated + with the material.
-
-
-
-
-
-
- Circulation Modifier +
+
+
+
+
+ Call Number +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ Circulation Modifier +
+
+ + +
+
+ +
+
+
- - -
-
- +
+
+
+
+
+ Item Status +
+
+ + +
+
+ +
+
+
+
+
+
+
+
+ Shelving Location +
+
+ + +
+
+ +
+
+
-
-
-
-
-
-
-
-
- Item Status + + + + +
+
+
+ Relationship +
+
- - -
-
- +
+
+
+ Relationship +
+
-
-
-
-
-
-
- Shelving Location + + + + + + + + + + + +
+
+
+
+ Relationship +
+ +
+
+
+
+
+ +
+ +
- - -
-
- +
+
+
-
-
-
-
+ + + -
- - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + +
-
- - - {{entry.barcode()}} - - + + + {{entry.item().barcode()}} + + - - - {{entry._title}} + + {{entry.record().wide_display_entry().title()}} - -{{r.label}} + {{r.label}} @@ -199,9 +258,7 @@ - - \ No newline at end of file + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.ts index 749bf9624a..b50634f52d 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.ts @@ -1,6 +1,7 @@ import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core'; -import {Router, ActivatedRoute} from '@angular/router'; -import {Observable, Observer, of} from 'rxjs'; +import {ActivatedRoute} from '@angular/router'; +import {from, Observable} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; import {DialogComponent} from '@eg/share/dialog/dialog.component'; import {AuthService} from '@eg/core/auth.service'; import {NetService} from '@eg/core/net.service'; @@ -13,7 +14,6 @@ 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 {StaffBannerComponent} from '@eg/staff/share/staff-banner.component'; import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; import {ToastService} from '@eg/share/toast/toast.service'; import {CourseService} from '@eg/staff/share/course.service'; @@ -23,7 +23,7 @@ import {CourseService} from '@eg/staff/share/course.service'; templateUrl: './course-associate-material.component.html' }) -export class CourseAssociateMaterialComponent extends DialogComponent { +export class CourseAssociateMaterialComponent extends DialogComponent implements OnInit { @Input() currentCourse: IdlObject; @Input() courseId: any; @Input() displayMode: String; @@ -55,6 +55,10 @@ export class CourseAssociateMaterialComponent extends DialogComponent { @Input() isModifyingCircMod: Boolean; @Input() isModifyingCallNumber: Boolean; @Input() isModifyingLocation: Boolean; + bibId: number; + + associateBriefRecord: (newRecord: string) => void; + associateElectronicBibRecord: () => void; constructor( private auth: AuthService, @@ -70,31 +74,53 @@ export class CourseAssociateMaterialComponent extends DialogComponent { ) { super(modal); this.materialsDataSource = new GridDataSource(); + + this.materialsDataSource.getRows = (pager: Pager, sort: any[]) => { + return this.net.request( + 'open-ils.courses', + 'open-ils.courses.course_materials.retrieve.fleshed', + {course: this.courseId} + ); + }; } ngOnInit() { - this.materialsDataSource.getRows = (pager: Pager, sort: any[]) => { - return this.loadMaterialsGrid(pager); - } + this.associateBriefRecord = (newRecord: string) => { + return this.net.request( + 'open-ils.courses', + 'open-ils.courses.attach.biblio_record', + this.auth.token(), + newRecord, + this.courseId, + this.relationshipInput + ).subscribe(() => { + this.materialsGrid.reload(); + this.materialAddSuccessString.current() + .then(str => this.toast.success(str)); + }); + }; + + this.associateElectronicBibRecord = () => { + return this.net.request( + 'open-ils.courses', + 'open-ils.courses.attach.electronic_resource', + this.auth.token(), + this.bibId, + this.courseId, + this.relationshipInput + ).subscribe(() => { + this.materialsGrid.reload(); + this.materialAddSuccessString.current() + .then(str => this.toast.success(str)); + }); + }; + } isDialog(): boolean { return this.displayMode === 'dialog'; } - loadMaterialsGrid(pager: Pager): Observable { - return new Observable(observer => { - this.course.getMaterials(this.courseId).then(materials => { - materials.forEach(material => { - this.course.fleshMaterial(material).then(fleshed_material => { - this.materialsDataSource.data.push(fleshed_material); - }); - }); - }); - observer.complete(); - }); - } - editSelectedMaterials(itemFields: IdlObject[]) { // Edit each IDL thing one at a time const editOneThing = (item: IdlObject) => { @@ -116,7 +142,7 @@ export class CourseAssociateMaterialComponent extends DialogComponent { this.materialEditSuccessString.current() .then(str => this.toast.success(str)); this.pcrud.retrieve('acmcm', result).subscribe(material => { - if (material.course() != this.courseId) { + if (material.course() !== this.courseId) { this.materialsDataSource.data.splice( this.materialsDataSource.data.indexOf(course_material, 0), 1 ); @@ -134,10 +160,10 @@ export class CourseAssociateMaterialComponent extends DialogComponent { ); }); } - + associateItem(barcode, relationship) { if (barcode) { - let args = { + const args = { barcode: barcode, relationship: relationship, isModifyingCallNumber: this.isModifyingCallNumber, @@ -148,24 +174,22 @@ export class CourseAssociateMaterialComponent extends DialogComponent { tempLocation: this.tempLocation, tempStatus: this.tempStatus, currentCourse: this.currentCourse - } + }; this.barcodeInput = null; this.pcrud.search('acp', {barcode: args.barcode}, { flesh: 3, flesh_fields: {acp: ['call_number']} }).subscribe(item => { - let associatedMaterial = this.course.associateMaterials(item, args); + const associatedMaterial = this.course.associateMaterials(item, args); associatedMaterial.material.then(res => { item = associatedMaterial.item; let new_cn = item.call_number().label(); - if (this.tempCallNumber) new_cn = this.tempCallNumber; + if (this.tempCallNumber) { new_cn = this.tempCallNumber; } this.course.updateItem(item, this.currentCourse.owning_lib(), new_cn, args.isModifyingCallNumber ).then(resp => { - this.course.fleshMaterial(res).then(fleshed_material => { - this.materialsDataSource.data.push(fleshed_material); - }); - if (item.circ_lib() != this.currentCourse.owning_lib()) { + this.materialsGrid.reload(); + if (item.circ_lib() !== this.currentCourse.owning_lib()) { this.materialAddDifferentLibraryString.current() .then(str => this.toast.warning(str)); } else { @@ -182,10 +206,10 @@ export class CourseAssociateMaterialComponent extends DialogComponent { } deleteSelectedMaterials(items) { - let item_ids = []; + const item_ids = []; items.forEach(item => { this.materialsDataSource.data.splice(this.materialsDataSource.data.indexOf(item, 0), 1); - item_ids.push(item.id()) + item_ids.push(item.id()); }); this.pcrud.search('acmcm', {course: this.courseId, item: item_ids}).subscribe(material => { material.isdeleted(true); @@ -202,4 +226,4 @@ export class CourseAssociateMaterialComponent extends DialogComponent { ); }); } -} \ No newline at end of file +} diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-reserves.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-reserves.module.ts index 3823648afe..96802cf7af 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-reserves.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-reserves.module.ts @@ -8,19 +8,21 @@ import {CourseAssociateMaterialComponent} from './course-associate-material.comp import {CourseAssociateUsersComponent} from './course-associate-users.component'; import {CourseReservesRoutingModule} from './routing.module'; import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-location-select.module'; +import {MarcSimplifiedEditorModule} from '@eg/staff/share/marc-edit/simplified-editor/simplified-editor.module'; @NgModule({ declarations: [ CourseListComponent, CoursePageComponent, CourseAssociateMaterialComponent, - CourseAssociateUsersComponent + CourseAssociateUsersComponent, ], imports: [ StaffCommonModule, AdminCommonModule, CourseReservesRoutingModule, ItemLocationSelectModule, + MarcSimplifiedEditorModule, TreeModule ], exports: [ diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts index 493eaadcbe..6dcaf24cb4 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts @@ -29,6 +29,7 @@ export interface MarcField { // Pass-through to marcrecord.js isControlfield(): boolean; + indicator?: (ind: number) => any; deleteExactSubfields(...subfield: MarcSubfield[]): number; } diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor-field.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor-field.component.ts new file mode 100644 index 0000000000..bfed188b22 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor-field.component.ts @@ -0,0 +1,42 @@ +import {Component, Host, Input, OnInit} from '@angular/core'; +import {MarcSimplifiedEditorComponent} from './simplified-editor.component'; +import {MarcSubfield} from '../marcrecord'; + +/** + * A field that a user can edit, which will later be + * compiled into MARC + */ + +@Component({ + selector: 'eg-marc-simplified-editor-field', + template: '' +}) +export class MarcSimplifiedEditorFieldComponent implements OnInit { + + @Input() tag: string; + @Input() subfield: string; + @Input() defaultValue: string; + + constructor(@Host() private editor: MarcSimplifiedEditorComponent) {} + + ngOnInit() { + this.editor.addField({ + tag: this.tag, + subfields: [[ + this.subfield, + this.defaultValue ? this.defaultValue : '', + 0 + ]], + authValid: false, + authChecked: false, + isCtrlField: false, + isControlfield: () => false, + indicator: (ind: number) => '0', + deleteExactSubfields: (...subfield: MarcSubfield[]) => 0, + }); + } + +} + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.html new file mode 100644 index 0000000000..a82dbf3b9d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.html @@ -0,0 +1,21 @@ + +
+ +
+
+ +
+
+ +
+
+
+ +
+
+ diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.ts new file mode 100644 index 0000000000..80f900a6f5 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.ts @@ -0,0 +1,72 @@ +import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {FormGroup, FormControl, ValidationErrors, ValidatorFn, FormArray} from '@angular/forms'; +import {MarcField, MarcRecord} from '../marcrecord'; +import {TagTableService} from '../tagtable.service'; + +/** + * A simplified editor for basic MARC records, which + * does not require knowledge of MARC tags + */ + +@Component({ + selector: 'eg-marc-simplified-editor', + templateUrl: './simplified-editor.component.html' +}) +export class MarcSimplifiedEditorComponent implements AfterViewInit, OnInit { + + @Input() buttonLabel: string; + @Output() xmlRecordEvent = new EventEmitter(); + + fields: MarcField[] = []; + editor: FormGroup; + + // DOM id prefix to prevent id collisions. + idPrefix: string; + + fieldIndex = 0; + fieldLabels: string[] = []; + + addField: (field: MarcField) => void; + + constructor( + private tagTable: TagTableService + ) {} + + ngOnInit() { + // Add some randomness to the generated DOM IDs to ensure against clobbering + this.idPrefix = 'marc-simplified-editor-' + Math.floor(Math.random() * 100000); + this.editor = new FormGroup({}); + + // Add a fieldId, and then add a new field to the array + this.addField = (field: MarcField) => { + field.fieldId = this.fieldIndex; + this.fields.push(field); + this.editor.addControl(String(this.fieldIndex), new FormControl(null, [])); + this.fieldIndex++; + }; + + } + + ngAfterViewInit() { + this.tagTable.loadTags({marcRecordType: 'biblio', ffType: 'BKS'}).then(table => { + this.fields.forEach((field) => { + this.fieldLabels[field.fieldId] = table.getSubfieldLabel(field.tag, field.subfields[0][0]); + }); + }); + } + + emitXml() { + const record = new MarcRecord(''); + // need to add the value to field.subfields[0][1] + this.fields.forEach((field) => { + if (field.subfields[0][1] === '') { // Default value has not been applied + field.subfields[0][1] = this.editor.get(String(field.fieldId)).value; + } + }); + record.fields = this.fields; + this.xmlRecordEvent.emit(record.toXml()); + } + +} + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.module.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.module.ts new file mode 100644 index 0000000000..ec40da9938 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.module.ts @@ -0,0 +1,27 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {CommonWidgetsModule} from '@eg/share/common-widgets.module'; +import {MarcSimplifiedEditorComponent} from './simplified-editor.component'; +import {MarcSimplifiedEditorFieldComponent} from './simplified-editor-field.component'; +import {TagTableService} from '../tagtable.service'; + +@NgModule({ + declarations: [ + MarcSimplifiedEditorComponent, + MarcSimplifiedEditorFieldComponent, + ], + imports: [ + StaffCommonModule, + CommonWidgetsModule + ], + exports: [ + MarcSimplifiedEditorComponent, + MarcSimplifiedEditorFieldComponent, + ], + providers: [ + TagTableService + ] +}) + +export class MarcSimplifiedEditorModule { } + diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts index ce6ddb7431..8b867a88eb 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts @@ -188,6 +188,13 @@ export class TagTable { return this.toCache('sfcodes', tag, null, list); } + getSubfieldLabel(tag: string, sfCode: string): string { + if (!tag || !this.tagMap[tag]) { return null; } + const subfieldResults = this.tagMap[tag].subfields.filter(sf => sf.code === sfCode); + return subfieldResults.length ? subfieldResults[0].description : null; + } + + getFieldTags(): ContextMenuEntry[] { if (!this.fieldTags) { diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm index c48f6ce9c3..bb4412b6d9 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm @@ -1044,161 +1044,6 @@ sub delete_copy_note { $e->commit; return 1; } -__PACKAGE__->register_method( - method => 'fetch_course_materials', - autoritative => 1, - api_name => 'open-ils.circ.course_materials.retrieve', - signature => q/ - Returns an array of course materials. - @params args : Supplied object to filter search. - /); - -__PACKAGE__->register_method( - method => 'fetch_course_materials', - autoritative => 1, - api_name => 'open-ils.circ.course_materials.retrieve.fleshed', - signature => q/ - Returns an array of course materials, each fleshed out with information - from the item and the course_material object. - @params args : Supplied object to filter search. - /); - -sub fetch_course_materials { - my ($self, $conn, $args) = @_; - my $e = new_editor(); - my $materials = {}; - my %items; - - $materials->{list} = $e->search_asset_course_module_course_materials($args); - return $materials->{list} unless ($self->api_name =~ /\.fleshed/); - - # If we want it fleshed out... - for my $course_material (@{$materials->{list}}) { - my $material = {}; - $material->{id} = $course_material->id; - $material->{relationship} = $course_material->relationship; - $material->{record} = $course_material->record; - my $copy = $e->retrieve_asset_copy([ - $course_material->item, { - flesh => 3, flesh_fields => { - 'acp' => ['call_number'], - 'acn' => ['record'] - } - } - ]); - - $material->{item_data} = $copy; - $material->{volume_data} = $copy->call_number; - $material->{record_data} = $copy->call_number->record; - $items{$course_material->item} = $material; - } - - my $targets = (); - for my $item (values %items) { - my $final_item = {}; - my $mvr = $U->record_to_mvr($item->{record_data}); - $final_item->{id} = $item->{id}; - $final_item->{relationship} = $item->{relationship}; - $final_item->{record} = $item->{record}; - $final_item->{barcode} = $item->{item_data}->barcode; - $final_item->{circ_lib} = $item->{item_data}->circ_lib; - $final_item->{title} = $mvr->title; - $final_item->{call_number} = $item->{volume_data}->label; - $final_item->{location} = $e->retrieve_asset_copy_location( - $item->{item_data}->location - ); - $final_item->{status} = $e->retrieve_config_copy_status( - $item->{item_data}->status - ); - - push @$targets, $final_item; - } - - return $targets; -} - -__PACKAGE__->register_method( - method => 'fetch_courses', - autoritative => 1, - api_name => 'open-ils.circ.courses.retrieve', - signature => q/ - Returns an array of course materials. - @params course_id: The id of the course we want to retrieve - /); - -sub fetch_courses { - my ($self, $conn, @course_ids) = @_; - my $e = new_editor(); - - return unless @course_ids; - my $targets = (); - foreach my $course_id (@course_ids) { - my $target = $e->retrieve_asset_course_module_course($course_id); - push @$targets, $target; - } - - return $targets; -} - -__PACKAGE__->register_method( - method => 'fetch_course_users', - autoritative => 1, - api_name => 'open-ils.circ.course_users.retrieve', - signature => q/ - Returns an array of course users. - @params course_id: The id of the course we want to retrieve from - /); -__PACKAGE__->register_method( - method => 'fetch_course_users', - autoritative => 1, - api_name => 'open-ils.circ.course_users.retrieve.staff', - signature => q/ - Returns an array of course users. - @params course_id: The id of the course we want to retrieve from - /); - -sub fetch_course_users { - my ($self, $conn, $course_id) = @_; - my $e = new_editor(); - my $filter = {}; - my $users = {}; - my %patrons; - - $filter->{course} = $course_id; - $filter->{is_public} = 't' - unless ($self->api_name =~ /\.staff/) and $e->allowed('MANAGE_RESERVES'); - - - $users->{list} = $e->search_asset_course_module_course_users($filter, {order_by => {acmcu => 'id'}}); - for my $course_user (@{$users->{list}}) { - my $patron = {}; - $patron->{id} = $course_user->id; - $patron->{usr_role} = $course_user->usr_role; - $patron->{patron_data} = $e->retrieve_actor_user($course_user->usr); - $patrons{$course_user->usr} = $patron; - } - - my $targets = (); - for my $user (values %patrons) { - my $final_user = {}; - $final_user->{id} = $user->{id}; - $final_user->{usr_role} = $user->{usr_role}; - $final_user->{patron_id} = $user->{patron_data}->id; - $final_user->{first_given_name} = $user->{patron_data}->first_given_name; - $final_user->{second_given_name} = $user->{patron_data}->second_given_name; - $final_user->{family_name} = $user->{patron_data}->family_name; - $final_user->{pref_first_given_name} = $user->{patron_data}->pref_first_given_name; - $final_user->{pref_family_name} = $user->{patron_data}->pref_family_name; - $final_user->{pref_second_given_name} = $user->{patron_data}->pref_second_given_name; - $final_user->{pref_suffix} = $user->{patron_data}->pref_suffix; - $final_user->{pref_prefix} = $user->{patron_data}->pref_prefix; - - push @$targets, $final_user; - } - - return $targets; - -} __PACKAGE__->register_method( method => 'fetch_copy_tags', diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Courses.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Courses.pm new file mode 100644 index 0000000000..fe32164cd4 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Courses.pm @@ -0,0 +1,235 @@ +package OpenILS::Application::Courses; + +use strict; +use warnings; + +use OpenSRF::AppSession; +use OpenILS::Application; +use base qw/OpenILS::Application/; + +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenILS::Utils::Fieldmapper; +use OpenILS::Application::AppUtils; +my $U = "OpenILS::Application::AppUtils"; + +use OpenSRF::Utils::Logger qw/$logger/; + +__PACKAGE__->register_method( + method => 'attach_electronic_resource_to_course', + api_name => 'open-ils.courses.attach.electronic_resource', + signature => { + desc => 'Attaches a bib record for an electronic resource to a course', + params => [ + {desc => 'Authentication token', type => 'string'}, + {desc => 'Record id', type => 'number'}, + {desc => 'Course id', type => 'number'}, + {desc => 'Relationship', type => 'string'} + ], + return => {desc => '1 on success, event on failure'} + }); +sub attach_electronic_resource_to_course { + my ($self, $conn, $authtoken, $record, $course, $relationship) = @_; + my $e = new_editor(authtoken=>$authtoken, xact=>1); + return $e->die_event unless $e->checkauth; + return $e->die_event unless + $e->allowed('MANAGE_RESERVES'); + + my $located_uris = $e->search_asset_call_number({ + record => $record, + deleted => 'f', + label => '##URI##' })->[0]; + my $bib = $e->retrieve_biblio_record_entry([ + $record, { + flesh => 1, + flesh_fields => {'bre' => ['source']} + } + ]); + return $e->event unless (($bib->source && $bib->source->transcendant) || $located_uris); + _attach_bib($e, $course, $record, $relationship); + + return 1; +} + +__PACKAGE__->register_method( + method => 'attach_brief_bib_to_course', + api_name => 'open-ils.courses.attach.biblio_record', + signature => { + desc => 'Creates a new bib record with the provided XML, and attaches it to a course', + params => [ + {desc => 'Authentication token', type => 'string'}, + {desc => 'XML', type => 'string'}, + {desc => 'Course id', type => 'number'}, + {desc => 'Relationship', type => 'string'} + ], + return => {desc => '1 on success, event on failure'} + }); +sub attach_brief_bib_to_course { + my ($self, $conn, $authtoken, $marcxml, $course, $relationship) = @_; + my $e = new_editor(authtoken=>$authtoken, xact=>1); + return $e->die_event unless $e->checkauth; + return $e->die_event unless $e->allowed('MANAGE_RESERVES'); + return $e->die_event unless $e->allowed('CREATE_MARC'); + + my $bib_source_id = $U->ou_ancestor_setting_value($self->{ou}, 'circ.course_materials_brief_record_bib_source'); + my $bib_source_name; + if ($bib_source_id) { + $bib_source_name = $e->retrieve_config_bib_source($bib_source_id)->source; + } else { + # The default value from the seed data + $bib_source_name = 'Course materials module'; + } + + my $bib_create = OpenSRF::AppSession + ->create('open-ils.cat') + ->request('open-ils.cat.biblio.record.xml.create', + $authtoken, $marcxml, $bib_source_name) + ->gather(1); + _attach_bib($e, $course, $bib_create->id, $relationship) if ($bib_create); + return 1; +} + +# Shared logic for both e-resources and brief bibs +sub _attach_bib { + my ($e, $course, $record, $relationship) = @_; + my $acmcm = Fieldmapper::asset::course_module_course_materials->new; + $acmcm->course($course); + $acmcm->record($record); + $acmcm->relationship($relationship); + $e->create_asset_course_module_course_materials( $acmcm ) or return $e->die_event; + $e->commit; +} + +sub detach_material_from_course { + my ($self, $conn, $authtoken, $acmcm) = @_; + +} + +__PACKAGE__->register_method( + method => 'fetch_course_materials', + autoritative => 1, + stream => 1, + api_name => 'open-ils.courses.course_materials.retrieve', + signature => q/ + Returns an array of course materials. + @params args : Supplied object to filter search. + /); + +__PACKAGE__->register_method( + method => 'fetch_course_materials', + autoritative => 1, + stream => 1, + api_name => 'open-ils.courses.course_materials.retrieve.fleshed', + signature => q/ + Returns an array of course materials, each fleshed out with information + from the item and the course_material object. + @params args : Supplied object to filter search. + /); + +sub fetch_course_materials { + my ($self, $conn, $args) = @_; + my $e = new_editor(); + my $materials; + + if ($self->api_name =~ /\.fleshed/) { + my $fleshing = { + 'flesh' => 2, 'flesh_fields' => { + 'acmcm' => ['item', 'record'], + 'acp' => ['call_number', 'circ_lib', 'location', 'status'], + 'bre' => ['wide_display_entry'], + } + }; + $materials = $e->search_asset_course_module_course_materials([$args, $fleshing]); + } else { + $materials = $e->search_asset_course_module_course_materials($args); + } + $conn->respond($_) for @$materials; + return undef; +} + +__PACKAGE__->register_method( + method => 'fetch_courses', + autoritative => 1, + api_name => 'open-ils.courses.courses.retrieve', + signature => q/ + Returns an array of course materials. + @params course_id: The id of the course we want to retrieve + /); + +sub fetch_courses { + my ($self, $conn, @course_ids) = @_; + my $e = new_editor(); + + return unless @course_ids; + my $targets = (); + foreach my $course_id (@course_ids) { + my $target = $e->retrieve_asset_course_module_course($course_id); + push @$targets, $target; + } + + return $targets; +} + +__PACKAGE__->register_method( + method => 'fetch_course_users', + autoritative => 1, + api_name => 'open-ils.courses.course_users.retrieve', + signature => q/ + Returns an array of course users. + @params course_id: The id of the course we want to retrieve from + /); +__PACKAGE__->register_method( + method => 'fetch_course_users', + autoritative => 1, + api_name => 'open-ils.courses.course_users.retrieve.staff', + signature => q/ + Returns an array of course users. + @params course_id: The id of the course we want to retrieve from + /); + +sub fetch_course_users { + my ($self, $conn, $course_id) = @_; + my $e = new_editor(); + my $filter = {}; + my $users = {}; + my %patrons; + + $filter->{course} = $course_id; + $filter->{is_public} = 't' + unless ($self->api_name =~ /\.staff/) and $e->allowed('MANAGE_RESERVES'); + + + $users->{list} = $e->search_asset_course_module_course_users($filter, {order_by => {acmcu => 'id'}}); + for my $course_user (@{$users->{list}}) { + my $patron = {}; + $patron->{id} = $course_user->id; + $patron->{usr_role} = $course_user->usr_role; + $patron->{patron_data} = $e->retrieve_actor_user($course_user->usr); + $patrons{$course_user->usr} = $patron; + } + + my $targets = (); + for my $user (values %patrons) { + my $final_user = {}; + $final_user->{id} = $user->{id}; + $final_user->{usr_role} = $user->{usr_role}; + $final_user->{patron_id} = $user->{patron_data}->id; + $final_user->{first_given_name} = $user->{patron_data}->first_given_name; + $final_user->{second_given_name} = $user->{patron_data}->second_given_name; + $final_user->{family_name} = $user->{patron_data}->family_name; + $final_user->{pref_first_given_name} = $user->{patron_data}->pref_first_given_name; + $final_user->{pref_family_name} = $user->{patron_data}->pref_family_name; + $final_user->{pref_second_given_name} = $user->{patron_data}->pref_second_given_name; + $final_user->{pref_suffix} = $user->{patron_data}->pref_suffix; + $final_user->{pref_prefix} = $user->{patron_data}->pref_prefix; + + push @$targets, $final_user; + } + + return $targets; + +} + + + +1; + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm index 42160c16e9..eaffd44223 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm @@ -22,20 +22,20 @@ sub load_course { unless $course_id and $course_id =~ /^\d+$/; $ctx->{course} = $U->simplereq( - 'open-ils.circ', - 'open-ils.circ.courses.retrieve', + 'open-ils.courses', + 'open-ils.courses.courses.retrieve', [$course_id] )->[0]; $ctx->{instructors} = $U->simplereq( - 'open-ils.circ', - 'open-ils.circ.course_users.retrieve', + 'open-ils.courses', + 'open-ils.courses.course_users.retrieve', $course_id ); $ctx->{course_materials} = $U->simplereq( - 'open-ils.circ', - 'open-ils.circ.course_materials.retrieve.fleshed', + 'open-ils.courses', + 'open-ils.courses.course_materials.retrieve.fleshed.atomic', {course => $course_id} ); return Apache2::Const::OK; @@ -394,4 +394,4 @@ sub _prepare_query { } return ($full_query, @queries); -} \ No newline at end of file +} diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm index 2cb5699a8b..494ddedd6f 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm @@ -118,8 +118,8 @@ sub load_record { ); if ($ctx->{course_module_opt_in}) { $copy->{course_materials} = $U->simplereq( - 'open-ils.circ', - 'open-ils.circ.course_materials.retrieve', + 'open-ils.courses', + 'open-ils.courses.course_materials.retrieve', {item => $copy->{id}} ); my %course_ids; @@ -128,8 +128,8 @@ sub load_record { } $copy->{courses} = $U->simplereq( - 'open-ils.circ', - 'open-ils.circ.courses.retrieve', + 'open-ils.courses', + 'open-ils.courses.courses.retrieve', keys %course_ids ); } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm index 4170b390be..adcff2ad9f 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm @@ -579,8 +579,8 @@ sub load_rresults { $rec->{popularity} = $res_rec->[2]; if ($course_module_opt_in) { $rec->{course_materials} = $U->simplereq( - 'open-ils.circ', - 'open-ils.circ.course_materials.retrieve', + 'open-ils.courses', + 'open-ils.courses.course_materials.retrieve', {record => $rec->{id}} ); my %course_ids; @@ -589,8 +589,8 @@ sub load_rresults { } $rec->{courses} = $U->simplereq( - 'open-ils.circ', - 'open-ils.circ.courses.retrieve', + 'open-ils.courses', + 'open-ils.courses.courses.retrieve', keys %course_ids ); } @@ -921,4 +921,4 @@ sub staff_save_search { return ($cache_key, $list); } -1; \ No newline at end of file +1; diff --git a/Open-ILS/src/perlmods/live_t/30-courses.t b/Open-ILS/src/perlmods/live_t/30-courses.t new file mode 100644 index 0000000000..480206c5df --- /dev/null +++ b/Open-ILS/src/perlmods/live_t/30-courses.t @@ -0,0 +1,23 @@ +#!perl + +use Test::More tests => 1; + +diag("Test the course materials module."); + +use strict; use warnings; + +use OpenILS::Utils::TestUtils; +my $script = OpenILS::Utils::TestUtils->new(); +$script->bootstrap; + +our $apputils = "OpenILS::Application::AppUtils"; + +is(1, 1, 'placeholder'); + + +# Test: can attach a bib record with located URI +# Test: cannot attach a bib record without a located URI + +# Test: can detach an item (just delete this) +# Test: can detach a record that is not temporary (just delete this) +# Test: can detach a record that is temporary (delete this, and delete the record too) diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql index 5a45e2a8fb..0207797284 100644 --- a/Open-ILS/src/sql/Pg/040.schema.asset.sql +++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql @@ -1125,22 +1125,15 @@ CREATE TABLE asset.course_module_course_users ( 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), + item INT REFERENCES asset.copy (id), relationship TEXT, record INT REFERENCES biblio.record_entry (id), + temporary_record BOOLEAN, 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, - unique (course, item) -); - -CREATE TABLE asset.course_module_non_cat_course_materials ( - id SERIAL PRIMARY KEY, - course INT NOT NULL REFERENCES asset.course_module_course (id), - item TEXT NOT NULL, - url TEXT, - relationship TEXT + unique (course, item, record) ); COMMIT; diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 4ed29ae4f0..b62ea3277b 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -3854,6 +3854,17 @@ INSERT into config.org_unit_setting_type 'coust', 'description'), 'bool', null) +,( 'circ.course_materials_brief_record_bib_source', 'circ', + oils_i18n_gettext( + 'circ.course_materials_brief_record_bib_source', + 'Bib source for brief records created in the course materials module', + 'coust', 'label'), + oils_i18n_gettext( + 'circ.course_materials_brief_record_bib_source', + 'The course materials module will use this bib source for any new brief bibliographic records made inside that module. For best results, use a transcendant bib source.', + 'coust', 'description'), + 'link', 'cbs') + ,( 'circ.password_reset_request_per_user_limit', 'sec', oils_i18n_gettext('circ.password_reset_request_per_user_limit', @@ -20620,6 +20631,14 @@ VALUES ( 'cwst', 'label' ) ); +INSERT INTO config.bib_source (quality, source, transcendant) VALUES + (1, oils_i18n_gettext(1, 'Course materials module', 'cbs', 'source'), TRUE); + +INSERT INTO actor.org_unit_setting (org_unit, name, value) + SELECT 1, 'circ.course_materials_brief_record_bib_source', id + FROM config.bib_source + WHERE source='Course materials module'; + INSERT INTO config.workstation_setting_type (name, grp, datatype, label) VALUES ( 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 b934d09f8c..a1fc120b86 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 @@ -22,22 +22,15 @@ CREATE TABLE asset.course_module_course_users ( 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), + item INT REFERENCES asset.copy (id), relationship TEXT, record INT REFERENCES biblio.record_entry (id), + temporary_record BOOLEAN, 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, - unique (course, item) -); - -CREATE TABLE asset.course_module_non_cat_course_materials ( - id SERIAL PRIMARY KEY, - course INT NOT NULL REFERENCES asset.course_module_course (id), - item TEXT NOT NULL, - url TEXT, - relationship TEXT + unique (course, item, record) ); INSERT INTO permission.perm_list(id, code, description) @@ -55,7 +48,7 @@ INSERT INTO permission.perm_list(id, code, description) INSERT INTO permission.grp_perm_map(perm, grp, depth) VALUES (624, 9, 0), (624, 11, 0), (624, 12, 0), (624, 13, 0); INSERT INTO config.org_unit_setting_type - (grp, name, datatype, label, description) + (grp, name, datatype, label, description, fm_class) VALUES ( 'circ', 'circ.course_materials_opt_in', 'bool', @@ -70,7 +63,7 @@ VALUES ( 'If enabled, the Org Unit will utilize Course Material functionality.' 'coust', 'description' - ) + ), null ), ( 'circ', 'circ.course_materials_browse_by_instructor', 'bool', @@ -85,7 +78,29 @@ VALUES ( 'If enabled, the Org Unit will allow OPAC users to browse Courses by instructor name.' 'coust', 'description' - ) + ), null +), ( + 'circ', + 'circ.course_materials_brief_record_bib_source', 'link', + oils_i18n_gettext( + 'circ.course_materials_brief_record_bib_source', + 'Bib source for brief records created in the course materials module', + 'coust', 'label' + ), + oils_i18n_gettext( + 'circ.course_materials_brief_record_bib_source', + 'The course materials module will use this bib source for any new brief bibliographic records made inside that module. For best results, use a transcendant bib source.', + 'coust', 'description' + ), 'cbs' + ); +INSERT INTO config.bib_source (quality, source, transcendant) VALUES + (1, oils_i18n_gettext(1, 'Course materials module', 'cbs', 'source'), TRUE); + +INSERT INTO actor.org_unit_setting (org_unit, name, value) + SELECT 1, 'circ.course_materials_brief_record_bib_source', id + FROM config.bib_source + WHERE source='Course materials module'; + COMMIT; diff --git a/Open-ILS/src/templates/opac/parts/course/body.tt2 b/Open-ILS/src/templates/opac/parts/course/body.tt2 index d48ce11b85..2d80caff69 100644 --- a/Open-ILS/src/templates/opac/parts/course/body.tt2 +++ b/Open-ILS/src/templates/opac/parts/course/body.tt2 @@ -74,22 +74,44 @@ - [% FOREACH copy_info IN ctx.course_materials %] + [% FOREACH material IN ctx.course_materials %] - [%- INCLUDE "opac/parts/library_name_link.tt2"; -%] - - + + [% IF material.item %] + [%- fleshed_ou = material.item.circ_lib -%] + [%- INCLUDE "opac/parts/library_name_link_from_ou.tt2"; -%] + [% ELSE %] + [% l('Online') %] + [% END %] + + + + + [% IF material.item %] + [% material.item.call_number.label %] + [% END %] - [% copy_info.call_number %] - - [% copy_info.title %] + + [% material.record.wide_display_entry.title %] - [% copy_info.barcode %] - [% copy_info.relationship %] - [% copy_info.status.name %] - [% copy_info.location.name %] + + [% IF material.item %] + [% material.item.barcode %] + [% END %] + + [% material.relationship %] + + [% IF material.item %] + [% material.item.status.name %] + [% END %] + + + [% IF material.item %] + [% material.item.location.name %] + [% END %] + [% END %] -- 2.43.2