LP1849212: Angular Course Page improvements, OPAC course search
authorKyle Huckins <khuckins@catalyte.io>
Tue, 10 Dec 2019 19:03:17 +0000 (19:03 +0000)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 14 Sep 2020 22:17:00 +0000 (18:17 -0400)
- Apply Course Users functionality to Angular Course Page
Admin UI.
- Condense Course User functionality into its own component,
usable as a dialog and inline.
- Condense Course Material functionality into its own component,
usable as a dialog and inline.
- Add Return to Course List button
- Move Archive Course button above tabs
- Allow Editing of Course Users and Materials
- Set datatype of is_public on Course User to 'bool' from 'boolean'
- Relabel is_public on Course User to "OPAC Viewable?"
- Undo removal of fm-editor in Course List
- Users with the MANAGE_RESERVES permission who are opted
into the course module can view a new Course Info tab on
the item record.
- Course Info tab displays a list of all courses(with link
to the Admin Course Page) item is associated with, and a
list of all instructors associated with those courses.
- Improvements to open-ils.circ.course_users.retrieve.
- Improve visuals when logged in as administrator.
- Implement Course Search OPAC page, based on Advanced Catalog
Search
- Edit Searchbar to include entry for Course Search

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>

23 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-material.component.html
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-associate-users.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-associate-users.component.ts
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.ts
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-page.component.ts
Open-ILS/src/eg2/src/app/staff/share/course.service.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm
Open-ILS/src/templates/opac/course/results.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/course_search.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/parts/course_search/global_row.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/course_search/qtype_selector.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/record/copy_table.tt2
Open-ILS/src/templates/opac/parts/searchbar.tt2
Open-ILS/src/templates/staff/cat/item/t_course_pane.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/item/t_view.tt2
Open-ILS/web/js/ui/default/staff/cat/item/app.js

index db6946f..9e896c9 100644 (file)
@@ -3121,7 +3121,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="Course Members" name="members" oils_persist:virtual="true" reporter:datatype="link" />
             <field reporter:label="Course Materials" name="materials" oils_persist:virtual="true" reporter:datatype="link" />
             <field reporter:label="Non-Cataloged Course Materials" name="non_cat_materials" oils_persist:virtual="true" reporter:datatype="link" />
-            <field reporter:label="Is Archived?" name="is_archived" reporter:datatype="boolean" />
+            <field reporter:label="Is Archived?" name="is_archived" reporter:datatype="bool" />
         </fields>
         <links>
             <link field="owning_lib" reltype="has_a" key="id" map="" class="aou" />
@@ -3144,7 +3144,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="Course" name="course" reporter:datatype="link" />
             <field reporter:label="User" name="usr" reporter:datatype="link" />
             <field reporter:label="User Role" name="usr_role" reporter:datatype="text" />
-            <field reporter:label="Is Public Viewable?" name="is_public" reporter:datatype="boolean" />
+            <field reporter:label="OPAC Viewable?" name="is_public" reporter:datatype="bool" />
         </fields>
         <links>
             <link field="course" reltype="has_a" key="id" map="" class="acmc" />
index 65dfb71..a34e757 100644 (file)
-<eg-string #deleteFailedString i18n-text text="Disassociation of Course Material failed or was not allowed"></eg-string>
-<eg-string #deleteSuccessString i18n-text text="Disassociation of Course Material succeeded"></eg-string>
-<eg-string #successString i18n-text text="Association of Course Material succeeded"></eg-string>
-<eg-string #failedString i18n-text text="Association of Course Material failed or was not allowed"></eg-string>
-<eg-string #differentLibraryString i18n-text text="Material exists at a different library"></eg-string>
+
+<eg-string #materialDeleteFailedString i18n-text text="Disassociation of Course Material failed or was not allowed"></eg-string>
+<eg-string #materialDeleteSuccessString i18n-text text="Disassociation of Course Material succeeded"></eg-string>
+<eg-string #materialAddSuccessString i18n-text text="Association of Course Material succeeded"></eg-string>
+<eg-string #materialAddFailedString i18n-text text="Association of Course Material failed or was not allowed"></eg-string>
+<eg-string #materialEditSuccessString i18n-text text="Update of Course Material succeeded"></eg-string>
+<eg-string #materialEditFailedString i18n-text text="Update of Course Material failed or was not allowed"></eg-string>
+<eg-string #MaterialAddDifferentLibraryString i18n-text text="Material exists at a different library"></eg-string>
 
 <ng-template #dialogContent>
