lp1849212 Angular Catalog Course integration
authorKyle Huckins <khuckins@catalyte.io>
Mon, 25 Nov 2019 22:06:04 +0000 (22:06 +0000)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 14 Sep 2020 22:16:29 +0000 (18:16 -0400)
- Add a column retrieving the names of courses linked to materials
when opted into the Course Reserves functionality.
- Expand the bib record summary when opted in to display all courses
associated with an item.
- Display associated courses on Search Results UI
- Move bulk of Associate Item funcitonality into Course Service

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Signed-off-by: Michele Morgan <mmorgan@noblenet.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>

Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.ts
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-reserves.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts
Open-ILS/src/eg2/src/app/staff/common.module.ts
Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html
Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts
Open-ILS/src/eg2/src/app/staff/share/course.service.ts

index 5fcad15..a6c1971 100644 (file)
@@ -88,53 +88,38 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
 
     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);                        
+            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.pcrud.search('acp', {barcode: 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();
+                    if (this.tempCallNumber) new_cn = this.tempCallNumber;
+                    this.courseSvc.updateItem(item,
+                        this.currentCourse.owning_lib(),
+                        new_cn, args.isModifyingCallNumber).then(resp => {
+                        this.fetchItem(item.id(), args.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));
                 });
index 64bd5dc..0ca63cf 100644 (file)
@@ -1,11 +1,11 @@
 import {NgModule} from '@angular/core';
 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 {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 '@eg/staff/share/course.service';
 
 @NgModule({
   declarations: [
@@ -13,6 +13,7 @@ import {CourseService} from '@eg/staff/share/course.service';
     CourseAssociateMaterialComponent
   ],
   imports: [
+    StaffCommonModule,
     AdminCommonModule,
     CourseReservesRoutingModule,
     ItemLocationSelectModule,
@@ -21,7 +22,6 @@ import {CourseService} from '@eg/staff/share/course.service';
   exports: [
   ],
   providers: [
-    CourseService
   ]
 })
 
index 8c511c3..ab44c6c 100644 (file)
   <span *ngIf="!context.holdable(copy)" i18n>No</span>
 </ng-template>
 
+<ng-template #courseTemplate let-copy="row">
+  <div *ngFor="let course of copy._courses">{{course.course_number()}}</div>
+</ng-template>
+
 <div class='eg-copies w-100 mt-3'>
   <eg-grid #copyGrid [dataSource]="gridDataSource" 
     [disableSelect]="true" [cellTextGenerator]="cellTextGenerator"
@@ -62,6 +66,9 @@
     <eg-grid-column i18n-label label="Holdable?" name="holdable" 
       [cellTemplate]="holdableTemplate" [cellContext]="copyContext">
     </eg-grid-column>
+    <eg-grid-column i18n-label label="Course" name="course"
+      [cellTemplate]="courseTemplate" *ngIf="usingCourseModule">
+    </eg-grid-column>
     <eg-grid-column i18n-label label="Status" path="copy_status">
     </eg-grid-column>
     <eg-grid-column i18n-label label="Due Date" path="due_date" datatype="timestamp">
index 9e288ee..b9f2ec6 100644 (file)
@@ -8,6 +8,7 @@ import {OrgService} from '@eg/core/org.service';
 import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {BroadcastService} from '@eg/share/util/broadcast.service';
+import {CourseService} from '@eg/staff/share/course.service';
 
 @Component({
   selector: 'eg-catalog-copies',
@@ -17,6 +18,7 @@ export class CopiesComponent implements OnInit {
 
     recId: number;
     initDone = false;
+    usingCourseModule = false;
     gridDataSource: GridDataSource;
     copyContext: any; // grid context
     @ViewChild('copyGrid', { static: true }) copyGrid: GridComponent;
@@ -33,6 +35,7 @@ export class CopiesComponent implements OnInit {
     cellTextGenerator: GridCellTextGenerator;
 
     constructor(
+        private course: CourseService,
         private net: NetService,
         private org: OrgService,
         private staffCat: StaffCatalogService,
@@ -43,6 +46,9 @@ export class CopiesComponent implements OnInit {
 
     ngOnInit() {
         this.initDone = true;
+        this.course.isOptedIn().then(res => {
+            this.usingCourseModule = res;
+        });
 
         this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
             // sorting not currently supported
@@ -94,6 +100,13 @@ export class CopiesComponent implements OnInit {
             pager.offset,
             this.staffCat.prefOrg ? this.staffCat.prefOrg.id() : null
         ).pipe(map(copy => {
+            this.org.settings('circ.course_materials_opt_in').then(res => {
+                if (res['circ.course_materials_opt_in']) {
+                    this.course.getCoursesFromMaterial(copy.id).then(courseList => {
+                        copy._courses = courseList;
+                    });
+                }
+            });
             copy.active_date = copy.active_date || copy.create_date;
             return copy;
         }));
index 8d839e1..85d75a2 100644 (file)
                     field="issn" joiner=","></eg-bib-display-field>
                 </div>
               </ng-container>
+              <ng-container *ngIf="has_course">
+                <div i18n>Associated Courses:
+                  {{courseNames.join(', ')}}</div>
+              </ng-container>
             </div>
           </div>
         </div>
index 664d366..94204c0 100644 (file)
@@ -10,6 +10,7 @@ import {CatalogSearchContext} from '@eg/share/catalog/search-context';
 import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
 import {StaffCatalogService} from '../catalog.service';
 import {BasketService} from '@eg/share/catalog/basket.service';
+import {CourseService} from '@eg/staff/share/course.service';
 
 @Component({
   selector: 'eg-catalog-result-record',
@@ -29,6 +30,8 @@ export class ResultRecordComponent implements OnInit, OnDestroy {
     searchContext: CatalogSearchContext;
     isRecordSelected: boolean;
     basketSub: Subscription;
+    has_course: boolean;
+    courseNames: any[] = [];
 
     constructor(
         private router: Router,
@@ -38,11 +41,13 @@ export class ResultRecordComponent implements OnInit, OnDestroy {
         private cat: CatalogService,
         private catUrl: CatalogUrlService,
         private staffCat: StaffCatalogService,
-        private basket: BasketService
+        private basket: BasketService,
+        private course: CourseService
     ) {}
 
     ngOnInit() {
         this.searchContext = this.staffCat.searchContext;
+        this.loadCourseInformation(this.summary.id)
         this.isRecordSelected = this.basket.hasRecordId(this.summary.id);
 
         // Watch for basket changes caused by other components
@@ -55,6 +60,23 @@ export class ResultRecordComponent implements OnInit, OnDestroy {
         this.basketSub.unsubscribe();
     }
 
+    loadCourseInformation(recordId) {
+        console.log("Entering loadCourseInformation");
+        this.course.isOptedIn().then(res => {
+            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.has_course = true;
+                });
+            } else {
+                this.has_course = false;
+            }
+        });
+    }
+
     orgName(orgId: number): string {
         return this.org.get(orgId).shortname();
     }
index a2f54cf..62c921c 100644 (file)
@@ -18,6 +18,7 @@ import {MultiSelectComponent} from '@eg/share/multi-select/multi-select.componen
 import {NotBeforeMomentValidatorDirective} from '@eg/share/validators/not_before_moment_validator.directive';
 import {PatronBarcodeValidatorDirective} from '@eg/share/validators/patron_barcode_validator.directive';
 import {BroadcastService} from '@eg/share/util/broadcast.service';
+import {CourseService} from './share/course.service'
 
 /**
  * Imports the EG common modules and adds modules common to all staff UI's.
@@ -72,6 +73,7 @@ export class StaffCommonModule {
                 AccessKeyService,
                 AudioService,
                 BroadcastService
+                CourseService
             ]
         };
     }
index 3fd9558..0dea4ba 100644 (file)
               <div class="flex-1">{{summary.record.edit_date() | date:'short'}}</div>
             </div>
           </li>
+          <ng-container *ngIf="expand && has_course">
+            <li class="list-group-item">
+              <div class="d-flex">
+                <div class="flex-1 font-weight-bold" i18n>Associated Courses</div>
+              </div>
+            </li>
+            <li class="list-group-item" *ngFor="let course of courses">
+              <div class="d-flex">
+                <div class="flex-1 font-weight-bold" i18n>Course Name:</div>
+                <div class="flex-3">{{course.name()}}</div>
+                <div class="flex-1 font-weight-bold" i18n>Course Number:</div>
+                <div class="flex-1">{{course.course_number()}}</div>
+                <div class="flex-1 font-weight-bold" i18n>Section Number:</div>
+                <div class="flex-1">{{course.section_number()}}</div>
+                <div class="flex-1 font-weight-bold" i18n>Owning Library:</div>
+                <div class="flex-1">{{this.org.get(course.owning_lib()).shortname()}}</div>
+              </div>
+            </li>
+          </ng-container>
         </ul>
       </div>
     </div><!-- col -->
index 84719fd..a227c07 100644 (file)
@@ -1,5 +1,6 @@
 import {Component, OnInit, Input} from '@angular/core';
 import {OrgService} from '@eg/core/org.service';
+import {CourseService} from '@eg/staff/share/course.service';
 import {BibRecordService, BibRecordSummary
     } from '@eg/share/catalog/bib-record.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
@@ -13,6 +14,8 @@ import {CatalogService} from '@eg/share/catalog/catalog.service';
 export class BibSummaryComponent implements OnInit {
 
     initDone = false;
+    has_course = false;
+    courses: any;
 
     // True / false if the display is vertically expanded
     private _exp: boolean;
@@ -33,6 +36,7 @@ export class BibSummaryComponent implements OnInit {
         this.summary = s;
         if (this.initDone && this.summary) {
             this.summary.getBibCallNumber();
+            this.loadCourseInformation(this.summary.record.id());
         }
     }
 
@@ -40,13 +44,15 @@ export class BibSummaryComponent implements OnInit {
         private bib: BibRecordService,
         private org: OrgService,
         private store: ServerStoreService,
-        private cat: CatalogService
+        private cat: CatalogService,
+        private course: CourseService
     ) {}
 
     ngOnInit() {
 
         if (this.summary) {
             this.summary.getBibCallNumber();
+            this.loadCourseInformation(this.summary.record.id());
         } else {
             if (this.recordId) {
                 this.loadSummary();
@@ -63,6 +69,7 @@ export class BibSummaryComponent implements OnInit {
     }
 
     loadSummary(): void {
+        this.loadCourseInformation(this.recordId);
         this.bib.getBibSummary(this.recordId).toPromise()
         .then(summary => {
             summary.getBibCallNumber();
@@ -70,6 +77,19 @@ export class BibSummaryComponent implements OnInit {
         });
     }
 
+    loadCourseInformation(record_id) {
+        this.org.settings('circ.course_materials_opt_in').then(setting => {
+            if (setting['circ.course_materials_opt_in']) {
+                this.course.fetchCopiesInCourseFromRecord(record_id).then(course_list => {
+                    this.courses = course_list;
+                    this.has_course = true;
+                });
+            } else {
+                this.has_course = false;
+            }
+        });
+    }
+
     orgName(orgId: number): string {
         if (orgId) {
             return this.org.get(orgId).shortname();
index c8b60cc..0856cfd 100644 (file)
@@ -1,9 +1,12 @@
+import {Injectable} from '@angular/core';
 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 {OrgService} from '@eg/core/org.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 
+@Injectable()
 export class CourseService {
 
     constructor(
@@ -11,9 +14,104 @@ export class CourseService {
         private evt: EventService,
         private idl: IdlService,
         private net: NetService,
+        private org: OrgService,
         private pcrud: PcrudService
     ) {}
 
+    isOptedIn(): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.org.settings('circ.course_materials_opt_in').then(res => {
+                resolve(res['circ.course_materials_opt_in']);
+            });
+        });
+    }
+    getCourses(course_ids?: Number[]): Promise<IdlObject[]> {
+        if (!course_ids) {
+            return this.pcrud.retrieveAll('acmc',
+                {}, {atomic: true}).toPromise();
+        } else {
+            return this.pcrud.search('acmc', {id: course_ids},
+                {}, {atomic: true}).toPromise();
+        }
+    }
+
+    getCoursesFromMaterial(copy_id): Promise<any> {
+        let id_list = [];
+        return new Promise((resolve, reject) => {
+
+            return this.pcrud.search('acmcm', {item: copy_id})
+            .subscribe(materials => {
+                if (materials) {
+                    id_list.push(materials.course());
+                }
+            }, err => {
+                console.log(err);
+                reject(err);
+            }, () => {
+                if (id_list.length) {
+                    return this.getCourses(id_list).then(courses => {
+                        resolve(courses);
+                    });
+                    
+                }
+            });
+        });
+    }
+
+    fetchCopiesInCourseFromRecord(record_id) {
+        let cp_list = [];
+        let course_list = [];
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.cat',
+                'open-ils.cat.asset.copy_tree.global.retrieve',
+                this.auth.token(), record_id
+            ).subscribe(copy_tree => {
+                copy_tree.forEach(cn => {
+                    cn.copies().forEach(cp => {
+                        cp_list.push(cp.id());
+                    });
+                });
+            }, err => reject(err),
+            () => {
+                resolve(this.getCoursesFromMaterial(cp_list));
+            });
+        });
+    }
+
+    // Creating a new acmcm Entry
+    associateMaterials(item, args) {
+        console.log("entering associateMaterials")
+        let material = this.idl.create('acmcm');
+        material.item(item.id());
+        material.course(args.currentCourse.id());
+        if (args.relationship) material.relationship(args.relationship);
+
+        // Apply temporary fields to the item
+        if (args.isModifyingStatus && args.tempStatus) {
+            material.original_status(item.status());
+            item.status(args.tempStatus);
+        }
+        if (args.isModifyingLocation && args.tempLocation) {
+            material.original_location(item.location());
+            item.location(args.tempLocation);
+        }
+        if (args.isModifyingCircMod) {
+            material.original_circ_modifier(item.circ_modifier());
+            item.circ_modifier(args.tempCircMod);
+            if (!args.tempCircMod) item.circ_modifier(null);
+        }
+        if (args.isModifyingCallNumber) {
+            material.original_callnumber(item.call_number());
+        }
+        let response = {
+            item: item,
+            material: this.pcrud.create(material).toPromise()
+        };
+
+        return response;
+    }
+
     disassociateMaterials(courses) {
         return new Promise((resolve, reject) => {
             let course_ids = [];