From 86967de6f5fcc17119bf476f1b7ee7762359d6d2 Mon Sep 17 00:00:00 2001 From: Kyle Huckins Date: Fri, 6 Dec 2019 07:17:53 +0000 Subject: [PATCH] lp1849212: Course Admin Page and OPAC improvements - Double-clicking on a course in the Course Reserves List will up a dedicated admin page for an individual course, featuring tabs for Editing and managing Course Materials. - Add bannerStyle and bannerIcon parameters to eg-staff-banner to improve eg-staff-banner accessability - Improve UX of Course Page - Properly disable inputs and buttons when course is already archived. - Change links in Angular Catalog to navigate the user to the Admin Course Page. - Prevent users from associating duplicate items to a course. - Remove unnecessary artifact from course page html - Implement Course column to view Course Numbers of classes associated with individual copies. - Add a record column to course_module_course_materials to improve efficiency of API and service methods. - Add previous circ_modifier adjustments to upgrade script - Provide Associated Course names and numbers on OPAC Catalog search results. - Fixed typo for User Role field, now matcheing DB column, usr_role - Added boolean "is_public" to differentiate between a role that should be viewable by anyone on the OPAC. - Perl Module and Course Page displaying name and course number of course, as well as names/roles of publicly viewable members, course details, and a table of all materials associated with the course. - URL listed as eg/opac/course/[COURSE_ID] - Ensure Shelving Location input checkbox ticks when selecting a value. - Ensure temporary value inputs are not cleared when entering a new item. - Fully select contents of Barcode input field when selected. - Clear bBarcode input upon hitting enter. - Add a new tab for managing Users associated with the course, in preparation for the User Dialog code. - Remove excess whitespace on OPAC course page. 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 | 5 +- .../course-associate-material.component.html | 7 +- .../course-associate-material.component.ts | 4 +- .../course-reserves/course-list.component.html | 4 +- .../local/course-reserves/course-list.component.ts | 13 +- .../course-reserves/course-page.component.html | 272 ++++++++++++++++++++ .../local/course-reserves/course-page.component.ts | 183 +++++++++++++ .../course-reserves/course-reserves.module.ts | 2 + .../admin/local/course-reserves/routing.module.ts | 4 + .../app/staff/catalog/record/copies.component.html | 6 +- .../app/staff/catalog/result/record.component.html | 7 +- .../app/staff/catalog/result/record.component.ts | 5 +- .../share/bib-summary/bib-summary.component.html | 6 +- .../src/eg2/src/app/staff/share/course.service.ts | 44 +++- .../src/app/staff/share/staff-banner.component.ts | 8 +- .../src/perlmods/lib/OpenILS/Application/Circ.pm | 150 +++++++++++ .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm | 2 + .../perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm | 42 +++ .../perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm | 24 ++- .../perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm | 25 ++- Open-ILS/src/sql/Pg/040.schema.asset.sql | 4 +- .../XXXX.schema.course-materials-module.sql | 6 +- Open-ILS/src/templates/opac/course.tt2 | 16 ++ Open-ILS/src/templates/opac/css/style.css.tt2 | 35 ++- Open-ILS/src/templates/opac/parts/course/body.tt2 | 101 ++++++++ .../src/templates/opac/parts/record/copy_table.tt2 | 12 + Open-ILS/src/templates/opac/parts/result/table.tt2 | 11 + 27 files changed, 964 insertions(+), 34 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.ts create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm create mode 100644 Open-ILS/src/templates/opac/course.tt2 create mode 100644 Open-ILS/src/templates/opac/parts/course/body.tt2 diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 3bd715d..b96a83d 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3143,7 +3143,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + + @@ -3169,6 +3170,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -3178,6 +3180,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + 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 488eea7..65dfb71 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 @@ -19,7 +19,9 @@
Barcode
- +
@@ -107,7 +109,8 @@
+ [(ngModel)]="tempLocation" (oninput)="isModifyingLocation = true" + (valueChange)="isModifyingLocation = true">
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 a6c1971..404451a 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 @@ -100,12 +100,12 @@ export class CourseAssociateMaterialComponent extends DialogComponent { tempStatus: this.tempStatus, currentCourse: this.currentCourse } + this.barcodeInput = null; - this.pcrud.search('acp', {barcode: barcode}, { + this.pcrud.search('acp', {barcode: args.barcode}, { flesh: 3, flesh_fields: {acp: ['call_number']} }).subscribe(item => { let associatedMaterial = this.courseSvc.associateMaterials(item, args); - console.log(associatedMaterial); associatedMaterial.material.then(res => { item = associatedMaterial.item; let new_cn = item.call_number().label(); diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.html index d31f440..2dd917f 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.html @@ -3,8 +3,8 @@ - - + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.ts index 53875f3..91c4b28 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.ts @@ -1,4 +1,5 @@ import {Component, Input, ViewChild, OnInit} from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; import {IdlObject} from '@eg/core/idl.service'; import {PcrudService} from '@eg/core/pcrud.service'; import {AuthService} from '@eg/core/auth.service'; @@ -47,11 +48,17 @@ export class CourseListComponent implements OnInit { private net: NetService, private org: OrgService, private pcrud: PcrudService, - private toast: ToastService, + private route: ActivatedRoute, + private router: Router, + private toast: ToastService ){} ngOnInit() { this.getSource(); + this.grid.onRowActivate.subscribe((course:IdlObject) => { + let idToEdit = course.id(); + this.navigateToCoursePage(idToEdit); + }) } /** @@ -76,6 +83,10 @@ export class CourseListComponent implements OnInit { }; } + navigateToCoursePage(id: any) { + this.router.navigate(["/staff/admin/local/asset/course_list/" + id]); + } + showEditDialog(standingPenalty: IdlObject): Promise { this.editDialog.mode = 'update'; this.editDialog.recordId = standingPenalty['id'](); diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.html new file mode 100644 index 0000000..0361526 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.html @@ -0,0 +1,272 @@ + + + + + + + +
+
+ +
+
+ + +
+
+
+
+ + + + +
+ +
+
+
+
+
+ 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 +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+ Circulation Modifier +
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+ Item Status +
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+ Shelving Location +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + +
+
+ + + + {{entry.barcode()}} + + + + + + + {{entry._title}} + + + + + {{r.label}} + +
+
+ + + + +
+
+
+
+
+
+ Patron Barcode +
+ + +
+
+
+
+
+
+
+ Role +
+ +
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.ts new file mode 100644 index 0000000..3765496 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.ts @@ -0,0 +1,183 @@ +import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core'; +import {Router, ActivatedRoute} from '@angular/router'; +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 {StaffBannerComponent} from '@eg/staff/share/staff-banner.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {CourseService} from '@eg/staff/share/course.service'; + +@Component({ + selector: 'eg-course-page', + templateUrl: './course-page.component.html' +}) + +export class CoursePageComponent implements OnInit { + + currentCourse: IdlObject; + courseId: any; + + // Edit Tab + @ViewChild('archiveFailedString', { static: true }) + archiveFailedString: StringComponent; + @ViewChild('archiveSuccessString', { static: true }) + archiveSuccessString: StringComponent; + + // Materials Tab + materials: any[] = []; + @ViewChild('materialsGrid', {static: true}) materialsGrid: GridComponent; + @ViewChild('materialDeleteFailedString', { static: true }) + materialDeleteFailedString: StringComponent; + @ViewChild('materialDeleteSuccessString', { static: true }) + materialDeleteSuccessString: StringComponent; + @ViewChild('materialAddSuccessString', { static: true }) + materialAddSuccessString: StringComponent; + @ViewChild('materialAddFailedString', { static: true }) + materialAddFailedString: StringComponent; + @ViewChild('materialAddDifferentLibraryString', { static: true }) + materialAddDifferentLibraryString: StringComponent; + materialsDataSource: GridDataSource; + @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; + + // Users Tab + @Input() userBarcode: String; + @Input() userRoleInput: String; + constructor( + private auth: AuthService, + private course: CourseService, + private event: EventService, + private idl: IdlService, + private org: OrgService, + private pcrud: PcrudService, + private route: ActivatedRoute, + private toast: ToastService + ) { + this.materialsDataSource = new GridDataSource(); + } + + ngOnInit() { + this.courseId = parseInt(this.route.snapshot.paramMap.get('id')); + this.course.getCourses([this.courseId]).then(course => { + this.currentCourse = course[0]; + }); + this.materialsDataSource.getRows = (pager: Pager, sort: any[]) => { + return this.loadMaterialsGrid(pager); + } + } + + // Edit Tab + archiveCourse() { + this.course.disassociateMaterials([this.currentCourse]).then(res => { + this.currentCourse.is_archived('t'); + this.pcrud.update(this.currentCourse).subscribe(val => { + console.debug('archived: ' + val); + this.archiveSuccessString.current() + .then(str => this.toast.success(str)); + }, err => { + this.archiveFailedString.current() + .then(str => this.toast.danger(str)); + }); + }); + } + + // Materials Tab + loadMaterialsGrid(pager: Pager): Observable { + return new Observable(observer => { + this.course.getMaterials(this.courseId).then(materials => { + materials.forEach(material => { + this.course.fleshMaterial(material.item(), material.relationship()).then(fleshed_material => { + this.materialsDataSource.data.push(fleshed_material); + }); + }); + }); + observer.complete(); + }); + } + + associateItem(barcode, relationship) { + if (barcode) { + let args = { + barcode: barcode, + relationship: relationship, + isModifyingCallNumber: this.isModifyingCallNumber, + isModifyingCircMod: this.isModifyingCircMod, + isModifyingLocation: this.isModifyingLocation, + isModifyingStatus: this.isModifyingStatus, + tempCircMod: this.tempCircMod, + 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); + associatedMaterial.material.then(res => { + item = associatedMaterial.item; + let new_cn = item.call_number().label(); + if (this.tempCallNumber) new_cn = this.tempCallNumber; + this.course.updateItem(item, this.currentCourse.owning_lib(), + new_cn, args.isModifyingCallNumber + ).then(resp => { + this.course.fleshMaterial(item.id(), args.relationship).then(fleshed_material => { + this.materialsDataSource.data.push(fleshed_material); + }); + if (item.circ_lib() != this.currentCourse.owning_lib()) { + this.materialAddDifferentLibraryString.current() + .then(str => this.toast.warning(str)); + } else { + this.materialAddSuccessString.current() + .then(str => this.toast.success(str)); + } + }); + }, err => { + this.materialAddFailedString.current() + .then(str => this.toast.danger(str)); + }); + }); + } + } + + deleteSelected(items) { + let item_ids = []; + items.forEach(item => { + this.materialsDataSource.data.splice(this.materialsDataSource.data.indexOf(item, 0), 1); + item_ids.push(item.id()) + }); + this.pcrud.search('acmcm', {course: this.courseId, item: item_ids}).subscribe(material => { + material.isdeleted(true); + this.pcrud.autoApply(material).subscribe( + val => { + this.course.resetItemFields(material, this.currentCourse.owning_lib()); + console.debug('deleted: ' + val); + this.materialDeleteSuccessString.current().then(str => this.toast.success(str)); + }, + err => { + this.materialDeleteFailedString.current() + .then(str => this.toast.danger(str)); + } + ); + }); + } +} \ 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 0ca63cf..180faca 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 @@ -3,6 +3,7 @@ import {TreeModule} from '@eg/share/tree/tree.module'; import {StaffCommonModule} from '@eg/staff/common.module'; import {AdminCommonModule} from '@eg/staff/admin/common.module'; import {CourseListComponent} from './course-list.component'; +import {CoursePageComponent} from './course-page.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'; @@ -10,6 +11,7 @@ import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-loca @NgModule({ declarations: [ CourseListComponent, + CoursePageComponent, CourseAssociateMaterialComponent ], imports: [ diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/routing.module.ts index 84e74ff..7c735dd 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/routing.module.ts @@ -1,8 +1,12 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {CourseListComponent} from './course-list.component'; +import {CoursePageComponent} from './course-page.component'; const routes: Routes = [{ + path: ':id', + component: CoursePageComponent +}, { path: '', component: CourseListComponent }]; diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html index ab44c6c..21b8d9f 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html @@ -36,7 +36,11 @@ -
{{course.course_number()}}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html index 85d75a2..4600a35 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html @@ -127,7 +127,12 @@
Associated Courses: - {{courseNames.join(', ')}}
+ + + {{course.name()}} ({{course.course_number()}}) + {{isLast ? '' : ', '}} + +
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts index 94204c0..8eea366 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts @@ -31,7 +31,7 @@ export class ResultRecordComponent implements OnInit, OnDestroy { isRecordSelected: boolean; basketSub: Subscription; has_course: boolean; - courseNames: any[] = []; + courses: any[] = []; constructor( private router: Router, @@ -66,8 +66,7 @@ export class ResultRecordComponent implements OnInit, OnDestroy { if (res) { this.course.fetchCopiesInCourseFromRecord(recordId).then(course_list => { Object.keys(course_list).forEach(key => { - this.courseNames.push(course_list[key].name() + - "(" + course_list[key].course_number() + ")"); + this.courses.push(course_list[key]); }); this.has_course = true; }); diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html index 0dea4ba..a8e3953 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html @@ -100,7 +100,11 @@
  • Course Name:
    -
    {{course.name()}}
    +
    Course Number:
    {{course.course_number()}}
    Section Number:
    diff --git a/Open-ILS/src/eg2/src/app/staff/share/course.service.ts b/Open-ILS/src/eg2/src/app/staff/share/course.service.ts index 0856cfd..edeeed3 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/course.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/course.service.ts @@ -35,6 +35,37 @@ export class CourseService { } } + getMaterials(course_ids?: Number[]): Promise { + if (!course_ids) { + return this.pcrud.retrieveAll('acmcm', + {}, {atomic: true}).toPromise(); + } else { + return this.pcrud.search('acmcm', {course: course_ids}, + {}, {atomic: true}).toPromise(); + } + } + + fleshMaterial(itemId, relationship?): Promise { + return new Promise((resolve, reject) => { + let item = this.idl.create('acp'); + this.net.request( + 'open-ils.circ', + 'open-ils.circ.copy_details.retrieve', + this.auth.token(), itemId + ).subscribe(res => { + if (res && res.copy) { + item = res.copy; + item.call_number(res.volume); + item._title = res.mvr.title(); + item.circ_lib(this.org.get(item.circ_lib())); + if (relationship) item._relationship = relationship; + } + }, err => { + reject(err); + }, () => resolve(item)); + }); + } + getCoursesFromMaterial(copy_id): Promise { let id_list = []; return new Promise((resolve, reject) => { @@ -45,7 +76,7 @@ export class CourseService { id_list.push(materials.course()); } }, err => { - console.log(err); + console.debug(err); reject(err); }, () => { if (id_list.length) { @@ -81,9 +112,11 @@ export class CourseService { // Creating a new acmcm Entry associateMaterials(item, args) { - console.log("entering associateMaterials") let material = this.idl.create('acmcm'); material.item(item.id()); + if (item.call_number() && item.call_number().record()) { + material.record(item.call_number().record()); + } material.course(args.currentCourse.id()); if (args.relationship) material.relationship(args.relationship); @@ -124,7 +157,6 @@ export class CourseService { material.isdeleted(true); this.resetItemFields(material, course_library_hash[material.course()]); this.pcrud.autoApply(material).subscribe(res => { - console.log(res); }, err => { reject(err); }, () => { @@ -189,7 +221,11 @@ export class CourseService { resolve(item); }); } else { - return this.pcrud.update(item); + this.pcrud.update(item).subscribe(rse => { + resolve(item); + }, err => { + reject(item); + }); } }); }); diff --git a/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts b/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts index a45e276..c49c495 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts @@ -3,15 +3,19 @@ import {Component, OnInit, Input} from '@angular/core'; @Component({ selector: 'eg-staff-banner', template: ` -
    +
    - {{bannerText}} + {{bannerIcon}} + {{bannerText}}
    ` }) export class StaffBannerComponent { @Input() public bannerText: string; + @Input() public bannerIcon: string; + @Input() public bannerStyle: string; } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm index bb4412b..0a7c80f 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm @@ -1044,6 +1044,156 @@ 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. + /); + +__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_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; +} + +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); + 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->{first_given_name} = $user->{patron_data}->first_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; + + push @$targets, $final_user; + } + + return $targets; + +} __PACKAGE__->register_method( method => 'fetch_copy_tags', diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm index c214c1a..2a9548f 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm @@ -24,6 +24,7 @@ use OpenILS::WWW::EGCatLoader::Browse; use OpenILS::WWW::EGCatLoader::Library; use OpenILS::WWW::EGCatLoader::Search; use OpenILS::WWW::EGCatLoader::Record; +use OpenILS::WWW::EGCatLoader::Course; use OpenILS::WWW::EGCatLoader::Container; use OpenILS::WWW::EGCatLoader::SMS; use OpenILS::WWW::EGCatLoader::Register; @@ -154,6 +155,7 @@ sub load { return $self->load_record if $path =~ m|opac/record/\d|; return $self->load_cnbrowse if $path =~ m|opac/cnbrowse|; return $self->load_browse if $path =~ m|opac/browse|; + return $self->load_course if $path =~ m|opac/course|; return $self->load_mylist_add if $path =~ m|opac/mylist/add|; return $self->load_mylist_delete if $path =~ m|opac/mylist/delete|; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm new file mode 100644 index 0000000..bd340ea --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm @@ -0,0 +1,42 @@ +package OpenILS::WWW::EGCatLoader; +use strict; use warnings; +use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_GONE HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST HTTP_NOT_FOUND); +use OpenSRF::Utils::Logger qw/$logger/; +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenILS::Utils::Fieldmapper; +use OpenILS::Application::AppUtils; +use Net::HTTP::NB; +use IO::Select; +my $U = 'OpenILS::Application::AppUtils'; + +sub load_course { + my $self = shift; + my $ctx = $self->ctx; + + $ctx->{page} = 'course'; + $ctx->{readonly} = $self->cgi->param('readonly'); + + my $course_id = $ctx->{page_args}->[0]; + + return Apache2::Const::HTTP_BAD_REQUEST + unless $course_id and $course_id =~ /^\d+$/; + + $ctx->{course} = $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.courses.retrieve', + [$course_id] + )->[0]; + + $ctx->{instructors} = $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.course_users.retrieve', + $course_id + ); + + $ctx->{course_materials} = $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.course_materials.retrieve.fleshed', + {course => $course_id} + ); + return Apache2::Const::OK; +} \ 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 5032716..ea30359 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm @@ -102,8 +102,13 @@ sub load_record { $ctx->{copies} = $copy_rec->gather(1); + my $course_module_opt_in = 0; + if ($ctx->{get_org_setting}->($org, "circ.course_materials_opt_in")) { + $course_module_opt_in = 1; + } + # Add public copy notes to each copy - and while we're in there, grab peer bib records - # and copy tags + # and copy tags. Oh and if we're working with course materials, those too. my %cached_bibs = (); foreach my $copy (@{$ctx->{copies}}) { $copy->{notes} = $U->simplereq( @@ -111,6 +116,23 @@ sub load_record { 'open-ils.circ.copy_note.retrieve.all', {itemid => $copy->{id}, pub => 1 } ); + if ($course_module_opt_in) { + $copy->{course_materials} = $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.course_materials.retrieve', + {item => $copy->{id}} + ); + my %course_ids; + for my $material (@{$copy->{course_materials}}) { + $course_ids{$material->course} = 1; + } + + $copy->{courses} = $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.courses.retrieve', + keys %course_ids + ); + } $self->timelog("past copy note retrieval call"); my $meth = 'open-ils.circ.copy_tags.retrieve'; $meth .= ".staff" if $ctx->{is_staff}; 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 459e308..4170b39 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm @@ -568,10 +568,32 @@ sub load_rresults { } } + my $course_module_opt_in = 0; + if ($ctx->{get_org_setting}->($self->_get_search_lib, "circ.course_materials_opt_in")) { + $course_module_opt_in = 1; + } + for my $rec (@{$ctx->{records}}) { my ($res_rec) = grep { $_->[0] == $rec->{$id_key} } @{$results->{ids}}; $rec->{badges} = [split(',', $res_rec->[1])] if $res_rec->[1]; $rec->{popularity} = $res_rec->[2]; + if ($course_module_opt_in) { + $rec->{course_materials} = $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.course_materials.retrieve', + {record => $rec->{id}} + ); + my %course_ids; + for my $material (@{$rec->{course_materials}}) { + $course_ids{$material->course} = 1; + } + + $rec->{courses} = $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.courses.retrieve', + keys %course_ids + ); + } if ($tag_circs) { # index 3 (5 for MR) in the per-record result array is a boolean which # indicates whether the record in question is in the users @@ -580,6 +602,7 @@ sub load_rresults { $rec->{user_circulated} = 1 if $res_rec->[$index]; } } + $ctx->{search_facets} = $facets; @@ -898,4 +921,4 @@ sub staff_save_search { return ($cache_key, $list); } -1; +1; \ 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 4fd2f01..7534094 100644 --- a/Open-ILS/src/sql/Pg/040.schema.asset.sql +++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql @@ -1126,10 +1126,12 @@ CREATE TABLE asset.course_module_course_materials ( course INT NOT NULL REFERENCES asset.course_module_course (id), item INT NOT NULL REFERENCES asset.copy (id), relationship TEXT, + record INT REFERENCES biblio.record_entry (id), 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 + original_callnumber INT REFERENCES asset.call_number, + unique (course, item) ); 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 eb4807e..6b0031e 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 @@ -23,10 +23,12 @@ CREATE TABLE asset.course_module_course_materials ( course INT NOT NULL REFERENCES asset.course_module_course (id), item INT NOT NULL REFERENCES asset.copy (id), relationship TEXT, + record INT REFERENCES biblio.record_entry (id), 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 + 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 ( diff --git a/Open-ILS/src/templates/opac/course.tt2 b/Open-ILS/src/templates/opac/course.tt2 new file mode 100644 index 0000000..e53fc06 --- /dev/null +++ b/Open-ILS/src/templates/opac/course.tt2 @@ -0,0 +1,16 @@ +[%- PROCESS "opac/parts/header.tt2"; + WRAPPER "opac/parts/base.tt2"; + INCLUDE "opac/parts/topnav.tt2"; + ctx.page_title = l("Course Details: [_1] - [_2]", ctx.course.name, ctx.course.course_number); +-%] + [%- INCLUDE "opac/parts/searchbar.tt2" %] +

    [% l('Course Details') %]

    +
    +
    +
    + [% INCLUDE "opac/parts/course/body.tt2" %] +
    +
    +
    +
    +[%- END %] diff --git a/Open-ILS/src/templates/opac/css/style.css.tt2 b/Open-ILS/src/templates/opac/css/style.css.tt2 index 6fe12ea..f998b7f 100644 --- a/Open-ILS/src/templates/opac/css/style.css.tt2 +++ b/Open-ILS/src/templates/opac/css/style.css.tt2 @@ -672,7 +672,8 @@ div.format_icon { padding-top: 1.5em; } -#rdetails_status td { +#rdetails_status td, +#course_material_table td { [% IF rtl == 't' -%] padding: 7px 13px 3px 0px; [%- ELSE %] @@ -685,7 +686,8 @@ div.format_icon { white-space: normal; } -#rdetails_status thead th { +#rdetails_status thead th, +#course_material_table thead th { [% IF rtl == 't' -%] padding: 13px 13px 13px 0px; text-align: right; @@ -698,7 +700,8 @@ div.format_icon { font-weight: bold; } -#rdetails_status tbody td { +#rdetails_status tbody td, +#course_material_table tbody td { [% IF rtl == 't' -%] padding-right: 13px; text-align: right; @@ -707,7 +710,8 @@ div.format_icon { text-align: left; [%- END %] } -#rdetails_status tbody td.copy_note { +#rdetails_status tbody td.copy_note +#course_material_table tbody { color: [% css_colors.primary %]; text-wrap:normal; white-space:pre-wrap !important; @@ -1875,7 +1879,7 @@ a.dash-link:hover { text-decoration: underline !important; } vertical-align: top; [% END -%] } -.rdetail-author-div { +.rdetail-author-div, .course-instructor-div { padding-bottom: 10px; display: inline-block; } @@ -2654,7 +2658,7 @@ a.preflib_change { display: none; } -.rdetail_authors_div { +.rdetail_authors_div, .course_instructors_div { margin-bottom: 1em; } @@ -2996,14 +3000,17 @@ a.preflib_change { #main-content { margin: 0 1px; } - #rdetails_status thead { + #rdetails_status thead, + #course_material_table thead { display: none; } - #rdetails_status tr { + #rdetails_status tr, + #course_material_table tr { display: block; margin-top: 3px; } - #rdetails_status td { + #rdetails_status td, + #course_material_table td { display: block; padding: 1px; } @@ -3444,3 +3451,13 @@ label[for*=expert_] .carousel .glide__arrow--left { left: -5em; } +.archived_course { + color: [% css_colors.text_badnews %]; + font-weight: bold; +} +.course_details { + padding-top: 1em; +} +.course_details_div { + padding-bottom: 1em; +} \ No newline at end of file diff --git a/Open-ILS/src/templates/opac/parts/course/body.tt2 b/Open-ILS/src/templates/opac/parts/course/body.tt2 new file mode 100644 index 0000000..d48ce11 --- /dev/null +++ b/Open-ILS/src/templates/opac/parts/course/body.tt2 @@ -0,0 +1,101 @@ +
    +
    +
    +

    + [% l(ctx.course.name) %] ([% ctx.course.course_number %]) + [% IF ctx.course.is_archived == 't' %] + + This course is inactive. + + [% END %] +

    + [% IF ctx.instructors.size %] +
    + Course Instructors: + [%- FOR instructor IN ctx.instructors; + instructorString = ''; + IF instructor.pref_family_name; + instructorString = instructorString _ instructor.pref_family_name _ ', '; + ELSE; + instructorString = instructorString _ instructor.family_name _ ', '; + END; + IF instructor.pref_first_given_name; + instructorString = instructorString _ instructor.pref_first_given_name; + ELSE; + instructorString = instructorString _ instructor.first_given_name; + END; + instructorString = instructorString _ ' (' _ l(instructor.usr_role) _ ')'; %] + [% instructorString %]. + [% END %] +
    + [% END %] + +
    +

    [% l('Course Details') %]

    +
    + [% l('Course Title') %]: + [% ctx.course.name %] +
    +
    + [% l('Course Number') %]: + [% ctx.course.course_number %] +
    +
    + [% l('Section Number') %]: + [% ctx.course.section_number %] +
    +
    + [% + owning_lib = ctx.get_aou(ctx.course.owning_lib); + lib_url = ctx.get_org_setting(owning_lib.id, 'lib.info_url'); + prefer_external_url = ctx.get_org_setting(owning_lib.id, 'lib.prefer_external_url'); + UNLESS lib_url && prefer_external_url; + lib_url = mkurl(ctx.opac_root _ '/library/' _ owning_lib.shortname, {}, 1); + END; + %] + [% l('Owning Library') %]: + [% owning_lib.name %] +
    +
    + + [%- UNLESS ctx.course.is_archived == 't' %] +
    +

    [% l('Course Materials') %]

    + + + + + + + + + + + + + + [% FOREACH copy_info IN ctx.course_materials %] + + + + + + + + + + [% END %] + +
    LocationCall NumberTitleBarcodeRelationshipStatusShelving Location
    [%- INCLUDE "opac/parts/library_name_link.tt2"; -%] + + + [% copy_info.call_number %] + + [% copy_info.title %] + + [% copy_info.barcode %][% copy_info.relationship %][% copy_info.status.name %][% copy_info.location.name %]
    +
    + [% END %] +
    +
    +
    diff --git a/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 b/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 index 9d126f6..f09e37b 100644 --- a/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 +++ b/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 @@ -54,6 +54,9 @@ IF has_copies or ctx.foreign_copies; [%- END %] [% l("Status") %] [% l("Due Date") %] + [%- IF ctx.get_org_setting(ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1 %] + [% l("Courses") %] + [%- END %] @@ -75,6 +78,10 @@ IF has_copies or ctx.foreign_copies; [% bib.target_copy.location.name | html %] [% bib.target_copy.status.name | html %] [% date.format(ctx.parse_datetime(copy_info.due_date, copy_info.circ_circ_lib),DATE_FORMAT) %] + [%- IF ctx.get_org_setting(CGI.param('loc') + OR ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1 %] + + [%- END %] [%- END; # FOREACH peer END; # FOREACH bib @@ -223,6 +230,11 @@ END; # FOREACH bib ELSE; '-'; END %] + [%- IF ctx.get_org_setting(ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1 %] + [%- FOREACH course IN copy_info.courses %] +
    [% course.course_number %]
    + [% END %] + [% END %] [% IF copy_info.notes; %] diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2 index de5e443..237b8d4 100644 --- a/Open-ILS/src/templates/opac/parts/result/table.tt2 +++ b/Open-ILS/src/templates/opac/parts/result/table.tt2 @@ -405,6 +405,17 @@ END; [% UNLESS rec.mmr_id; PROCESS "opac/parts/result/copy_counts.tt2"; END; %] + [%- IF ctx.get_org_setting(ctx.search_ou, 'circ.course_materials_opt_in') == 1 %] + [%- courseStrings = [] %] + [% FOREACH course IN rec.courses %] + [% courseString = course.name _ ' (' _ course.course_number _ ')' %] + [% courseStrings.push(courseString); %] + [% END %] + [% IF courseStrings.size > 0 %] + [% l('Associated Courses: ') %] + [% courseStrings.join(', ') %] + [% END %] + [% END %] [% IF rec.user_circulated %]
    [% l('Checked Out Before') %] -- 1.7.2.5