-  <div class="modal-header bg-info">
-    <h4 class="modal-title" i18n>Course Materials</h4>
-    <button type="button" class="close"
-      i18n-aria-label aria-label="Close" (click)="close()">
-      <span aria-hidden="true">&times;</span>
-    </button>
-  </div>
-  <div class="modal-body">
-    <div class="row mt-3">
-      <div class="col-md-4">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <span class="input-group-text" i18n>Barcode</span>
+<div class="modal-header bg-info"
+  [ngClass]="isDialog() ? 'modal-header' : 'alert mt-3'">
+  <h4 class="modal-title" i18n>Course Materials</h4>
+  <ng-container *ngIf="isDialog()">
+  <button type="button" class="close"
+    i18n-aria-label aria-label="Close" (click)="close()">
+    <span aria-hidden="true">&times;</span>
+  </button>
+  </ng-container>
+</div>
+<div [ngClass]="isDialog() ? 'modal-body' : ''">
+  <div class="row">
+    <div [ngClass]="isDialog() ? 'col-md-12' : 'col-md-4'">
+      <div class="row" [ngClass]="isDialog() ? '' : 'mt-3'">
+        <div class="d-flex"  [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <span class="input-group-text" i18n>Barcode</span>
+            </div>
+            <input type="text" class="flex-grow-1" [(ngModel)]="barcodeInput"
+              (click)="$event.target.select()" 
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              (keyup.enter)="associateItem(barcodeInput, relationshipInput)" />
           </div>
-          <input type="text" [(ngModel)]="barcodeInput" 
-            (click)="$event.target.select()"
-            (keyup.enter)="associateItem(barcodeInput, relationshipInput)" />
         </div>
-      </div>
-      <div class="col-md-5">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <span class="input-group-text" i18n>Relationship</span>
+        <div class="d-flex" [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12 mt-3'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <span class="input-group-text" i18n>Relationship</span>
+            </div>
+            <input type="text" [(ngModel)]="relationshipInput"
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              placeholder-i18n placeholder="e.g. Required"
+              class="flex-grow-1" />
           </div>
-          <input type="text" [(ngModel)]="relationshipInput" placeholder-i18n placeholder="e.g. Required" />
         </div>
       </div>
-      <div class="col-md-3">
-        <button class="btn btn-outline-dark" (click)="associateItem(barcodeInput, relationshipInput)" i18n [disabled]="!barcodeInput">Add Material</button>
+      <div class="row mt-3">
+        <div class="col-lg-12 text-right">
+          <button class="btn btn-primary" 
+            [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+            (click)="associateItem(barcodeInput, relationshipInput)"
+            i18n [disabled]="!barcodeInput">
+            Add Material
+          </button>
+        </div>
       </div>
-    </div>
-    <div class="row justify-content-center mt-3">
-      <div class="col">
-        <h5 i18n>The following fields will be applied to the material added, and reverted once the course is no longer associated with the material.</h5>
+      <div class="row justify-content-center mt-3">
+        <div class="col">
+          <h5 i18n>The following fields will be applied to the material 
+                added, and reverted once the course is no longer associated 
+                with the material.</h5>
+        </div>
       </div>
-    </div>
-    <div class="row mt-3">
-      <div class="col-md-6">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <div class="input-group-text">
-              <span i18n>Call Number</span>
+      <div class="row mt-3">
+        <div class="d-flex"  [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <div class="input-group-text">
+                <span i18n>Call Number</span>
+              </div>
             </div>
-          </div>
-          <input type="text" [(ngModel)]="tempCallNumber"
-            (input)="isModifyingCallNumber = true"/>
-          <div class="input-group-append">
-            <div class="input-group-text">
-              <input type="checkbox" [(ngModel)]="isModifyingCallNumber"
-                aria-label="Checkbox for setting a temporary Call Number" />
+            <input type="text" [(ngModel)]="tempCallNumber"
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              (input)="isModifyingCallNumber = true" class="flex-grow-1" />
+            <div class="input-group-append">
+              <div class="input-group-text">
+                <input type="checkbox" [(ngModel)]="isModifyingCallNumber"
+                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+                  aria-label="Checkbox for setting a temporary Call Number" />
+              </div>
             </div>
           </div>
         </div>
-      </div>
-      <div class="col-md-6">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <div class="input-group-text">
-              <span i18n>Circulation Modifier</span>
+        <div class="d-flex"  [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12 mt-3'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <div class="input-group-text">
+                <span i18n>Circulation Modifier</span>
+              </div>
             </div>
-          </div>
-          <eg-combobox i18n-placeholder placeholder="Circulation Modifier..."
-            idlClass="ccm" idlField="name" [displayTemplate]="idlClassLabel"
-            [asyncSupportsEmptyTermClick]="true"
-            (onChange)="tempCircMod = $event.id; isModifyingCircMod = true">
-          </eg-combobox>
-          <div class="input-group-append">
-            <div class="input-group-text">
-              <input type="checkbox" [(ngModel)]="isModifyingCircMod"
-                aria-label="Checkbox for setting a temporary Circulation Modifier" />
+            <eg-combobox i18n-placeholder placeholder="Circulation Modifier..."
+              idlClass="ccm" idlField="name" [displayTemplate]="idlClassLabel"
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              [asyncSupportsEmptyTermClick]="true" class="flex-grow-1" 
+              (onChange)="tempCircMod = $event.id; isModifyingCircMod = true">
+            </eg-combobox>
+            <div class="input-group-append">
+              <div class="input-group-text">
+                <input type="checkbox" [(ngModel)]="isModifyingCircMod"
+                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+                  aria-label="Checkbox for setting a temporary Circulation Modifier" />
+              </div>
             </div>
           </div>
         </div>
       </div>
-    </div>
-    <div class="row mt-3">
-      <div class="col-md-6">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <div class="input-group-text">
-              <span i18n>Item Status</span>
+      <div class="row mt-3">
+        <div class="d-flex"  [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <div class="input-group-text">
+                <span i18n>Item Status</span>
+              </div>
             </div>
-          </div>
-          <eg-combobox i18n-placeholder placeholder="Item Status..."
-            idlClass="ccs" idlField="name" [displayTemplate]="idlClassLabel"
-            [asyncSupportsEmptyTermClick]="true"
-            (onChange)="tempStatus = $event.id; isModifyingStatus = true">
-          </eg-combobox>
-          <div class="input-group-append">
-            <div class="input-group-text">
-              <input type="checkbox" [(ngModel)]="isModifyingStatus"
-                aria-label="Checkbox for setting a temporary Item Status" />
+            <eg-combobox i18n-placeholder placeholder="Item Status..."
+              idlClass="ccs" idlField="name" [displayTemplate]="idlClassLabel"
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              [asyncSupportsEmptyTermClick]="true" class="flex-grow-1" 
+              (onChange)="tempStatus = $event.id; isModifyingStatus = true">
+            </eg-combobox>
+            <div class="input-group-append">
+              <div class="input-group-text">
+                <input type="checkbox" [(ngModel)]="isModifyingStatus"
+                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+                  aria-label="Checkbox for setting a temporary Item Status" />
+              </div>
             </div>
           </div>
         </div>
-      </div>
-      <div class="col-md-6">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <div class="input-group-text">
-              <span i18n>Shelving Location</span>
+        <div class="d-flex"  [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12 mt-3'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <div class="input-group-text">
+                <span i18n>Shelving Location</span>
+              </div>
             </div>
-          </div>
-          <eg-item-location-select permFilter="MANAGE_RESERVES"
-            [(ngModel)]="tempLocation" (oninput)="isModifyingLocation = true"
-            (valueChange)="isModifyingLocation = true">
-          </eg-item-location-select>
-          <div class="input-group-append">
-            <div class="input-group-text">
-              <input type="checkbox" [(ngModel)]="isModifyingLocation"
-                aria-label="Checkbox for setting a temporary Shelving Location" />
+            <eg-item-location-select permFilter="MANAGE_RESERVES" class="flex-grow-1" 
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              [(ngModel)]="tempLocation" (valueChange)="isModifyingLocation = true">
+            </eg-item-location-select>
+            <div class="input-group-append">
+              <div class="input-group-text">
+                <input type="checkbox" [(ngModel)]="isModifyingLocation"
+                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+                  aria-label="Checkbox for setting a temporary Shelving Location" />
+              </div>
             </div>
           </div>
         </div>
       </div>
     </div>
 
-    <div class="mt-3">
-      <eg-grid #materialGrid [dataSource]="gridDataSource">
-        <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
+    <div class="mt-3" [ngClass]="isDialog() ? 'col-md-12' : 'col-md-8'">
+      <eg-grid #materialsGrid [dataSource]="materialsDataSource">
+        <eg-grid-toolbar-action label="Remove Selected" i18n-label (onClick)="deleteSelectedMaterials($event)">
         </eg-grid-toolbar-action>
-
-        <eg-grid-column path="id" [index]=true [hidden]="true" label="ID" i18n-label></eg-grid-column>
-        <eg-grid-column label="Barcode" i18n-label name="barcode" [cellTemplate]="barcodeCellTemplate"></eg-grid-column>
+        <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelectedMaterials($event)">
+        </eg-grid-toolbar-action>
+        <eg-grid-column path="_id" [index]=true [hidden]="true" label="ID" i18n-label></eg-grid-column>
+        <eg-grid-column label="Barcode" i18n-label name="card" [cellTemplate]="barcodeCellTemplate"></eg-grid-column>
         <eg-grid-column label="Title" i18n-label name="title" [cellTemplate]="titleCellTemplate"></eg-grid-column>
         <eg-grid-column path="call_number.label" label="Call Number" i18n-label></eg-grid-column>
         <eg-grid-column path="call_number.prefix.label" [hidden]="true" label="Call Number Prefix" i18n-label hidden></eg-grid-column>
       </eg-grid>
     </div>
   </div>
-  <ng-template #barcodeCellTemplate let-entry="row">
-    <span>
-      <a class="pl-1"
-        href="/eg/staff/cat/item/{{entry.id()}}">
-        {{entry.barcode()}}
-      </a>
-    </span>
-  </ng-template>
-  <ng-template #titleCellTemplate let-entry="row">
-    <span>
-      <a class="pl-1"
-        href="/eg/staff/cat/catalog/record/{{entry.call_number().record()}}">
-        {{entry._title}}
-      </a>
-    </span>
-  </ng-template>
-  <ng-template #idlClassLabel let-r="result" i18n>
-    {{r.label}}
-  </ng-template>
-</ng-template>
\ No newline at end of file
+</div>
+</ng-template>
+<ng-template #barcodeCellTemplate let-entry="row">
+<span>
+  <a class="pl-1"
+    href="/eg/staff/cat/item/{{entry.id()}}">
+    {{entry.barcode()}}
+  </a>
+</span>
+</ng-template>
+<ng-template #titleCellTemplate let-entry="row">
+<span>
+  <a class="pl-1"
+    href="/eg/staff/cat/catalog/record/{{entry.call_number().record()}}">
+    {{entry._title}}
+  </a>
+</span>
+</ng-template>
+<ng-template #idlClassLabel let-r="result" i18n>
+{{r.label}}
+</ng-template>
+
+<ng-container *ngIf="!isDialog()">
+  <!-- in "inline" mode, render the grid pane right here -->
+  <ng-container *ngTemplateOutlet="dialogContent">
+  </ng-container>
+</ng-container>
+
+<eg-fm-record-editor #editDialog
+  idlClass='acmcm'
+  [fieldOptions]="{course: {linkedSearchField: 'course_number'}}"
+  [preloadLinkedValues]="true"
+  hiddenFields="id,item,original_callnumber,original_status,original_location,original_circ_modifier,record">
+</eg-fm-record-editor>
\ No newline at end of file
index 404451a..749bf96 100644 (file)
@@ -1,4 +1,5 @@
 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';
@@ -12,6 +13,8 @@ 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';
 
@@ -21,14 +24,27 @@ import {CourseService} from '@eg/staff/share/course.service';
 })
 
 export class CourseAssociateMaterialComponent extends DialogComponent {
-
+    @Input() currentCourse: IdlObject;
+    @Input() courseId: any;
+    @Input() displayMode: String;
+    materials: any[] = [];
+    @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
     @ViewChild('materialsGrid', {static: true}) materialsGrid: GridComponent;
-    @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;
-    @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
-    @ViewChild('successString', { static: true }) successString: StringComponent;
-    @ViewChild('failedString', { static: true }) failedString: StringComponent;
-    @ViewChild('differentLibraryString', { static: true }) differentLibraryString: StringComponent;
-    @Input() table_name = "Course Materials";
+    @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('materialEditSuccessString', { static: true })
+        materialEditSuccessString: StringComponent;
+    @ViewChild('materialEditFailedString', { static: true })
+        materialEditFailedString: StringComponent;
+    @ViewChild('materialAddDifferentLibraryString', { static: true })
+        materialAddDifferentLibraryString: StringComponent;
+    materialsDataSource: GridDataSource;
     @Input() barcodeInput: String;
     @Input() relationshipInput: String;
     @Input() tempCallNumber: String;
@@ -39,53 +55,86 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
     @Input() isModifyingCircMod: Boolean;
     @Input() isModifyingCallNumber: Boolean;
     @Input() isModifyingLocation: Boolean;
-    currentCourse: IdlObject;
-    materials: any[];
-    gridDataSource: GridDataSource;
 
     constructor(
         private auth: AuthService,
+        private course: CourseService,
+        private event: EventService,
         private idl: IdlService,
         private net: NetService,
-        private pcrud: PcrudService,
         private org: OrgService,
-        private evt: EventService,
-        private modal: NgbModal,
+        private pcrud: PcrudService,
+        private route: ActivatedRoute,
         private toast: ToastService,
-        private courseSvc: CourseService
+        private modal: NgbModal
     ) {
         super(modal);
-        this.gridDataSource = new GridDataSource();
+        this.materialsDataSource = new GridDataSource();
     }
 
     ngOnInit() {
-        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
-            return this.fetchMaterials(pager);
+        this.materialsDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.loadMaterialsGrid(pager);
         }
     }
 
-    deleteSelected(items) {
-        let item_ids = [];
-        items.forEach(item => {
-            this.gridDataSource.data.splice(this.gridDataSource.data.indexOf(item, 0), 1);
-            item_ids.push(item.id())
+    isDialog(): boolean {
+        return this.displayMode === 'dialog';
+    }
+
+    loadMaterialsGrid(pager: Pager): Observable<any> {
+        return new Observable<any>(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();
         });
-        this.pcrud.search('acmcm', {course: this.currentCourse.id(), item: item_ids}).subscribe(material => {
-            material.isdeleted(true);
-            this.pcrud.autoApply(material).subscribe(
-                val => {
-                    this.courseSvc.resetItemFields(material, this.currentCourse.owning_lib());
-                    console.debug('deleted: ' + val);
-                    this.deleteSuccessString.current().then(str => this.toast.success(str));
+    }
+
+    editSelectedMaterials(itemFields: IdlObject[]) {
+        // Edit each IDL thing one at a time
+        const editOneThing = (item: IdlObject) => {
+            if (!item) { return; }
+
+            this.showEditDialog(item).then(
+                () => editOneThing(itemFields.shift()));
+        };
+
+        editOneThing(itemFields.shift());
+    }
+
+    showEditDialog(course_material: IdlObject): Promise<any> {
+        this.editDialog.mode = 'update';
+        this.editDialog.recordId = course_material._id;
+        return new Promise((resolve, reject) => {
+            this.editDialog.open({size: 'lg'}).subscribe(
+                result => {
+                    this.materialEditSuccessString.current()
+                        .then(str => this.toast.success(str));
+                    this.pcrud.retrieve('acmcm', result).subscribe(material => {
+                        if (material.course() != this.courseId) {
+                            this.materialsDataSource.data.splice(
+                                this.materialsDataSource.data.indexOf(course_material, 0), 1
+                            );
+                        } else {
+                            course_material._relationship = material.relationship();
+                        }
+                    });
+                    resolve(result);
                 },
-                err => {
-                    this.deleteFailedString.current()
+                error => {
+                    this.materialEditFailedString.current()
                         .then(str => this.toast.danger(str));
+                    reject(error);
                 }
             );
         });
     }
-
+    
     associateItem(barcode, relationship) {
         if (barcode) {
             let args = {
@@ -101,59 +150,56 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
                 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.courseSvc.associateMaterials(item, args);
+                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.courseSvc.updateItem(item,
-                        this.currentCourse.owning_lib(),
-                        new_cn, args.isModifyingCallNumber).then(resp => {
-                        this.fetchItem(item.id(), args.relationship);
+                    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.differentLibraryString.current().then(str => this.toast.warning(str));
+                            this.materialAddDifferentLibraryString.current()
+                            .then(str => this.toast.warning(str));
                         } else {
-                            this.successString.current().then(str => this.toast.success(str));
+                            this.materialAddSuccessString.current()
+                            .then(str => this.toast.success(str));
                         }
                     });
                 }, err => {
-                    this.failedString.current().then(str => this.toast.danger(str));
+                    this.materialAddFailedString.current()
+                    .then(str => this.toast.danger(str));
                 });
             });
         }
     }
 
-    fetchMaterials(pager: Pager): Observable<any> {
-        return new Observable<any>(observer => {
-            this.materials.forEach(material => {
-                this.fetchItem(material.item, material.relationship);
-            });
-            observer.complete();
+    deleteSelectedMaterials(items) {
+        let item_ids = [];
+        items.forEach(item => {
+            this.materialsDataSource.data.splice(this.materialsDataSource.data.indexOf(item, 0), 1);
+            item_ids.push(item.id())
         });
-    }
-
-    fetchItem(itemId, relationship): Promise<any> {
-        return new Promise((resolve, reject) => {
-            this.net.request(
-                'open-ils.circ',
-                'open-ils.circ.copy_details.retrieve',
-                this.auth.token(), itemId
-            ).subscribe(res => {
-                if (res) {
-                    let item = res.copy;
-                    item.call_number(res.volume);
-                    item.circ_lib(this.org.get(item.circ_lib()));
-                    item._title = res.mvr.title();
-                    item._relationship = relationship;
-                    this.gridDataSource.data.push(item);
+        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));
                 }
-            }, err => {
-                reject(err);
-            }, () => resolve(this.gridDataSource.data));
+            );
         });
     }
 }
\ No newline at end of file
index 079acd7..a0969bf 100644 (file)
@@ -1,49 +1,81 @@
-<eg-string #deleteFailedString i18n-text text="Disassociation of Course Material failed or was not allowed"></eg-string>
-<eg-string #deleteSuccessString i18n-text text="Disassociation of Course Material succeeded"></eg-string>
-<eg-string #successString i18n-text text="Association of Course Material succeeded"></eg-string>
-<eg-string #failedString i18n-text text="Association of Course Material failed or was not allowed"></eg-string>
-<eg-string #differentLibraryString i18n-text text="Material exists at a different library"></eg-string>
+<eg-string #userDeleteFailedString i18n-text text="Removal of User failed or was not allowed"></eg-string>
+<eg-string #userDeleteSuccessString i18n-text text="Removal of User succeeded"></eg-string>
+<eg-string #userAddSuccessString i18n-text text="Addition of User succeeded"></eg-string>
+<eg-string #userAddFailedString i18n-text text="Addition of User failed or was not allowed"></eg-string>
+<eg-string #userEditSuccessString i18n-text text="Update of User succeeded"></eg-string>
+<eg-string #userEditFailedString i18n-text text="Update of User failed or was not allowed"></eg-string>
 
 <ng-template #dialogContent>
-  <div class="modal-header bg-info">
-    <h4 class="modal-title" i18n>Course Users</h4>
-    <button type="button" class="close"
-      i18n-aria-label aria-label="Close" (click)="close()">
-      <span aria-hidden="true">&times;</span>
-    </button>
-  </div>
-  <div class="modal-body">
-    <div class="row mt-4">
-      <div class="col-md-5">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <span class="input-group-text" i18n>User Role</span>
+<div class="modal-header bg-info"
+  [ngClass]="isDialog() ? 'modal-header' : 'alert mt-3'">
+  <h4 class="modal-title" i18n>Course Users</h4>
+  <ng-container *ngIf="isDialog()">
+  <button type="button" class="close"
+    i18n-aria-label aria-label="Close" (click)="close()">
+    <span aria-hidden="true">&times;</span>
+  </button>
+  </ng-container>
+</div>
+<div [ngClass]="isDialog() ? 'modal-body' : ''">
+  <div class="row">
+    <div [ngClass]="isDialog() ? 'col-md-12' : 'col-md-4'">
+      <div class="row" [ngClass]="isDialog() ? '' : 'mt-3'">
+        <div class="d-flex"  [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <span class="input-group-text" i18n>Patron Barcode</span>
+            </div>
+            <input type="text" class="flex-grow-1" [(ngModel)]="userBarcode"
+              (click)="$event.target.select()" 
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              (keyup.enter)="associateUser(userBarcode)" />
           </div>
-          <input type="text" [(ngModel)]="userRoleInput" />
         </div>
-    </div>
-    <div class="row justify-content-center mt-3">
-    </div>
-      <div class="col-md-6">
-        <div class="input-group">
-          <div class="input-group-prepend">
-            <div class="input-group-text">
-              <span i18n>User</span>
+        <div class="d-flex" [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12 mt-3'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <span class="input-group-text" i18n>Role</span>
             </div>
+            <input type="text" [(ngModel)]="userRoleInput"
+              [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+              placeholder-i18n placeholder="e.g. Student, TA, Instructor..."
+              class="flex-grow-1" />
           </div>
-            <input type="text" [(ngModel)]="new_usr" />
+        </div>
+      </div>
+      <div class="row mt-3">
+        <div [ngClass]="isDialog() ? 'offset-md-6 col-md-4' : 'col-md-6'">
+          <div class="input-group">
+            <div class="input-group-prepend">
+              <div class="input-group-text">
+                <span i18n>Is Public Role?</span>
+              </div>
+            </div>
+            <div class="input-group-append">
+              <div class="input-group-text">
+                <input type="checkbox" [(ngModel)]="isPublicRole"
+                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+                  aria-label="Checkbox for allowing user to display on the OPAC Course Page" />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="text-right" [ngClass]="isDialog() ? 'col-md-2' : 'col-md-6'">
+          <button class="btn btn-primary"
+            [disabled]="currentCourse && currentCourse.is_archived() == 't'"
+            i18n [disabled]="!userBarcode" (click)="associateUser(userBarcode)">
+            Add User
+          </button>
         </div>
       </div>
     </div>
-    <div class="col-md-0">
-        <button class="btn btn-outline-dark" (click)="associateUsers(userRoleInput)" i18n [disabled]="!new_usr || !userRoleInput">Add User</button>
-    </div>
-    <div class="mt-3">
-      <eg-grid #usersGrid [dataSource]="gridDataSource">
-        <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
+    <div class="mt-3" [ngClass]="isDialog() ? 'col-md-12' : 'col-md-8'">
+      <eg-grid #usersGrid [dataSource]="usersDataSource">
+        <eg-grid-toolbar-action label="Remove Selected" i18n-label (onClick)="deleteSelectedUsers($event)">
+        </eg-grid-toolbar-action>
+        <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelectedUsers($event)">
         </eg-grid-toolbar-action>
-        <eg-grid-column label="id" [index]=true [hidden]="true" i18n-label></eg-grid-column>
-        <eg-grid-column label="User Role" name="_usr_role" i18n-label></eg-grid-column>
+        <eg-grid-column label="ID" path="_id" [index]=true [hidden]="true" i18n-label></eg-grid-column>
         <eg-grid-column label="First Name" name="first_given_name" i18n-label></eg-grid-column>
         <eg-grid-column label="Second Name" name="second_given_name" i18n-label></eg-grid-column>
         <eg-grid-column label="Last Name" name="family_name" i18n-label></eg-grid-column>
         <eg-grid-column label="Preferred Second Name" name="pref_second_given_name"[hidden]="true"  i18n-label></eg-grid-column>
         <eg-grid-column label="Preferred Family Name" name="pref_family_name"[hidden]="true"  i18n-label></eg-grid-column>
         <eg-grid-column label="Preferred Suffix" name="pref_suffix" [hidden]="true" i18n-label></eg-grid-column>
+        <eg-grid-column label="User Role" name="_role" i18n-label></eg-grid-column>
+        <eg-grid-column label="Viewable on OPAC" name="_is_public" i18n-label datatype="bool"></eg-grid-column>
       </eg-grid>
     </div>
-  </div>
\ No newline at end of file
+  </div>
+</div>
+</ng-template>
+
+<ng-container *ngIf="!isDialog()">
+  <!-- in "inline" mode, render the grid pane right here -->
+  <ng-container *ngTemplateOutlet="dialogContent">
+  </ng-container>
+</ng-container>
+
+<eg-fm-record-editor #editDialog
+  idlClass='acmcu'
+  [fieldOptions]="{course: {linkedSearchField: 'course_number'}}"
+  [preloadLinkedValues]="true"
+  hiddenFields="id,usr">
+</eg-fm-record-editor>
\ No newline at end of file
index b3be2c4..1b1ebfa 100644 (file)
-import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core';\r
-import {Observable, Observer, of} from 'rxjs';\r
-import {DialogComponent} from '@eg/share/dialog/dialog.component';\r
-import {AuthService} from '@eg/core/auth.service';\r
-import {NetService} from '@eg/core/net.service';\r
-import {EventService} from '@eg/core/event.service';\r
-import {OrgService} from '@eg/core/org.service';\r
-import {PcrudService} from '@eg/core/pcrud.service';\r
-import {Pager} from '@eg/share/util/pager';\r
-import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';\r
-import {GridDataSource} from '@eg/share/grid/grid';\r
-import {GridComponent} from '@eg/share/grid/grid.component';\r
-import {IdlObject, IdlService} from '@eg/core/idl.service';\r
-import {StringComponent} from '@eg/share/string/string.component';\r
-import {ToastService} from '@eg/share/toast/toast.service';\r
-import {CourseService} from '@eg/staff/share/course.service';\r
-\r
-@Component({\r
-    selector: 'eg-course-associate-users-dialog',\r
-    templateUrl: './course-associate-users.component.html'\r
-})\r
-\r
-export class CourseAssociateUsersComponent extends DialogComponent {\r
-\r
-    @ViewChild('usersGrid', {static: true}) usersGrid: GridComponent;\r
-    @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;\r
-    @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;\r
-    @ViewChild('successString', { static: true }) successString: StringComponent;\r
-    @ViewChild('failedString', { static: true }) failedString: StringComponent;\r
-    @ViewChild('differentLibraryString', { static: true }) differentLibraryString: StringComponent;\r
-    @Input() table_name = "Course Users";\r
-    @Input() userRoleInput: String;\r
-    \r
-    idl_class = "acmcu";\r
-    new_usr:any;\r
-    currentCourse: IdlObject;\r
-    users: any[];\r
-    gridDataSource: GridDataSource;\r
-\r
-    constructor(\r
-        private auth: AuthService,\r
-        private idl: IdlService,\r
-        private net: NetService,\r
-        private pcrud: PcrudService,\r
-        private org: OrgService,\r
-        private evt: EventService,\r
-        private modal: NgbModal,\r
-        private toast: ToastService,\r
-        private courseSvc: CourseService\r
-    ) {\r
-        super(modal);\r
-        this.gridDataSource = new GridDataSource();\r
-    }\r
-\r
-    ngOnInit() {\r
-    }\r
-\r
-    /**\r
-     * Takes the user id and creates a course user based around it.\r
-     * @param user_input The inputted user Id.\r
-     */\r
-    associateUsers(user_input) {\r
-        if (user_input) {\r
-            let user = this.idl.create('acmcu');\r
-            user.course(this.currentCourse.id());\r
-            user.usr(this.new_usr);\r
-            user.usr_role(user_input);\r
-            this.pcrud.create(user).subscribe(\r
-            val => {\r
-               console.debug('created: ' + val);\r
-               this.successString.current().then(str => this.toast.success(str));\r
-            }, err => {\r
-                this.failedString.current().then(str => this.toast.danger(str));\r
-            })\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Delete a user based on the id selected from the grid.\r
-     * @param users \r
-     */\r
-    deleteSelected(users) {\r
-        let user_ids = [];\r
-        users.forEach(user => {\r
-            this.gridDataSource.data.splice(this.gridDataSource.data.indexOf(user, 0), 1);\r
-            user_ids.push(user.id())\r
-        });\r
-        this.pcrud.remove(users).subscribe(user => {\r
-            this.pcrud.autoApply(user).subscribe(\r
-                val => {\r
-                    console.debug('deleted: ' + val);\r
-                    this.deleteSuccessString.current().then(str => this.toast.success(str));\r
-                },\r
-                err => {\r
-                    this.deleteFailedString.current()\r
-                        .then(str => this.toast.danger(str));\r
-                }\r
-            );\r
-        });\r
-    }\r
-\r
+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 {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';
+
+@Component({
+    selector: 'eg-course-associate-users-dialog',
+    templateUrl: './course-associate-users.component.html'
+})
+
+export class CourseAssociateUsersComponent extends DialogComponent implements OnInit {
+    @Input() currentCourse: IdlObject;
+    @Input() courseId: any;
+    @Input() displayMode: String;
+    users: any[] = [];
+    @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
+    @ViewChild('usersGrid', {static: true}) usersGrid: GridComponent;
+    @ViewChild('userDeleteFailedString', { static: true })
+        userDeleteFailedString: StringComponent;
+    @ViewChild('userDeleteSuccessString', { static: true })
+        userDeleteSuccessString: StringComponent;
+    @ViewChild('userAddSuccessString', { static: true })
+        userAddSuccessString: StringComponent;
+    @ViewChild('userAddFailedString', { static: true })
+        userAddFailedString: StringComponent;
+    @ViewChild('userEditSuccessString', { static: true })
+        userEditSuccessString: StringComponent;
+    @ViewChild('userEditFailedString', { static: true })
+        userEditFailedString: StringComponent;
+    usersDataSource: GridDataSource;
+    @Input() userBarcode: String;
+    @Input() userRoleInput: String;
+    @Input() isPublicRole: Boolean;
+
+    constructor(
+        private auth: AuthService,
+        private course: CourseService,
+        private event: EventService,
+        private idl: IdlService,
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private route: ActivatedRoute,
+        private toast: ToastService,
+        private modal: NgbModal
+    ) {
+        super(modal);
+        this.usersDataSource = new GridDataSource();
+    }
+
+    ngOnInit() {
+        this.usersDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.loadUsersGrid(pager);
+        }
+    }
+
+    isDialog(): boolean {
+        return this.displayMode === 'dialog';
+    }
+
+    loadUsersGrid(pager: Pager): Observable<any> {
+        return new Observable<any>(observer => {
+            this.course.getUsers(this.courseId).then(users => {
+                users.forEach(user => {
+                    this.course.fleshUser(user).then(fleshed_user => {
+                        this.usersDataSource.data.push(fleshed_user);
+                    });
+                    observer.complete();
+                });
+            });
+        });
+    }
+
+    associateUser(barcode) {
+        if (barcode) {
+            let args = {
+                currentCourse: this.currentCourse,
+                barcode: barcode,
+                role: this.userRoleInput,
+                is_public: this.isPublicRole
+            }
+
+            this.userBarcode = null;
+
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+                this.auth.token(), barcode
+            ).subscribe(patron => {
+                let associatedUser = this.course.associateUsers(patron, args).then(res => {
+                    this.course.fleshUser(res).then(fleshed_user => {
+                        this.usersDataSource.data.push(fleshed_user);
+                        this.userAddSuccessString.current().then(str => this.toast.success(str));
+                    });
+                }, err => {
+                    this.userAddFailedString.current().then(str => this.toast.danger(str));
+                });
+            });
+        }
+    }
+
+    editSelectedUsers(userFields: IdlObject[]) {
+        // Edit each IDL thing one at a time
+        const editOneThing = (user: IdlObject) => {
+            if (!user) { return; }
+
+            this.showEditDialog(user).then(
+                () => editOneThing(userFields.shift()));
+        };
+
+        editOneThing(userFields.shift());
+    }
+
+    showEditDialog(user: IdlObject): Promise<any> {
+        this.editDialog.mode = 'update';
+        this.editDialog.recordId = user._id;
+        return new Promise((resolve, reject) => {
+            this.editDialog.open({size: 'lg'}).subscribe(
+                result => {
+                    this.userEditSuccessString.current()
+                        .then(str => this.toast.success(str));
+                    this.pcrud.retrieve('acmcu', result).subscribe(u => {
+                        if (u.course() != this.courseId) {
+                            this.usersDataSource.data.splice(this.usersDataSource.data.indexOf(user, 0), 1);
+                        } else {
+                            user._is_public = u.is_public();
+                            user._role = u.usr_role();
+                        }
+                    });
+                    resolve(result);
+                },
+                error => {
+                    this.userEditFailedString.current()
+                        .then(str => this.toast.danger(str));
+                    reject(error);
+                }
+            );
+        });
+    }
+
+    deleteSelectedUsers(users) {
+        let user_ids = [];
+        users.forEach(user => {
+            this.usersDataSource.data.splice(this.usersDataSource.data.indexOf(user, 0), 1);
+            user_ids.push(user.id())
+        });
+        this.pcrud.search('acmcu', {course: this.courseId, usr: user_ids}).subscribe(user => {
+            user.isdeleted(true);
+            this.pcrud.autoApply(user).subscribe(
+                val => {
+                    console.debug('deleted: ' + val);
+                    this.userDeleteSuccessString.current().then(str => this.toast.success(str));
+                },
+                err => {
+                    this.userDeleteFailedString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            );
+        });
+    }
+
 }
\ No newline at end of file
index d7312a9..1a3d9c6 100644 (file)
@@ -4,17 +4,11 @@
 <eg-string #successString i18n-text text="{{table_name}} Update Succeeded"></eg-string>
 <eg-string #createString i18n-text text="{{table_name}} Was Created Successfully"></eg-string>
 <eg-string #deleteFailedString i18n-text text="Deletion of {{table_name}} failed or was not allowed"></eg-string>
-<eg-string #deleteSucailedString i18n-text text="Deletion of {{table_name}} was successful"></eg-string>
+<eg-string #deleteSuccessString i18n-text text="Deletion of {{table_name}} was successful"></eg-string>
 <eg-string #archiveFailedString i18n-text text="Archival of {{table_name}} failed or was not allowed"></eg-string>
 <eg-string #archiveSuccessString i18n-text text="Archival of {{table_name}} succeeded"></eg-string>
 <eg-string #flairTooltip i18n-text text="Limited Editing"></eg-string>
 
-<eg-course-associate-material-dialog #courseMaterialDialog>
-</eg-course-associate-material-dialog>
-
-<eg-course-associate-users-dialog #courseUserDialog>
-</eg-course-associate-users-dialog>
-
 <div class="w-100 mt-2 mb-2">
   <eg-grid #grid idlClass={{idl_class}}
     [dataSource]="grid_source"
     <eg-grid-toolbar-button
       label="Create {{table_name}}" (onClick)="createNew()" i18n-label>
     </eg-grid-toolbar-button>
-    <eg-grid-toolbar-action label="View Materials" i18n-label (onClick)="openMaterialsDialog($event)">
-    </eg-grid-toolbar-action>
     <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)">
     </eg-grid-toolbar-action>
     <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
     </eg-grid-toolbar-action>
     <eg-grid-toolbar-action label="Archive Selected" i18n-label (onClick)="archiveSelected($event)">
     </eg-grid-toolbar-action>
-    <eg-grid-toolbar-action label="View Users" i18n-label (onClick)="openUsersDialog($event)">
-    </eg-grid-toolbar-action>
+    <eg-grid-column label="ID" path="id" [index]=true [hidden]="true" i18n-label></eg-grid-column>
+    <eg-grid-column label="Title" name="name" i18n-label></eg-grid-column>
+    <eg-grid-column label="CourseNumber" name="course_number" i18n-label></eg-grid-column>
+    <eg-grid-column label="Section Number" name="section_number" i18n-label></eg-grid-column>
+    <eg-grid-column label="Is Archived?" name="is_archived" i18n-label datatype="bool"></eg-grid-column>
   </eg-grid>
 </div>
 
 <eg-fm-record-editor #editDialog
-  idlClass={{idl_class}}
+  idlClass="acmc"
   [preloadLinkedValues]="true"
-  hiddenFields="is_archived,id">
-</eg-fm-record-editor>
+  hiddenFields="id,is_archived">
+</eg-fm-record-editor>
\ No newline at end of file
index ebde776..631c19f 100644 (file)
@@ -12,6 +12,7 @@ import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {StringComponent} from '@eg/share/string/string.component';
 import {ToastService} from '@eg/share/toast/toast.service';
+import {LocaleService} from '@eg/core/locale.service';
 
 import {CourseAssociateMaterialComponent
     } from './course-associate-material.component';
@@ -51,6 +52,7 @@ export class CourseListComponent implements OnInit {
     constructor(
         private auth: AuthService,
         private courseSvc: CourseService,
+        private locale: LocaleService,
         private net: NetService,
         private org: OrgService,
         private pcrud: PcrudService,
@@ -89,28 +91,19 @@ export class CourseListComponent implements OnInit {
         };
     }
 
-    navigateToCoursePage(id: any) {
-        this.router.navigate(["/staff/admin/local/asset/course_list/" + id]);
-    }
-
-    showEditDialog(standingPenalty: IdlObject): Promise<any> {
-        this.editDialog.mode = 'update';
-        this.editDialog.recordId = standingPenalty['id']();
-        return new Promise((resolve, reject) => {
-            this.editDialog.open({size: this.dialog_size}).subscribe(
-                result => {
-                    this.successString.current()
-                        .then(str => this.toast.success(str));
-                    this.grid.reload();
-                    resolve(result);
-                },
-                error => {
-                    this.updateFailedString.current()
-                        .then(str => this.toast.danger(str));
-                    reject(error);
-                }
-            );
+    navigateToCoursePage(id_arr: IdlObject[]) {
+        if (typeof id_arr == 'number') id_arr = [id_arr];
+        let urls = [];
+        id_arr.forEach(id => {console.log(this.router.url);
+            urls.push([this.locale.currentLocaleCode() + this.router.url + '/' +  id]);
         });
+        if (id_arr.length == 1) {
+        this.router.navigate([this.router.url + '/' + id_arr[0]]);
+        } else {
+            urls.forEach(url => {
+                window.open(url)
+            });
+        }
     }
 
     createNew() {
@@ -134,12 +127,15 @@ export class CourseListComponent implements OnInit {
 
     editSelected(fields: IdlObject[]) {
         // Edit each IDL thing one at a time
-        const editOneThing = (field_object: IdlObject) => {
-            if (!field_object) { return; }
-            this.showEditDialog(field_object).then(
-                () => editOneThing(fields.shift()));
-        };
-        editOneThing(fields.shift());
+        let course_ids = [];
+        fields.forEach(field => {
+            if (typeof field['id'] == 'function') {
+                course_ids.push(field.id());
+            } else {
+                course_ids.push(field['id']);
+            }
+        });
+        this.navigateToCoursePage(course_ids);
     }
 
     archiveSelected(course: IdlObject[]) {
@@ -182,99 +178,5 @@ export class CourseListComponent implements OnInit {
             );
         });
     };
-
-    fetchCourseMaterials(course, currentMaterials): Promise<any> {
-        return new Promise((resolve, reject) => {
-            this.pcrud.search('acmcm', {course: course}).subscribe(res => {
-                if (res) this.fleshItemDetails(res.item(), res.relationship());
-            }, err => {
-                reject(err);
-            }, () => resolve(this.courseMaterialDialog.gridDataSource.data));
-        });
-    }
-
-    /**
-     * Uses the course id to fetch the different users associated with that course.
-     * @param course The course id
-     * @param currentMaterials 
-     */
-    fetchCourseUsers(course, currentMaterials): Promise<any> {
-        return new Promise((resolve, reject) => {
-            this.pcrud.search('acmcu', {course: course}).subscribe(res => {
-                if(res) this.fleshUserDetails(res.usr(), res.usr_role());
-            }, err => {
-                reject(err);
-            }, () => resolve(this.courseUserDialog.gridDataSource.data));
-        });
-    }
-
-    /**
-     * Takes the user id from the course table, and cross references that with the user table,
-     * to find the right data.
-     * @param userId The user id that is to be cross referenced.
-     * @param usr_role The user role that is to be added to the grid.
-     */
-    fleshUserDetails(userId, usr_role) {
-        return new Promise((resolve, reject) => {
-            this.pcrud.search("au", {id:userId}).subscribe(res => {
-                if (res) {
-                    let user = res;
-                    user._usr_role = usr_role;
-                    this.courseUserDialog.gridDataSource.data.push(user);
-                }
-            }, err => {
-                reject(err);
-            }, () => resolve(this.courseMaterialDialog.gridDataSource.data));
-        });
-    }
-
-    fleshItemDetails(itemId, relationship): Promise<any> {
-        return new Promise((resolve, reject) => {
-            this.net.request(
-                'open-ils.circ',
-                'open-ils.circ.copy_details.retrieve',
-                this.auth.token(), itemId
-            ).subscribe(res => {
-                if (res) {
-                    let item = res.copy;
-                    item.call_number(res.volume);
-                    item._title = res.mvr.title();
-                    item.circ_lib(this.org.get(item.circ_lib()));
-                    item._relationship = relationship;
-                    this.courseMaterialDialog.gridDataSource.data.push(item);
-                }
-            }, err => {
-                reject(err);
-            }, () => resolve(this.courseMaterialDialog.gridDataSource.data));
-        });
-    }
-
-    openMaterialsDialog(course) {
-        let currentMaterials = []
-        this.courseMaterialDialog.gridDataSource.data = [];
-        this.fetchCourseMaterials(course[0].id(), currentMaterials).then(res => {
-            this.courseMaterialDialog.currentCourse = course[0];
-            this.courseMaterialDialog.materials = currentMaterials;
-            this.courseMaterialDialog.open({size: 'lg'}).subscribe(res => {
-                console.log(res);
-            });
-        });
-    }
-
-    /**
-     * Opens the user dialog component using the course id
-     * @param course 
-     */
-    openUsersDialog(course) { 
-        let currentUsers = []
-        this.courseUserDialog.gridDataSource.data = [];
-        this.fetchCourseUsers(course[0].id(), currentUsers).then(res => {
-            this.courseUserDialog.currentCourse = course[0];
-            this.courseUserDialog.users = currentUsers;
-            this.courseUserDialog.open({size: 'lg'}).subscribe(res => {
-                console.log(res);
-            });
-        });
-    }
 }
 
index 0361526..2061807 100644 (file)
@@ -4,6 +4,21 @@
   [bannerStyle]="currentCourse.is_archived() == 't' ? 'alert-secondary' : null"
   [bannerIcon]="currentCourse.is_archived() == 't' ? 'lock' : null">
 </eg-staff-banner>
+
+<div class="row">
+  <div class="col text-right">
+    <button class="btn btn-danger"
+      (click)="archiveCourse()" [disabled]="currentCourse && currentCourse.is_archived() == 't'">
+      <i class="material-icons align-middle"
+        *ngIf="currentCourse && currentCourse.is_archived() == 't'">block</i>
+      <span class="align-middle">Archive Course</span>
+    </button>
+    <a class="btn btn-warning ml-3" routerLink="/staff/admin/local/asset/course_list" i18n>
+      <i class="material-icons align-middle">keyboard_return</i>
+      <span class="align-middle">Return to Course List</span>
+    </a>
+  </div>
+</div>
 <ngb-tabset class="mb-3">
 
   <!-- Edit Tab -->
     <ng-template ngbTabContent>
       <div class="row">
         <div class="col-lg-3 mt-3">
-          <button class="p-2 mb-3 btn btn-danger btn-lg"
-            (click)="archiveCourse()" [disabled]="currentCourse && currentCourse.is_archived() == 't'">
-            <i class="material-icons align-middle"
-              *ngIf="currentCourse && currentCourse.is_archived() == 't'">block</i>
-            <span class="align-middle">Archive Course</span>
-          </button>
         </div>
         <div class="col-lg-6 mt-3">
           <eg-fm-record-editor displayMode="inline"
   <!-- Materials Tab -->
   <ngb-tab title="Course Materials" i18n-title id="courseMaterials">
     <ng-template ngbTabContent>
-      <div class="row mt-3">
-        <!-- Input Sidebar -->
-        <div class="col-lg-4 mt-3">
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text" i18n>Barcode</span>
-                </div>
-                <input type="text" class="flex-grow-1" [(ngModel)]="barcodeInput"
-                  (click)="$event.target.select()" 
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                  (keyup.enter)="associateItem(barcodeInput, relationshipInput)" />
-              </div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text" i18n>Relationship</span>
-                </div>
-                <input type="text" [(ngModel)]="relationshipInput"
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                  placeholder-i18n placeholder="e.g. Required"
-                  class="flex-grow-1" />
-              </div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 text-right">
-              <button class="btn btn-primary" 
-                [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                (click)="associateItem(barcodeInput, relationshipInput)"
-                i18n [disabled]="!barcodeInput">
-                Add Material
-              </button>
-            </div>
-          </div>
-          <div class="row justify-content-center mt-3">
-            <div class="col">
-              <h5 i18n>The following fields will be applied to the material 
-                    added, and reverted once the course is no longer associated 
-                    with the material.</h5>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <div class="input-group-text">
-                    <span i18n>Call Number</span>
-                  </div>
-                </div>
-                <input type="text" [(ngModel)]="tempCallNumber"
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                  (input)="isModifyingCallNumber = true" class="flex-grow-1" />
-                <div class="input-group-append">
-                  <div class="input-group-text">
-                    <input type="checkbox" [(ngModel)]="isModifyingCallNumber"
-                      [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                      aria-label="Checkbox for setting a temporary Call Number" />
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <div class="input-group-text">
-                    <span i18n>Circulation Modifier</span>
-                  </div>
-                </div>
-                <eg-combobox i18n-placeholder placeholder="Circulation Modifier..."
-                  idlClass="ccm" idlField="name" [displayTemplate]="idlClassLabel"
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                  [asyncSupportsEmptyTermClick]="true" class="flex-grow-1" 
-                  (onChange)="tempCircMod = $event.id; isModifyingCircMod = true">
-                </eg-combobox>
-                <div class="input-group-append">
-                  <div class="input-group-text">
-                    <input type="checkbox" [(ngModel)]="isModifyingCircMod"
-                      [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                      aria-label="Checkbox for setting a temporary Circulation Modifier" />
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <div class="input-group-text">
-                    <span i18n>Item Status</span>
-                  </div>
-                </div>
-                <eg-combobox i18n-placeholder placeholder="Item Status..."
-                  idlClass="ccs" idlField="name" [displayTemplate]="idlClassLabel"
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                  [asyncSupportsEmptyTermClick]="true" class="flex-grow-1" 
-                  (onChange)="tempStatus = $event.id; isModifyingStatus = true">
-                </eg-combobox>
-                <div class="input-group-append">
-                  <div class="input-group-text">
-                    <input type="checkbox" [(ngModel)]="isModifyingStatus"
-                      [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                      aria-label="Checkbox for setting a temporary Item Status" />
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <div class="input-group-text">
-                    <span i18n>Shelving Location</span>
-                  </div>
-                </div>
-                <eg-item-location-select permFilter="MANAGE_RESERVES" class="flex-grow-1" 
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                  [(ngModel)]="tempLocation" (valueChange)="isModifyingLocation = true">
-                </eg-item-location-select>
-                <div class="input-group-append">
-                  <div class="input-group-text">
-                    <input type="checkbox" [(ngModel)]="isModifyingLocation"
-                      [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                      aria-label="Checkbox for setting a temporary Shelving Location" />
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-        <!-- End Input Sidebar -->
-        <div class="col-lg-8 mt-3">
-          <eg-grid #materialsGrid [dataSource]="materialsDataSource">
-            <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
-            </eg-grid-toolbar-action>
-
-            <eg-grid-column path="id" [index]=true [hidden]="true" label="ID" i18n-label></eg-grid-column>
-            <eg-grid-column label="Barcode" i18n-label name="barcode" [cellTemplate]="barcodeCellTemplate"></eg-grid-column>
-            <eg-grid-column label="Title" i18n-label name="title" [cellTemplate]="titleCellTemplate"></eg-grid-column>
-            <eg-grid-column path="call_number.label" label="Call Number" i18n-label></eg-grid-column>
-            <eg-grid-column path="call_number.prefix.label" [hidden]="true" label="Call Number Prefix" i18n-label hidden></eg-grid-column>
-            <eg-grid-column path="call_number.suffix.label" [hidden]="true" label="Call Number Suffix" i18n-label hidden></eg-grid-column>
-            <eg-grid-column path="circ_modifier" [hidden]="true" label="Circulation Modifier" i18n-label></eg-grid-column>
-            <eg-grid-column path="circ_lib.shortname" label="Circulation Library" i18n-label></eg-grid-column>
-            <eg-grid-column path="location.name" [hidden]="true" label="Shelving Location" i18n-label></eg-grid-column>
-            <eg-grid-column path="status.name" [hidden]="true" label="Copy Status" i18n-label></eg-grid-column>
-            <eg-grid-column path="_relationship" label="Relationship" i18n-label></eg-grid-column>
-          </eg-grid>
-        </div>
-      </div>
-      <ng-template #barcodeCellTemplate let-entry="row">
-        <span>
-          <a class="pl-1"
-            href="/eg/staff/cat/item/{{entry.id()}}">
-            {{entry.barcode()}}
-          </a>
-        </span>
-      </ng-template>
-      <ng-template #titleCellTemplate let-entry="row">
-        <span>
-          <a class="pl-1"
-            href="/eg/staff/cat/catalog/record/{{entry.call_number().record()}}">
-            {{entry._title}}
-          </a>
-        </span>
-      </ng-template>
-      <ng-template #idlClassLabel let-r="result" i18n>
-        {{r.label}}
-      </ng-template>
+      <eg-course-associate-material-dialog [courseId]="courseId" 
+        [currentCourse]="currentCourse" displayMode="inline">
+      </eg-course-associate-material-dialog>
     </ng-template>
   </ngb-tab>
 
   <!-- Users Tab -->
   <ngb-tab title="Course Users" i18n-title id="courseUsers">
     <ng-template ngbTabContent>
-      <div class="row mt-3">
-        <div class="col-lg-4 mt-3">
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text" i18n>Patron Barcode</span>
-                </div>
-                <input type="text" class="flex-grow-1" [(ngModel)]="userBarcode"
-                  (click)="$event.target.select()" 
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'" />
-                  <!--(keyup.enter)="associateUser(userBarcode)"-->
-              </div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 d-flex">
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text" i18n>Role</span>
-                </div>
-                <input type="text" [(ngModel)]="userRoleInput"
-                  [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                  placeholder-i18n placeholder="e.g. Student, TA, Instructor..."
-                  class="flex-grow-1" />
-              </div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-lg-12 text-right">
-              <!--(click)="associateUser(userBarcode)"-->
-              <button class="btn btn-primary"
-                [disabled]="currentCourse && currentCourse.is_archived() == 't'"
-                i18n [disabled]="!userBarcode">
-                Add User
-              </button>
-            </div>
-          </div>
-        </div>
-        <div class="col-lg-8 mt-3">
-          <!-- eg-grid -->
-        </div>
-      </div>
+      <eg-course-associate-users-dialog [courseId]="courseId" 
+        [currentCourse]="currentCourse" displayMode="inline">
+      </eg-course-associate-users-dialog>
     </ng-template>
   </ngb-tab>
 </ngb-tabset>
 
 <eg-string #archiveFailedString i18n-text text="Archival of Course failed or was not allowed"></eg-string>
-<eg-string #archiveSuccessString i18n-text text="Archival of Course succeeded"></eg-string>
-<eg-string #materialDeleteFailedString i18n-text text="Disassociation of Course Material failed or was not allowed"></eg-string>
-<eg-string #materialDeleteSuccessString i18n-text text="Disassociation of Course Material succeeded"></eg-string>
-<eg-string #materialAddSuccessString i18n-text text="Association of Course Material succeeded"></eg-string>
-<eg-string #materialAddFailedString i18n-text text="Association of Course Material failed or was not allowed"></eg-string>
-<eg-string #MaterialAddDifferentLibraryString i18n-text text="Material exists at a different library"></eg-string>
\ No newline at end of file
+<eg-string #archiveSuccessString i18n-text text="Archival of Course succeeded"></eg-string>
\ No newline at end of file
index 3765496..6818a11 100644 (file)
@@ -16,6 +16,8 @@ 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';
+import {CourseAssociateUsersComponent} from './course-associate-users.component';
+import {CourseAssociateMaterialComponent} from './course-associate-material.component';
 
 @Component({
     selector: 'eg-course-page',
@@ -26,6 +28,11 @@ export class CoursePageComponent implements OnInit {
 
     currentCourse: IdlObject;
     courseId: any;
+
+    @ViewChild('courseMaterialDialog', {static: true})
+        private courseMaterialDialog: CourseAssociateMaterialComponent;
+    @ViewChild('courseUserDialog', {static: true})
+        private courseUserDialog: CourseAssociateUsersComponent;
     
     // Edit Tab
     @ViewChild('archiveFailedString', { static: true })
@@ -34,44 +41,18 @@ export class CoursePageComponent implements OnInit {
         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 net: NetService,
         private org: OrgService,
         private pcrud: PcrudService,
         private route: ActivatedRoute,
         private toast: ToastService
     ) {
-        this.materialsDataSource = new GridDataSource();
     }
 
     ngOnInit() {
@@ -79,9 +60,6 @@ export class CoursePageComponent implements OnInit {
         this.course.getCourses([this.courseId]).then(course => {
             this.currentCourse = course[0];
         });
-        this.materialsDataSource.getRows = (pager: Pager, sort: any[]) => {
-            return this.loadMaterialsGrid(pager);
-        }
     }
 
     // Edit Tab
@@ -100,84 +78,5 @@ export class CoursePageComponent implements OnInit {
     }
 
     // Materials Tab
-    loadMaterialsGrid(pager: Pager): Observable<any> {
-        return new Observable<any>(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
index de946c4..6089bf2 100644 (file)
@@ -45,20 +45,33 @@ export class CourseService {
         }
     }
 
-    fleshMaterial(itemId, relationship?): Promise<any> {
+    getUsers(course_ids?: Number[]): Promise<IdlObject[]> {
+        if (!course_ids) {
+            return this.pcrud.retrieveAll('acmcu',
+                {}, {atomic: true}).toPromise();
+        } else {
+            return this.pcrud.search('acmcu', {course: course_ids},
+                {}, {atomic: true}).toPromise();
+        }
+    }
+
+    fleshMaterial(material): Promise<any> {
+        console.log(material)
         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
+                this.auth.token(), material.item()
             ).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;
+                    item._id = material.id();
+                    if (material.relationship())
+                        item._relationship = material.relationship();
                 }
             }, err => {
                 reject(err);
@@ -66,6 +79,24 @@ export class CourseService {
         });
     }
 
+    fleshUser(course_user): Promise<any> {
+        return new Promise((resolve, reject) => {
+            let user = this.idl.create('au');
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.user.fleshed.retrieve',
+                this.auth.token(), course_user.usr()
+            ).subscribe(patron => {
+                user = patron;
+                user._id = course_user.id();
+                if (course_user.usr_role()) user._role = course_user.usr_role();
+                if (course_user.is_public()) user._is_public = course_user.is_public();
+            }, err => {
+                reject(err);
+            }, () => resolve(user));
+        });
+    }
+
     getCoursesFromMaterial(copy_id): Promise<any> {
         let id_list = [];
         return new Promise((resolve, reject) => {
@@ -145,6 +176,15 @@ export class CourseService {
         return response;
     }
 
+    associateUsers(patron_id, args) {
+        let new_user = this.idl.create('acmcu');
+        if (args.is_public) new_user.is_public(args.is_public);
+        if (args.role) new_user.usr_role(args.role);
+        new_user.course(args.currentCourse.id());
+        new_user.usr(patron_id);
+        return this.pcrud.create(new_user).toPromise()
+    }
+
     disassociateMaterials(courses) {
         return new Promise((resolve, reject) => {
             let course_ids = [];
@@ -181,7 +221,7 @@ export class CourseService {
             this.pcrud.search('acmcu', {user: user_ids}).subscribe(user => {
                 user.course(user_ids);
                 this.pcrud.autoApply(user).subscribe(res => {
-                    console.log(res);
+                    console.debug(res);
                 }, err => {
                     reject(err);
                 }, () => {
index 0a7c80f..c48f6ce 100644 (file)
@@ -1063,15 +1063,6 @@ __PACKAGE__->register_method(
         @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();
@@ -1126,6 +1117,15 @@ sub fetch_course_materials {
     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();
@@ -1169,7 +1169,7 @@ sub fetch_course_users {
         unless ($self->api_name =~ /\.staff/) and $e->allowed('MANAGE_RESERVES');
     
     
-    $users->{list} =  $e->search_asset_course_module_course_users($filter);
+    $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;
@@ -1183,10 +1183,15 @@ sub fetch_course_users {
         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;
     }
index 2a9548f..c252552 100644 (file)
@@ -145,6 +145,8 @@ sub load {
 
     return $self->load_simple("home") if $path =~ m|opac/home|;
     return $self->load_simple("css") if $path =~ m|opac/css|;
+    return $self->load_cresults if $path =~ m|opac/course/results|;
+    return $self->load_simple("course_search") if $path =~ m|opac/course_search|;
     return $self->load_simple("advanced") if
         $path =~ m:opac/(advanced|numeric|expert):;
 
index bd340ea..496c043 100644 (file)
@@ -39,4 +39,200 @@ sub load_course {
         {course => $course_id}
     );
     return Apache2::Const::OK;
+}
+
+sub load_cresults {
+    my $self = shift;
+    my %args = @_;
+    my $internal = $args{internal};
+    my $cgi = $self->cgi;
+    my $ctx = $self->ctx;
+    my $e = $self->editor;
+    my $limit = 10;
+
+    $ctx->{page} = 'cresult' unless $internal;
+    $ctx->{ids} = [];
+    $ctx->{courses} = [];
+    $ctx->{hit_count} = 0;
+    $ctx->{search_ou} = $self->_get_search_lib();
+    my $page = $cgi->param('page') || 0;
+    my $offset = $page * $limit;
+    my $results;
+    $ctx->{page_size} = $limit;
+    $ctx->{search_page} = $page;
+    $ctx->{pagable_limit} = 50;
+
+    # fetch this page plus the first hit from the next page
+    if ($internal) {
+        $limit = $offset + $limit + 1;
+        $offset = 0;
+    }
+
+    my ($user_query, $query, @queries, $modifiers) = _prepare_course_search($cgi, $ctx);
+
+    return Apache2::Const::OK unless $query;
+
+    $ctx->{user_query} = $user_query;
+    $ctx->{processed_search_query} = $query;
+    my $search_args = {};
+    my $course_numbers = ();
+    
+    my $where_clause;
+    my $and_terms = [];
+    my $or_terms = [];
+
+    # Handle is_archived checkbox and Org Selector
+    my $search_orgs = $U->get_org_descendants($ctx->{search_ou});
+    push @$and_terms, {'owning_lib' => $search_orgs};
+    push @$and_terms, {'-not' => {'+acmc' => 'is_archived'}} unless $query =~ qr\#include_archived\;
+
+    # Now let's push the actual queries
+    for my $query_obj (@queries) {
+        my $type = $query_obj->{'qtype'};
+        my $query = $query_obj->{'value'};
+        my $bool = $query_obj->{'bool'};
+        my $contains = $query_obj->{'contains'};
+        my $operator = ($contains eq 'nocontains') ? '!~*' : '~*';
+        my $search_query;
+        if ($type eq 'instructor') {
+            my $in = ($contains eq 'nocontains') ? "not in" : "in";
+            $search_query = {'id' => {$in => {
+                'from' => 'acmcu',
+                'select' => {'acmcu' => ['course']},
+                'where' => {'usr' => {'in' => {
+                    'from' => 'au',
+                    'select' => {'au' => ['id']},
+                    'where' => {
+                        '-or' => [
+                            {'pref_first_given_name' => {'~*' => $query}},
+                            {'first_given_name' => {'~*' => $query}},
+                            {'pref_second_given_name' => {'~*' => $query}},
+                            {'second_given_name' => {'~*' => $query}},
+                            {'pref_family_name' => {'~*' => $query}},
+                            {'family_name' => {'~*' => $query}}
+                        ]
+                    }
+                }}}
+            }}};
+        } else {
+            $search_query = ($contains eq 'nocontains') ?
+              {'+acmc' => { $type => {$operator => $query}}} :
+              {$type => {$operator => $query}};
+        }
+
+        if ($bool eq 'or') {
+            push @$or_terms, $search_query;
+        }
+
+        if ($bool eq 'and') {
+            push @$and_terms, $search_query;
+        }
+    }
+
+    if ($or_terms and @$or_terms > 0) {
+        if ($and_terms and @$and_terms > 0) {
+            push @$or_terms, $and_terms;
+        }
+        $where_clause = {'-or' => $or_terms};
+    } else {
+        $where_clause = {'-and' => $and_terms};
+    }
+
+    my $hits = $e->json_query({
+        "from" => "acmc",
+        "select" => {"acmc" => ['id']},
+        "where" => $where_clause
+    });
+
+    my $results = $e->json_query({
+        "from" => "acmc",
+        "select" => {"acmc" => [
+            'id',
+            'name',
+            'course_number',
+            'section_number',
+            'is_archived',
+            'owning_lib'
+        ]},
+        "limit" => $limit,
+        "offset" => $offset,
+        "order_by" => {"acmc" => ['id']},
+        "where" => $where_clause
+    });
+    for my $result (@$results) {
+        push @{$ctx->{courses}}, {
+            id => $result->{id},
+            course_number => $result->{course_number},
+            section_number => $result->{section_number},
+            owning_lib => $result->{owning_lib},
+            name => $result->{name},
+            is_archived => $result->{is_archived},
+            instructors => []
+        }
+    }
+
+    #$ctx->{courses} = $@courses;#[{id=>10, name=>"test", course_number=>"LIT"}];
+    $ctx->{hit_count} = @$hits || 0;
+    #$ctx->{hit_count} = 0;
+    return Apache2::Const::OK;
+}
+
+sub _prepare_course_search {
+    my ($cgi, $ctx) = @_;
+
+    my ($user_query, @queries) = _prepare_query($cgi);
+    my $modifiers;
+    $user_query //= '';
+
+    my $query = $user_query;
+    $query .= ' ' . $ctx->{global_search_filter} if $ctx->{global_search_filter};
+
+    foreach ($cgi->param('modifier')) {
+        $query = ('#' . $_ . ' ' . $query) unless $query =~ qr/\#\Q$_/;
+
+    }
+    # filters
+    foreach (grep /^fi:/, $cgi->param) {
+        /:(-?\w+)$/ or next;
+        my $term = join(",", $cgi->param($_));
+        $query .= " $1($term)" if length $term;
+    }
+
+    return () unless $query;
+
+    return ($user_query, $query, @queries);
+}
+
+sub _prepare_query {
+    my $cgi = shift;
+
+    return $cgi->param('query') unless $cgi->param('qtype');
+
+    my %parts;
+    my @part_names = qw/qtype contains query bool modifier/;
+    $parts{$_} = [ $cgi->param($_) ] for (@part_names);
+
+    my $full_query = '';
+    my @queries;
+    for (my $i = 0; $i < scalar @{$parts{'qtype'}}; $i++) {
+        my ($qtype, $contains, $query, $bool, $modifier) = map { $parts{$_}->[$i] } @part_names;
+        next unless $query =~ /\S/;
+
+        $contains = "" unless defined $contains;
+
+        push @queries, {
+            contains => $contains,
+            bool => $bool,
+            qtype => $qtype,
+            value => $query
+        };
+
+        $bool = ($bool and $bool eq 'or') ? '||' : '&&';
+
+        $query = "$qtype:$query";
+
+        $full_query = $full_query ? "($full_query $bool $query)" : $query;
+    }
+
+    return ($full_query, @queries);
 }
\ No newline at end of file
diff --git a/Open-ILS/src/templates/opac/course/results.tt2 b/Open-ILS/src/templates/opac/course/results.tt2
new file mode 100644 (file)
index 0000000..f8ae3b9
--- /dev/null
@@ -0,0 +1,114 @@
+[%- PROCESS "opac/parts/header.tt2";
+    WRAPPER "opac/parts/base.tt2";
+    INCLUDE "opac/parts/topnav.tt2";
+    ctx.page_title = l("Course Search Results");
+    page = CGI.param('page');
+    page = page.match('^\d+$') ? page : 0; # verify page is a sane value
+
+    page_count = (!ctx.page_size.defined || !ctx.hit_count.defined || ctx.page_size == 0) ? 1 : POSIX.ceil(ctx.hit_count / ctx.page_size);
+
+    # We don't want search engines indexing search results
+    ctx.metalinks.push('<meta name="robots" content="noindex,follow">');
+
+    PROCESS "opac/parts/misc_util.tt2";
+    PROCESS get_library;
+    ctx.result_start = 1 + ctx.page_size * page;
+    ctx.result_stop = ctx.page_size * (page + 1);
+    IF ctx.result_stop > ctx.hit_count; ctx.result_stop = ctx.hit_count; END;
+
+    result_count = ctx.result_start;
+-%]
+<h2 class="sr-only">[% l('Course Search Results') %]</h2>
+[% INCLUDE "opac/parts/searchbar.tt2" %]
+<div class="almost-content-wrapper">
+  <div id="results_header_bar">
+    <div id="results_header_inner">
+      <div class="results_header_btns">
+        <a href="[% mkurl(ctx.opac_root _ '/course_search', {$loc_name => loc_value}, 1) %]">[% l('Another Search') %]</a>
+      </div>
+    </div>
+  </div>
+</div>
+<div id="content-wrapper">
+  <div id="main-content">
+    <div id="results-page">
+      [% PROCESS "opac/parts/result/paginate.tt2" %] 
+      [% ctx.results_count_header = PROCESS results_count_header;
+    ctx.results_count_header %]
+      <div id="result_table_div">
+      <div id="result_block" class="result_block_visible">
+
+      <table id="result_table_table" title="[% l('Search Results') %]"
+        class="table_no_border_space table_no_cell_pad">
+        <thead class="sr-only">
+          <tr>
+            <th>[% l('Search result number') %]</th>
+            <th>[% l('Course details') %]</th>
+          </tr>
+        </thead>
+        <tbody id="result_table">
+        [% FOR course IN ctx.courses %]
+          [% course_url_path = ctx.opac_root _ '/course/' _ course.id; %]
+          [% # Do not pass "advanced params" to result detail code.
+          # Instead, pass the scrubed query in one-line form
+          del_parms = del_parms.merge(['query', 'bool', 
+                   'qtype', 'contains', '_adv']);
+          add_parms.import(
+           {query => ctx.naive_query_scrub(ctx.user_query)} );
+                  %]
+          <tr class="result_table_row">
+            <td class="results_row_count" name="results_row_count">
+              [% result_count; result_count = result_count + 1 %].
+            </td>
+            <td class="result_table_pic_header"></td>
+            <td class="result_table_title_cell" name="result_table_title_cell">
+              <div class="result_metadata">
+                <a class="record_title search_link" name="course_[% course.id %]"
+                  href="[% mkurl(course_url_path) %]"
+                  [% html_text_attr('title', l('Display course details for "[_1]"', course.name)) %]>
+                  [% course.course_number %]:  [% l(course.name) %]
+                </a>
+                <div>
+                  [% FOR instructor IN course.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; %]
+                  <a title="[% l('Perform an Instructor Search') %]"
+                    class="record_author"
+                    href="[%
+                       mkurl(ctx.opac_root _ '/results', {qtype => 'instructor', query => instructorString})
+                    %]" rel="nofollow" vocab="">
+                  [% instructorString %] ([% l(instructor.usr_role) %])</a>. 
+                  [% END %]
+                </div>
+                <div>
+                  <span><strong>[% l('Course Number') %]</strong>: [% course.course_number %]</span>
+                </div>
+                <div>
+                  <span><strong>[% l('Section Number') %]</strong>: [% course.section_number %]</span>
+                </div>
+              </div>
+            </td>
+            <td>
+              [% ctx.get_aou(course.owning_lib).name %]
+            </td>
+          </tr>
+        [% END %]
+        </tbody>
+      </table>
+      </div>
+      </div>
+    </div>
+    <div class="common-full-pad"></div>
+  </div>
+  <br class="clear-both" />
+</div>
+[%- END %]
diff --git a/Open-ILS/src/templates/opac/course_search.tt2 b/Open-ILS/src/templates/opac/course_search.tt2
new file mode 100644 (file)
index 0000000..979930a
--- /dev/null
@@ -0,0 +1,95 @@
+[%- PROCESS "opac/parts/header.tt2";
+    WRAPPER "opac/parts/base.tt2";
+    INCLUDE "opac/parts/topnav.tt2";
+    ctx.page_title = l("Course Search");
+
+    PROCESS "opac/parts/misc_util.tt2";
+    PROCESS get_library;
+    ctx.metalinks.push('<meta name="robots" content="noindex,follow">');
+-%]
+
+<h2 class="sr-only">[% l('Course Search') %]</h2>
+
+<div id="search-wrapper">
+  <div id="search-box">
+    <span class="search_catalog_lbl mobile_hide">[% l('Search the Catalog') %]</span>
+    <span class="adv_search_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/home') %]"
+      id="home_adv_search_link">[%l('Basic Search')%]</a></span>
+    <span class="adv_search_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/advanced', {}, depart_list) %]"
+      id="home_adv_search_link">[%l('Advanced Search')%]</a></span>
+    <span class="browse_the_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/browse') %]">[%
+      l('Browse the Catalog')%]</a></span>
+    [% INCLUDE 'opac/parts/cart.tt2' %]
+  </div>
+</div>
+
+<div id="content-wrapper">
+  <div id="main-content">
+  <form action="[% ctx.opac_root %]/course/results" method="get">
+    <!-- TODO: Refactor simple.js addSearchRow to better handle
+      tables with IDs that aren't adv_... -->
+    <div class="header_middle" id="adv_search_input">
+      [% l("Course Search Input") %]
+    </div>
+
+    <div id="adv_search_filters" class="adv_filter_block">
+      <div class="adv_filter_block_item">
+        <div>
+          <strong><label for="ord.id">[% l('Search Library') %]</label></strong>
+        </div>
+        <div>
+          [% PROCESS "opac/parts/org_selector.tt2" %]
+          [% INCLUDE build_org_selector show_loc_groups=1 id=org.id %]
+          <span class="course_search_archived">
+            <input type="checkbox" name="modifier" value="include_archived"
+              [% CGI.param('modifier').grep('include_archived').size ? ' checked="checked"' : '' %]
+              id="opac.course_result.include_archived" />
+            <label for="opac.course_result.include_archived">
+              [% l('Include Archived Courses?') %]
+            </label>
+          </span>
+        </div>
+      </div>
+    </div>
+    
+    <div class="advanced_div">
+      <div id='adv_global_search' class='data_grid data_grid_center'>
+        <div id='adv_search_rows'>
+          <div class='adv_global_input_container'>
+            <table id="adv_global_input_table" role="presentation">
+              <tbody id='adv_global_tbody'>
+                [% INCLUDE "opac/parts/course_search/global_row.tt2" %]
+                <!-- add a new row -->
+                <tr id="adv_global_addrow">
+                  <td class="td-search-left">
+                    <a href="javascript:;" id="myopac_new_global_row" 
+                      onclick='addSearchRow();'>
+                      [% l('Add Search Row') %]
+                    </a>
+                  </td>
+                </tr>
+                <tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+
+        <div id='course_search_submit'>
+          <input type="hidden" name="_course" value="1" />
+          <span>
+            <input id='search-submit-go' type="submit" value="[% l('Search') %]" title="[% l('Search') %]" class="opac-button"
+              onclick='setTimeout(function(){$("search-submit-spinner").className=""; $("search-submit-go").className="hidden"}, 2000)'/>
+            <img id='search-submit-spinner' src='/opac/images/progressbar_green.gif[% ctx.cache_key %]'
+              class='hidden' alt="[% l('Search in progress icon') %]"/>
+          </span>
+          <a href="[% mkurl(ctx.opac_root _ '/course_search', {$loc_name => loc_value}, 1) %]"
+            class="opac-button">[% l('Clear Form') %]</a>
+        </div>
+      </div>
+    </div>
+    <div class="common-full-pad"></div>
+  </form>
+  </div>
+</div>
+
+[% END %]
\ No newline at end of file
index f998b7f..3cb686a 100644 (file)
@@ -1427,6 +1427,10 @@ div.result_table_utils_cont {
     font-size: [% css_fonts.size_bigger %];
 }
 
+.search_courses_label {
+    font-size: [% css_fonts.size_bigger %];
+}
+
 .lbl1 {
     font-size: [% css_fonts.size_bigger %];
     font-weight:bold;
diff --git a/Open-ILS/src/templates/opac/parts/course_search/global_row.tt2 b/Open-ILS/src/templates/opac/parts/course_search/global_row.tt2
new file mode 100644 (file)
index 0000000..d68842f
--- /dev/null
@@ -0,0 +1,55 @@
+[%
+    contains_options = [
+        {value => 'contains', label => l('Contains')},
+        {value => 'nocontains', label => l('Does not contain')}
+    ];
+    contains = CGI.param('contains');
+    queries = CGI.param('query');
+    bools = CGI.param('bool');
+    qtypes = CGI.param('qtype');
+    rowcount = 3;
+
+    # scalar.merge treats the scalar as a 1-item array
+    WHILE queries.size < rowcount; queries = queries.merge(['']); END;
+    WHILE bools.size < rowcount; bools = bools.merge(['and']); END;
+    WHILE qtypes.size < rowcount; qtypes = qtypes.merge(search.default_qtypes.${qtypes.size} ? [search.default_qtypes.${qtypes.size}] : ['keyword']); END;
+
+    FOR qtype IN qtypes;
+        c = contains.shift;
+        b = bools.shift;
+        q = queries.shift; %]
+
+<!-- tag the second row so the bool column won't be hidden -->
+<tr[% IF loop.index == 1 %] id="adv_global_row"[% END %]>
+    <td class="td-left">
+
+        <!-- bool selector.  hide for first row.  safe to ignore first bool value in form submission -->
+        <select title="[% l('Boolean search operator') %]" 
+          name='bool' style='width: auto' [% IF loop.first %] class='invisible' [% END %]>
+            <option value='and' [% b == 'and' ? 'selected="selected"' : '' %]>[% l('And') %]</option>
+            <option value='or' [% b == 'or' ? 'selected="selected"' : '' %]>[% l('Or') %]</option>
+        </select>
+
+        <!-- keyword, subject, etc. selector -->
+        <span class="qtype_selector_margin">
+            [% INCLUDE "opac/parts/course_search/qtype_selector.tt2"
+                query_type=qtype %]
+        </span>
+
+        <select title="[% l('Search phrase match strictness') %]"
+          name='contains' style='margin-right: 7px;'>
+            [% FOR o IN contains_options; -%]
+            <option value="[% o.value %]" [% c == o.value ? ' selected="selected"' : '' %]>[% o.label %]</option>
+            [% END %]
+        </select>
+        <input title="[% l('Search term') %]" aria-label="[% l('Search term') %]"
+          type='text' size='18' name='query' value="[% q | html %]" x-webkit-speech [% IF loop.index == 0 %] autofocus [% END %] />
+        <a href="javascript:;" class="row-remover"
+            title="[% l('Remove row') %]" alt="[% l('Remove row') %]"
+            onclick='return killRowIfAtLeast(2, this);'>
+                <img src="[% ctx.media_prefix %]/images/expert_row_close_btn.png[% ctx.cache_key %]"
+                    alt="[% l('Remove row') %]"/>
+            </a>
+    </td>
+</tr>
+[% END %]
diff --git a/Open-ILS/src/templates/opac/parts/course_search/qtype_selector.tt2 b/Open-ILS/src/templates/opac/parts/course_search/qtype_selector.tt2
new file mode 100644 (file)
index 0000000..a9770b1
--- /dev/null
@@ -0,0 +1,20 @@
+[%  query_types = [
+    {value => "name", label => l("Title"), plural_label => l("Titles"), browse => 1},
+    {value => "instructor", label => l("Instructor"), plural_label => l('Instructors')},
+    {value => "course_number", label => l("Course Number")}
+];
+-%]
+<select name="[% name || 'qtype' %]"[% IF id; ' id="'; id ; '"' ; END -%]
+    title="[% l('Select query type:') %]">
+    [%  query_type = query_type || CGI.param('qtype') || search.default_qtypes.0;
+      FOR qt IN query_types;
+        NEXT IF browse_only AND NOT qt.browse -%]
+    <option value='[% qt.value | html %]'[%
+        query_type == qt.value ? ' selected="selected"' : ''
+    %]>[% IF plural AND qt.plural_label;
+        qt.plural_label | html;
+    ELSE;
+        qt.label | html;
+    END %]</option>
+    [% END -%]
+</select>
index c0a4bfc..48d9c0d 100644 (file)
@@ -150,7 +150,10 @@ END; # FOREACH bib
 
             [% IF copy_info.courses.size > 0 || ctx.is_staff %]
             <td>[% copy_info.circ_modifier | html %]</td>
-            [% ELSIF ctx.is_staff %]
+            [% ELSIF ctx.get_org_setting(ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1 %]
+            <td></td>
+            [% END %]
+            [% IF ctx.is_staff %]
             <td>
                 [% copy_info.age_protect ?
                     ctx.get_crahp(copy_info.age_protect).name : l('None') | html %]
@@ -171,8 +174,6 @@ END; # FOREACH bib
                    '-';
                 END;
             %]</td>
-            [% ELSIF ctx.get_org_setting(ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1 %]
-            <td></td>
             [% END # is_staff %]
             [% IF ctx.is_staff OR serial_holdings %]
             <td>[%  # Show copy/volume hold links to staff (without
index d86bd7d..7213986 100644 (file)
@@ -44,6 +44,9 @@ END;
         <span class="adv_search_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/advanced', {},  expert_search_parms.merge(browse_search_parms, facet_search_parms)) %]"
             id="home_adv_search_link">[% l('Advanced Search') %]</a></span>
         <span class="browse_the_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/browse', {}, expert_search_parms.merge(general_search_parms, facet_search_parms, ['fi:has_browse_entry'])) %]">[% l('Browse the Catalog') %]</a></span>
+        [% IF ctx.get_org_setting(ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1 %]
+        <span class="search_courses_label"><a href="[% mkurl(ctx.opac_root _ '/course_search') %]">[% l('Search for Courses') %]</a></span>
+        [% END %]
         [% INCLUDE 'opac/parts/cart.tt2' %]
     </div>
     <div class="searchbar [% is_home_page ? 'searchbar-home' : '' %]">
diff --git a/Open-ILS/src/templates/staff/cat/item/t_course_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_course_pane.tt2
new file mode 100644 (file)
index 0000000..befa04a
--- /dev/null
@@ -0,0 +1,72 @@
+<div class="col-md-12" ng-show="!has_course_perms">
+  <div class="alert alert-danger">
+    [% l('You do not have sufficient permissions to view this page') %]
+  </div>
+</div>
+
+<div ng-show="has_course_perms">
+  <div class="col-md-6" ng-show="!courses.length">
+    <div class="alert alert-info">
+      [% l('No Associated Courses') %]
+    </div>
+  </div>
+  <div class="col-md-6" ng-show="courses.length">
+    <div class="flex-row">
+      <div class="flex-cell flex-2 strong-text-2">
+        [% l('Associated Courses') %]
+      </div>
+    </div>
+
+    <div class="flex-row well well-sm" ng-repeat="course in courses">
+      <div class="flex-cell">
+        <a href="/eg2/staff/admin/local/asset/course_list/{{course.id()}}">{{course.course_number()}}: {{course.name()}}</a>
+      </div>
+  </div>
+</div>
+
+  <div class="col-md-6" ng-show="!instructors_exist">
+    <div class="alert alert-info">
+      [% l('No Associated Instructors') %]
+    </div>
+  </div>
+  <div class="col-md-6" ng-show="instructors_exist">
+    <div class="flex-row">
+      <div class="flex-cell flex-2 strong-text-2">
+        [% l('Associated Instructors') %]
+      </div>
+    </div>
+
+    <div class="flex-row" ng-repeat="(key, instructor) in instructors">
+      <div class="flex-cell">
+        <strong>
+        <span ng-if="instructor.pref_family_name">
+          {{instructor.pref_family_name}}, 
+        </span>
+        <span ng-if="!instructor.pref_family_name">
+          {{instructor.family_name}}, 
+        </span>
+        <span ng-if="instructor.pref_first_given_name">
+          {{instructor.pref_first_given_name}}
+        </span>
+        <span ng-if="!instructor.pref_first_given_name">
+          {{instructor.first_given_name}}
+        </span>
+        <span ng-if="instructor.pref_second_given_name">
+          {{instructor.pref_second_given_name}}
+        </span>
+        <span ng-if="!instructor.pref_second_given_name && instructor.second_given_name">
+          {{instructor.second_given_name}}
+        </span>
+        </strong>
+        <div class="well well-sm dt">
+          <div class="flex-row" ng-repeat="key in instructor._linked_course">
+            <div class="flex-cell">
+              <span>{{key.course}} </span> 
+              <span>({{key.role}})</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
index 1859865..bf9eaa4 100644 (file)
@@ -23,6 +23,9 @@
   <li ng-class="{active : tab == 'triggered_events'}">
     <a href="./cat/item/{{copy.id()}}/triggered_events">[% l('Triggered Events') %]</a>
   </li>
+  <li ng-class="{active : tab == 'course'}" ng-if="has_course_perms && courseModulesOptIn">
+    <a href="./cat/item/{{copy.id()}}/course">[% l('Course Info') %]</a>
+  </li>
 </ul>
 <div class="tab-content">
   <div class="tab-pane active">
index 2237d75..2995069 100644 (file)
@@ -777,21 +777,21 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
  * Detail view -- shows one copy
  */
 .controller('ViewCtrl', 
-       ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling','egCirc',
-function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling , egCirc) {
+       ['$scope','$q','egGridDataProvider','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling','egCirc',
+function($scope , $q , egGridDataProvider , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling , egCirc) {
     var copyId = $routeParams.id;
     $scope.args.copyId = copyId;
     $scope.tab = $routeParams.tab || 'summary';
     $scope.context.page = 'detail';
     $scope.summaryRecord = null;
-
+    $scope.courseModulesOptIn = fetchCourseOptIn();
+    $scope.has_course_perms = fetchCoursePerms();
     $scope.edit = false;
     if ($scope.tab == 'edit') {
         $scope.tab = 'summary';
         $scope.edit = true;
     }
 
-
     // use the cached record info
     if (itemSvc.copy) {
         $scope.copy_alert_count = itemSvc.copy.copy_alerts().filter(function(aca) {
@@ -998,6 +998,27 @@ console.debug($scope.copy_alert_count);
         });
     }
 
+    // Check for Course Modules Opt-In to enable Course Info tab
+    function fetchCourseOptIn() {
+        return egCore.org.settings(
+            'circ.course_materials_opt_in'
+        ).then(function(set) {
+            $scope.courseModulesOptIn = set['circ.course_materials_opt_in'];
+
+            return $scope.courseModulesOptIn;
+        });
+    }
+
+    function fetchCoursePerms() {
+        return egCore.perm.hasPermAt('MANAGE RESERVES', true).then(function(orgIds) {
+            if(orgIds.indexOf(egCore.auth.user().ws_ou()) != -1){
+                $scope.has_course_perms = true;
+
+                return $scope.has_course_perms;
+            }
+        });
+    }
+
     $scope.addBilling = function(circ) {
         egBilling.showBillDialog({
             xact_id : circ.id(),
@@ -1172,6 +1193,47 @@ console.debug($scope.copy_alert_count);
         })
     }
 
+    function loadCourseInfo() {
+        delete $scope.courses;
+        delete $scope.instructors;
+        delete $scope.course_ids;
+        delete $scope.instructors_exist;
+        if (!copyId) return;
+        $scope.course_ids = [];
+        $scope.courses = [];
+        $scope.instructors = {};
+
+        egCore.pcrud.search('acmcm', {
+            item: copyId
+        }, {
+            flesh: 3,
+            flesh_fields: {
+                acmcm: ['course']
+            }, order_by: {acmc : 'id desc'}
+        }).then(null, null, function(material) {
+            
+            $scope.courses.push(material.course());
+            egCore.net.request(
+                'open-ils.circ',
+                'open-ils.circ.course_users.retrieve',
+                material.course().id()
+            ).then(null, null, function(instructors) {
+                angular.forEach(instructors, function(instructor) {
+                    var patron_id = instructor.patron_id.toString();
+                    if (!$scope.instructors[patron_id]) {
+                        $scope.instructors[patron_id] = instructor;
+                        $scope.instructors_exist = true;
+                        $scope.instructors[patron_id]._linked_course = [];
+                    }
+                    $scope.instructors[patron_id]._linked_course.push({
+                        role: instructor.usr_role,
+                        course: material.course().name()
+                    });
+                });
+            });
+        });
+    }
+
 
     // we don't need all data on all tabs, so fetch what's needed when needed.
     function loadTabData() {
@@ -1194,6 +1256,10 @@ console.debug($scope.copy_alert_count);
                 loadMostRecentTransit();
                 break;
 
+            case 'course':
+                loadCourseInfo();
+                break;
+
             case 'triggered_events':
                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
                 url += '?copy_id=' + encodeURIComponent(copyId);