LP1849212: Users can attach brief bib records and e-resources to courses
authorJane Sandberg <sandbej@linnbenton.edu>
Wed, 15 Jul 2020 07:45:59 +0000 (00:45 -0700)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 14 Sep 2020 22:17:10 +0000 (18:17 -0400)
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Signed-off-by: Michele Morgan <mmorgan@noblenet.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>

22 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/examples/opensrf.xml.example
Open-ILS/examples/opensrf_core.xml.example
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-reserves.module.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/marcrecord.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor-field.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/marc-edit/tagtable.service.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Courses.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
Open-ILS/src/perlmods/live_t/30-courses.t [new file with mode: 0644]
Open-ILS/src/sql/Pg/040.schema.asset.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.course-materials-module.sql
Open-ILS/src/templates/opac/parts/course/body.tt2

index 9e896c9..8c76cab 100644 (file)
@@ -3127,7 +3127,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <link field="owning_lib" reltype="has_a" key="id" map="" class="aou" />
             <link field="members" reltype="has_many" key="course" map="" class="acmcu" />
             <link field="materials" reltype="has_many" key="course" map="" class="acmcm" />
-            <link field="non_cat_materials" reltype="has_many" key="course" map="" class="acmncm" />
         </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -3171,6 +3170,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="Item" name="item" reporter:datatype="link" />
             <field reporter:label="Record" name="record" reporter:datatype="link" />
+            <field reporter:label="Record is temporary?" name="temporary_record" reporter:datatype="bool" />
             <field reporter:label="Item Relationship" name="relationship" reporter:datatype="text" />
             <field reporter:label="Original Status" name="original_status" reporter:datatype="link" />
             <field reporter:label="Original Circ Modifier" name="original_circ_modifier" reporter:datatype="link" />
@@ -3201,32 +3201,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             </actions>
         </permacrud>
     </class>
-    <class id="acmncm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_non_cat_course_materials" oils_persist:tablename="asset.course_module_non_cat_course_materials" reporter:label="Non-Cataloged Course Materials">
-        <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_non_cat_course_materials_id_seq">
-            <field reporter:label="ID" name="id" reporter:datatype="id" />
-            <field reporter:label="Course" name="course" reporter:datatype="link" />
-            <field reporter:label="Title" name="title" reporter:datatype="text" />
-            <field reporter:label="URL" name="url" reporter:datatype="text" />
-            <field reporter:label="Item Relationship" name="relationship" reporter:datatype="text" />
-        </fields>
-        <links>
-            <link field="course" reltype="has_a" key="id" map="" class="acmc" />
-        </links>
-        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
-            <actions>
-                <create permission="MANAGE_RESERVES">
-                                       <context link="course" field="owning_lib" />
-                </create>
-                <retrieve/>
-                <update permission="MANAGE_RESERVES">
-                                       <context link="course" field="owning_lib" />
-                </update>
-                <delete permission="MANAGE_RESERVES">
-                                       <context link="course" field="owning_lib" />
-                </delete>
-            </actions>
-        </permacrud>
-    </class>
     <class id="acnc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::call_number_class" oils_persist:tablename="asset.call_number_class" reporter:label="Call number classification scheme">
         <fields oils_persist:primary="id" oils_persist:sequence="asset.call_number_class_id_seq">
             <field reporter:label="Call number class ID" name="id" reporter:datatype="id"/>
index eb875b1..0549643 100644 (file)
@@ -1270,6 +1270,26 @@ vim:et:ts=4:sw=4:
                   <request_timeout>60</request_timeout>
                 </app_settings>
             </open-ils.ebook_api>
+
+            <open-ils.courses>
+                <keepalive>5</keepalive>
+                <stateless>1</stateless>
+                <language>perl</language>
+                <implementation>OpenILS::Application::Courses</implementation>
+                <max_requests>100</max_requests>
+                <unix_config>
+                    <unix_sock>courses_unix.sock</unix_sock>
+                    <unix_pid>courses_unix.pid</unix_pid>
+                    <unix_log>courses_unix.log</unix_log>
+                    <max_requests>100</max_requests>
+                    <min_children>1</min_children>
+                    <max_children>15</max_children>
+                    <min_spare_children>1</min_spare_children>
+                    <max_spare_children>5</max_spare_children>
+                </unix_config>
+                <app_settings>
+                </app_settings>
+            </open-ils.courses>
         </apps>
     </default>
 
@@ -1317,6 +1337,7 @@ vim:et:ts=4:sw=4:
                 <appname>open-ils.serial</appname>  
                 <appname>open-ils.hold-targeter</appname>  
                 <appname>open-ils.ebook_api</appname>
+                <appname>open-ils.courses</appname>
             </activeapps>
         </localhost>
     </hosts>
index 45e5f5a..e801db9 100644 (file)
@@ -27,6 +27,7 @@ Example OpenSRF bootstrap configuration file for Evergreen
           <service>open-ils.cat</service>
           <service>open-ils.circ</service>
           <service>open-ils.collections</service>
+          <service>open-ils.courses</service>
           <service>open-ils.fielder</service>
           <service>open-ils.pcrud</service>
           <service>open-ils.permacrud</service>
index a34e757..724545a 100644 (file)
-
-<eg-string #materialDeleteFailedString i18n-text text="Disassociation of Course Material failed or was not allowed"></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 #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"
-  [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>
-        </div>
-        <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 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">
+      <ngb-tabset [ngClass]="isDialog() ? 'col-md-12' : 'col-md-4'" type="tabs">
+        <ngb-tab title="Associate item" i18n-title>
+          <ng-template ngbTabContent>
+            <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>
+              </div>
+              <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>
+              </div>
             </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="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 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>
-            <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 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>
-        <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 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>
+                  <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 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>
+                  <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>
-            <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 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>
+                  <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 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>
+                  <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>
-      </div>
-      <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>
+          </ng-template>
+        </ngb-tab>
+        <ngb-tab title="Associate brief record" i18n-title>
+          <ng-template ngbTabContent>
+            <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>
             </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 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>
             </div>
-          </div>
-        </div>
-        <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>
+            <eg-marc-simplified-editor (xmlRecordEvent)="associateBriefRecord($event)" buttonLabel="Add material" i18n-buttonLabel>
+              <eg-marc-simplified-editor-field tag="245" subfield="a"></eg-marc-simplified-editor-field>
+              <eg-marc-simplified-editor-field tag="856" subfield="u"></eg-marc-simplified-editor-field>
+              <eg-marc-simplified-editor-field tag="856" subfield="9" defaultValue="CONS"></eg-marc-simplified-editor-field>
+              <eg-marc-simplified-editor-field tag="990" subfield="a" i18n-defaultValue
+                defaultValue="This record was created using the Course Materials Module -- please edit it there">
+                </eg-marc-simplified-editor-field>
+            </eg-marc-simplified-editor>
+          </ng-template>
+        </ngb-tab>
+        <ngb-tab title="Associate electronic resource from catalog" i18n-title>
+          <ng-template ngbTabContent>
+            <div class="row" [ngClass]="isDialog() ? '' : 'mt-3'">
+              <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'" class="flex-grow-1" />
+                </div>
+              </div>
+              <div class="d-flex" [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12 mt-3'">
+                <div class="input-group">
+                  <div class="input-group-prepend">
+                    <label for="bib-id" class="input-group-text" i18n>Bibliographic Record ID</label>
+                  </div>
+                  <input type="text" [(ngModel)]="bibId" id="bib-id"
+                    [disabled]="currentCourse && currentCourse.is_archived() == 't'" class="flex-grow-1" />
+                </div>
               </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 class="row mt-3">
+              <div class="col-lg-12 text-right">
+                <button class="btn btn-primary" i18n (click)="associateElectronicBibRecord()"
+                  [disabled]="currentCourse && currentCourse.is_archived() == 't'">
+                  Add Material
+                </button>
               </div>
             </div>
-          </div>
-        </div>
-      </div>
-    </div>
+          </ng-template>
+        </ngb-tab>
+      </ngb-tabset>
 
-    <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-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-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 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-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-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>
   </div>
-</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>
+  <span *ngIf="entry.item()">
+    <a class="pl-1" href="/eg/staff/cat/item/{{entry.item().id()}}">
+      {{entry.item().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 class="pl-1" routerLink="/staff/catalog/record/{{entry.record().id()}}">
+    {{entry.record().wide_display_entry().title()}}
   </a>
-</span>
 </ng-template>
 <ng-template #idlClassLabel let-r="result" i18n>
-{{r.label}}
+  {{r.label}}
 </ng-template>
 
 <ng-container *ngIf="!isDialog()">
   </ng-container>
 </ng-container>
 
-<eg-fm-record-editor #editDialog
-  idlClass='acmcm'
-  [fieldOptions]="{course: {linkedSearchField: 'course_number'}}"
+<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
+</eg-fm-record-editor>
index 749bf96..b50634f 100644 (file)
@@ -1,6 +1,7 @@
 import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core';
-import {Router, ActivatedRoute} from '@angular/router';
-import {Observable, Observer, of} from 'rxjs';
+import {ActivatedRoute} from '@angular/router';
+import {from, Observable} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 import {AuthService} from '@eg/core/auth.service';
 import {NetService} from '@eg/core/net.service';
@@ -13,7 +14,6 @@ import {GridDataSource} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
 import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {StringComponent} from '@eg/share/string/string.component';
-import {StaffBannerComponent} from '@eg/staff/share/staff-banner.component';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {CourseService} from '@eg/staff/share/course.service';
@@ -23,7 +23,7 @@ import {CourseService} from '@eg/staff/share/course.service';
     templateUrl: './course-associate-material.component.html'
 })
 
-export class CourseAssociateMaterialComponent extends DialogComponent {
+export class CourseAssociateMaterialComponent extends DialogComponent implements OnInit {
     @Input() currentCourse: IdlObject;
     @Input() courseId: any;
     @Input() displayMode: String;
@@ -55,6 +55,10 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
     @Input() isModifyingCircMod: Boolean;
     @Input() isModifyingCallNumber: Boolean;
     @Input() isModifyingLocation: Boolean;
+    bibId: number;
+
+    associateBriefRecord: (newRecord: string) => void;
+    associateElectronicBibRecord: () => void;
 
     constructor(
         private auth: AuthService,
@@ -70,31 +74,53 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
     ) {
         super(modal);
         this.materialsDataSource = new GridDataSource();
+
+        this.materialsDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.net.request(
+                'open-ils.courses',
+                'open-ils.courses.course_materials.retrieve.fleshed',
+                {course: this.courseId}
+            );
+        };
     }
 
     ngOnInit() {
-        this.materialsDataSource.getRows = (pager: Pager, sort: any[]) => {
-            return this.loadMaterialsGrid(pager);
-        }
+        this.associateBriefRecord = (newRecord: string) => {
+            return this.net.request(
+                'open-ils.courses',
+                'open-ils.courses.attach.biblio_record',
+                this.auth.token(),
+                newRecord,
+                this.courseId,
+                this.relationshipInput
+            ).subscribe(() => {
+                this.materialsGrid.reload();
+                this.materialAddSuccessString.current()
+                    .then(str => this.toast.success(str));
+            });
+        };
+
+        this.associateElectronicBibRecord = () => {
+            return this.net.request(
+                'open-ils.courses',
+                'open-ils.courses.attach.electronic_resource',
+                this.auth.token(),
+                this.bibId,
+                this.courseId,
+                this.relationshipInput
+            ).subscribe(() => {
+                this.materialsGrid.reload();
+                this.materialAddSuccessString.current()
+                    .then(str => this.toast.success(str));
+            });
+         };
+
     }
 
     isDialog(): boolean {
         return this.displayMode === 'dialog';
     }
 
-    loadMaterialsGrid(pager: Pager): Observable<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();
-        });
-    }
-
     editSelectedMaterials(itemFields: IdlObject[]) {
         // Edit each IDL thing one at a time
         const editOneThing = (item: IdlObject) => {
@@ -116,7 +142,7 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
                     this.materialEditSuccessString.current()
                         .then(str => this.toast.success(str));
                     this.pcrud.retrieve('acmcm', result).subscribe(material => {
-                        if (material.course() != this.courseId) {
+                        if (material.course() !== this.courseId) {
                             this.materialsDataSource.data.splice(
                                 this.materialsDataSource.data.indexOf(course_material, 0), 1
                             );
@@ -134,10 +160,10 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
             );
         });
     }
-    
+
     associateItem(barcode, relationship) {
         if (barcode) {
-            let args = {
+            const args = {
                 barcode: barcode,
                 relationship: relationship,
                 isModifyingCallNumber: this.isModifyingCallNumber,
@@ -148,24 +174,22 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
                 tempLocation: this.tempLocation,
                 tempStatus: this.tempStatus,
                 currentCourse: this.currentCourse
-            }
+            };
             this.barcodeInput = null;
 
             this.pcrud.search('acp', {barcode: args.barcode}, {
                 flesh: 3, flesh_fields: {acp: ['call_number']}
             }).subscribe(item => {
-                let associatedMaterial = this.course.associateMaterials(item, args);
+                const associatedMaterial = this.course.associateMaterials(item, args);
                 associatedMaterial.material.then(res => {
                     item = associatedMaterial.item;
                     let new_cn = item.call_number().label();
-                    if (this.tempCallNumber) new_cn = this.tempCallNumber;
+                    if (this.tempCallNumber) { new_cn = this.tempCallNumber; }
                     this.course.updateItem(item, this.currentCourse.owning_lib(),
                         new_cn, args.isModifyingCallNumber
                     ).then(resp => {
-                        this.course.fleshMaterial(res).then(fleshed_material => {
-                            this.materialsDataSource.data.push(fleshed_material);
-                        });
-                        if (item.circ_lib() != this.currentCourse.owning_lib()) {
+                        this.materialsGrid.reload();
+                        if (item.circ_lib() !== this.currentCourse.owning_lib()) {
                             this.materialAddDifferentLibraryString.current()
                             .then(str => this.toast.warning(str));
                         } else {
@@ -182,10 +206,10 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
     }
 
     deleteSelectedMaterials(items) {
-        let item_ids = [];
+        const item_ids = [];
         items.forEach(item => {
             this.materialsDataSource.data.splice(this.materialsDataSource.data.indexOf(item, 0), 1);
-            item_ids.push(item.id())
+            item_ids.push(item.id());
         });
         this.pcrud.search('acmcm', {course: this.courseId, item: item_ids}).subscribe(material => {
             material.isdeleted(true);
@@ -202,4 +226,4 @@ export class CourseAssociateMaterialComponent extends DialogComponent {
             );
         });
     }
-}
\ No newline at end of file
+}
index 3823648..96802cf 100644 (file)
@@ -8,19 +8,21 @@ import {CourseAssociateMaterialComponent} from './course-associate-material.comp
 import {CourseAssociateUsersComponent} from './course-associate-users.component';
 import {CourseReservesRoutingModule} from './routing.module';
 import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-location-select.module';
+import {MarcSimplifiedEditorModule} from '@eg/staff/share/marc-edit/simplified-editor/simplified-editor.module';
 
 @NgModule({
   declarations: [
     CourseListComponent,
     CoursePageComponent,
     CourseAssociateMaterialComponent,
-    CourseAssociateUsersComponent
+    CourseAssociateUsersComponent,
   ],
   imports: [
     StaffCommonModule,
     AdminCommonModule,
     CourseReservesRoutingModule,
     ItemLocationSelectModule,
+    MarcSimplifiedEditorModule,
     TreeModule
   ],
   exports: [
index 493eaad..6dcaf24 100644 (file)
@@ -29,6 +29,7 @@ export interface MarcField {
 
     // Pass-through to marcrecord.js
     isControlfield(): boolean;
+    indicator?: (ind: number) => any;
 
     deleteExactSubfields(...subfield: MarcSubfield[]): number;
 }
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor-field.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor-field.component.ts
new file mode 100644 (file)
index 0000000..bfed188
--- /dev/null
@@ -0,0 +1,42 @@
+import {Component, Host, Input, OnInit} from '@angular/core';
+import {MarcSimplifiedEditorComponent} from './simplified-editor.component';
+import {MarcSubfield} from '../marcrecord';
+
+/**
+ * A field that a user can edit, which will later be
+ * compiled into MARC
+ */
+
+@Component({
+  selector: 'eg-marc-simplified-editor-field',
+  template: '<ng-template></ng-template>'
+})
+export class MarcSimplifiedEditorFieldComponent implements OnInit {
+
+  @Input() tag: string;
+  @Input() subfield: string;
+  @Input() defaultValue: string;
+
+  constructor(@Host() private editor: MarcSimplifiedEditorComponent) {}
+
+  ngOnInit() {
+      this.editor.addField({
+          tag: this.tag,
+          subfields: [[
+              this.subfield,
+              this.defaultValue ? this.defaultValue : '',
+              0
+          ]],
+          authValid: false,
+          authChecked: false,
+          isCtrlField: false,
+          isControlfield: () => false,
+          indicator: (ind: number) => '0',
+          deleteExactSubfields: (...subfield: MarcSubfield[]) => 0,
+      });
+  }
+
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.html
new file mode 100644 (file)
index 0000000..a82dbf3
--- /dev/null
@@ -0,0 +1,21 @@
+<ng-container *ngIf="editor">
+  <form [formGroup]="editor">
+    <ng-container *ngFor="let field of fields">
+      <div class="row" *ngIf="!field.subfields[0][1]">
+        <div class="col-lg-3">
+          <label for="{{idPrefix}}-{{field.tag}}{{field.subfields[0]}}">
+            {{fieldLabels[field.fieldId]}}
+          </label>
+        </div>
+        <div class="col-lg-9">
+          <input id="{{idPrefix}}-{{field.tag}}{{field.subfields[0]}}" formControlName="{{field.fieldId}}" />
+        </div>
+      </div>
+    </ng-container>
+    <button class="btn btn-primary" (click)="emitXml()">
+      <ng-container *ngIf="buttonLabel">{{buttonLabel}}</ng-container>
+      <ng-container *ngIf="!buttonLabel" i18n>Save</ng-container>
+    </button>
+  </form>
+</ng-container>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.component.ts
new file mode 100644 (file)
index 0000000..80f900a
--- /dev/null
@@ -0,0 +1,72 @@
+import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FormGroup, FormControl, ValidationErrors, ValidatorFn, FormArray} from '@angular/forms';
+import {MarcField, MarcRecord} from '../marcrecord';
+import {TagTableService} from '../tagtable.service';
+
+/**
+ * A simplified editor for basic MARC records, which
+ * does not require knowledge of MARC tags
+ */
+
+@Component({
+  selector: 'eg-marc-simplified-editor',
+  templateUrl: './simplified-editor.component.html'
+})
+export class MarcSimplifiedEditorComponent implements AfterViewInit, OnInit {
+
+    @Input() buttonLabel: string;
+    @Output() xmlRecordEvent = new EventEmitter<string>();
+
+    fields: MarcField[] = [];
+    editor: FormGroup;
+
+    // DOM id prefix to prevent id collisions.
+    idPrefix: string;
+
+    fieldIndex = 0;
+    fieldLabels: string[] = [];
+
+    addField: (field: MarcField) => void;
+
+    constructor(
+        private tagTable: TagTableService
+    ) {}
+
+    ngOnInit() {
+        // Add some randomness to the generated DOM IDs to ensure against clobbering
+        this.idPrefix = 'marc-simplified-editor-' + Math.floor(Math.random() * 100000);
+        this.editor = new FormGroup({});
+
+        // Add a fieldId, and then add a new field to the array
+        this.addField = (field: MarcField) => {
+            field.fieldId = this.fieldIndex;
+            this.fields.push(field);
+            this.editor.addControl(String(this.fieldIndex), new FormControl(null, []));
+            this.fieldIndex++;
+        };
+
+    }
+
+    ngAfterViewInit() {
+        this.tagTable.loadTags({marcRecordType: 'biblio', ffType: 'BKS'}).then(table => {
+            this.fields.forEach((field) => {
+                this.fieldLabels[field.fieldId] = table.getSubfieldLabel(field.tag, field.subfields[0][0]);
+            });
+        });
+    }
+
+    emitXml() {
+        const record = new MarcRecord('<record xmlns="http://www.loc.gov/MARC21/slim"></record>');
+        // need to add the value to field.subfields[0][1]
+        this.fields.forEach((field) => {
+            if (field.subfields[0][1] === '') { // Default value has not been applied
+                field.subfields[0][1] = this.editor.get(String(field.fieldId)).value;
+            }
+        });
+        record.fields = this.fields;
+        this.xmlRecordEvent.emit(record.toXml());
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.module.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/simplified-editor/simplified-editor.module.ts
new file mode 100644 (file)
index 0000000..ec40da9
--- /dev/null
@@ -0,0 +1,27 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
+import {MarcSimplifiedEditorComponent} from './simplified-editor.component';
+import {MarcSimplifiedEditorFieldComponent} from './simplified-editor-field.component';
+import {TagTableService} from '../tagtable.service';
+
+@NgModule({
+    declarations: [
+        MarcSimplifiedEditorComponent,
+        MarcSimplifiedEditorFieldComponent,
+    ],
+    imports: [
+        StaffCommonModule,
+        CommonWidgetsModule
+    ],
+    exports: [
+        MarcSimplifiedEditorComponent,
+        MarcSimplifiedEditorFieldComponent,
+    ],
+    providers: [
+        TagTableService
+    ]
+})
+
+export class MarcSimplifiedEditorModule { }
+
index ce6ddb7..8b867a8 100644 (file)
@@ -188,6 +188,13 @@ export class TagTable {
         return this.toCache('sfcodes', tag, null, list);
     }
 
+    getSubfieldLabel(tag: string, sfCode: string): string {
+        if (!tag || !this.tagMap[tag]) { return null; }
+        const subfieldResults = this.tagMap[tag].subfields.filter(sf => sf.code === sfCode);
+        return subfieldResults.length ? subfieldResults[0].description : null;
+    }
+
+
     getFieldTags(): ContextMenuEntry[] {
 
         if (!this.fieldTags) {
index c48f6ce..bb4412b 100644 (file)
@@ -1044,161 +1044,6 @@ sub delete_copy_note {
     $e->commit;
     return 1;
 }
-__PACKAGE__->register_method(
-    method          => 'fetch_course_materials',
-    autoritative    => 1,
-    api_name        => 'open-ils.circ.course_materials.retrieve',
-    signature       => q/
-        Returns an array of course materials.
-        @params args     : Supplied object to filter search.
-    /);
-
-__PACKAGE__->register_method(
-    method          => 'fetch_course_materials',
-    autoritative    => 1,
-    api_name        => 'open-ils.circ.course_materials.retrieve.fleshed',
-    signature       => q/
-        Returns an array of course materials, each fleshed out with information
-        from the item and the course_material object.
-        @params args     : Supplied object to filter search.
-    /);
-
-sub fetch_course_materials {
-    my ($self, $conn, $args) = @_;
-    my $e = new_editor();
-    my $materials = {};
-    my %items;
-
-    $materials->{list} = $e->search_asset_course_module_course_materials($args);
-    return $materials->{list} unless ($self->api_name =~ /\.fleshed/);
-
-    # If we want it fleshed out...
-    for my $course_material (@{$materials->{list}}) {
-        my $material = {};
-        $material->{id} = $course_material->id;
-        $material->{relationship} = $course_material->relationship;
-        $material->{record} = $course_material->record;
-        my $copy = $e->retrieve_asset_copy([
-            $course_material->item, {
-                flesh => 3, flesh_fields => {
-                    'acp' => ['call_number'],
-                    'acn' => ['record']
-                }
-            }
-        ]);
-
-        $material->{item_data} = $copy;
-        $material->{volume_data} = $copy->call_number;
-        $material->{record_data} = $copy->call_number->record;
-        $items{$course_material->item} = $material;
-    }
-
-    my $targets = ();
-    for my $item (values %items) {
-        my $final_item = {};
-        my $mvr = $U->record_to_mvr($item->{record_data});
-        $final_item->{id} = $item->{id};
-        $final_item->{relationship} = $item->{relationship};
-        $final_item->{record} = $item->{record};
-        $final_item->{barcode} = $item->{item_data}->barcode;
-        $final_item->{circ_lib} = $item->{item_data}->circ_lib;
-        $final_item->{title} = $mvr->title;
-        $final_item->{call_number} = $item->{volume_data}->label;
-        $final_item->{location} = $e->retrieve_asset_copy_location(
-            $item->{item_data}->location
-        );
-        $final_item->{status} = $e->retrieve_config_copy_status(
-            $item->{item_data}->status
-        );
-
-        push @$targets, $final_item;
-    }
-
-    return $targets;
-}
-
-__PACKAGE__->register_method(
-    method          => 'fetch_courses',
-    autoritative    => 1,
-    api_name        => 'open-ils.circ.courses.retrieve',
-    signature       => q/
-        Returns an array of course materials.
-        @params course_id: The id of the course we want to retrieve
-    /);
-
-sub fetch_courses {
-    my ($self, $conn, @course_ids) = @_;
-    my $e = new_editor();
-
-    return unless @course_ids;
-    my $targets = ();
-    foreach my $course_id (@course_ids) {
-        my $target = $e->retrieve_asset_course_module_course($course_id);
-        push @$targets, $target;
-    }
-
-    return $targets;
-}
-
-__PACKAGE__->register_method(
-    method          => 'fetch_course_users',
-    autoritative    => 1,
-    api_name        => 'open-ils.circ.course_users.retrieve',
-    signature       => q/
-        Returns an array of course users.
-        @params course_id: The id of the course we want to retrieve from
-    /);
-__PACKAGE__->register_method(
-    method          => 'fetch_course_users',
-    autoritative    => 1,
-    api_name        => 'open-ils.circ.course_users.retrieve.staff',
-    signature       => q/
-        Returns an array of course users.
-        @params course_id: The id of the course we want to retrieve from
-    /);
-
-sub fetch_course_users {
-    my ($self, $conn, $course_id) = @_;
-    my $e = new_editor();
-    my $filter = {};
-    my $users = {};
-    my %patrons;
-
-    $filter->{course} = $course_id;
-    $filter->{is_public} = 't'
-        unless ($self->api_name =~ /\.staff/) and $e->allowed('MANAGE_RESERVES');
-    
-    
-    $users->{list} =  $e->search_asset_course_module_course_users($filter, {order_by => {acmcu => 'id'}});
-    for my $course_user (@{$users->{list}}) {
-        my $patron = {};
-        $patron->{id} = $course_user->id;
-        $patron->{usr_role} = $course_user->usr_role;
-        $patron->{patron_data} = $e->retrieve_actor_user($course_user->usr);
-        $patrons{$course_user->usr} = $patron;
-    }
-
-    my $targets = ();
-    for my $user (values %patrons) {
-        my $final_user = {};
-        $final_user->{id} = $user->{id};
-        $final_user->{usr_role} = $user->{usr_role};
-        $final_user->{patron_id} = $user->{patron_data}->id;
-        $final_user->{first_given_name} = $user->{patron_data}->first_given_name;
-        $final_user->{second_given_name} = $user->{patron_data}->second_given_name;
-        $final_user->{family_name} = $user->{patron_data}->family_name;
-        $final_user->{pref_first_given_name} = $user->{patron_data}->pref_first_given_name;
-        $final_user->{pref_family_name} = $user->{patron_data}->pref_family_name;
-        $final_user->{pref_second_given_name} = $user->{patron_data}->pref_second_given_name;
-        $final_user->{pref_suffix} = $user->{patron_data}->pref_suffix;
-        $final_user->{pref_prefix} = $user->{patron_data}->pref_prefix;
-        
-        push @$targets, $final_user;
-    }
-
-    return $targets;
-
-}
 
 __PACKAGE__->register_method(
     method      => 'fetch_copy_tags',
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Courses.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Courses.pm
new file mode 100644 (file)
index 0000000..fe32164
--- /dev/null
@@ -0,0 +1,235 @@
+package OpenILS::Application::Courses;
+
+use strict;
+use warnings;
+
+use OpenSRF::AppSession;
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+my $U = "OpenILS::Application::AppUtils";
+
+use OpenSRF::Utils::Logger qw/$logger/;
+
+__PACKAGE__->register_method(
+    method          => 'attach_electronic_resource_to_course',
+    api_name        => 'open-ils.courses.attach.electronic_resource',
+    signature => {
+        desc => 'Attaches a bib record for an electronic resource to a course',
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'Record id', type => 'number'},
+            {desc => 'Course id', type => 'number'},
+            {desc => 'Relationship', type => 'string'}
+        ],
+        return => {desc => '1 on success, event on failure'}
+    });
+sub attach_electronic_resource_to_course {
+    my ($self, $conn, $authtoken, $record, $course, $relationship) = @_;
+    my $e = new_editor(authtoken=>$authtoken, xact=>1);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless
+        $e->allowed('MANAGE_RESERVES');
+
+    my $located_uris = $e->search_asset_call_number({
+        record => $record,
+        deleted => 'f',
+        label => '##URI##' })->[0];
+    my $bib = $e->retrieve_biblio_record_entry([
+        $record, {
+            flesh => 1,
+            flesh_fields => {'bre' => ['source']}
+        }
+    ]);
+    return $e->event unless (($bib->source && $bib->source->transcendant) || $located_uris);
+    _attach_bib($e, $course, $record, $relationship);
+
+    return 1;
+}
+
+__PACKAGE__->register_method(
+    method          => 'attach_brief_bib_to_course',
+    api_name        => 'open-ils.courses.attach.biblio_record',
+    signature => {
+        desc => 'Creates a new bib record with the provided XML, and attaches it to a course',
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'XML', type => 'string'},
+            {desc => 'Course id', type => 'number'},
+            {desc => 'Relationship', type => 'string'}
+        ],
+        return => {desc => '1 on success, event on failure'}
+    });
+sub attach_brief_bib_to_course {
+    my ($self, $conn, $authtoken, $marcxml, $course, $relationship) = @_;
+    my $e = new_editor(authtoken=>$authtoken, xact=>1);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed('MANAGE_RESERVES');
+    return $e->die_event unless $e->allowed('CREATE_MARC');
+
+    my $bib_source_id = $U->ou_ancestor_setting_value($self->{ou}, 'circ.course_materials_brief_record_bib_source');
+    my $bib_source_name;
+    if ($bib_source_id) {
+        $bib_source_name = $e->retrieve_config_bib_source($bib_source_id)->source;
+    } else {
+        # The default value from the seed data
+        $bib_source_name = 'Course materials module';
+    }
+
+    my $bib_create = OpenSRF::AppSession
+        ->create('open-ils.cat')
+        ->request('open-ils.cat.biblio.record.xml.create',
+            $authtoken, $marcxml, $bib_source_name)
+        ->gather(1);
+    _attach_bib($e, $course, $bib_create->id, $relationship) if ($bib_create);
+    return 1;
+}
+
+# Shared logic for both e-resources and brief bibs
+sub _attach_bib {
+    my ($e, $course, $record, $relationship) = @_;
+    my $acmcm = Fieldmapper::asset::course_module_course_materials->new;
+    $acmcm->course($course);
+    $acmcm->record($record);
+    $acmcm->relationship($relationship);
+    $e->create_asset_course_module_course_materials( $acmcm ) or return $e->die_event;
+    $e->commit;
+}
+
+sub detach_material_from_course {
+    my ($self, $conn, $authtoken, $acmcm) = @_;
+
+}
+
+__PACKAGE__->register_method(
+    method          => 'fetch_course_materials',
+    autoritative    => 1,
+    stream          => 1,
+    api_name        => 'open-ils.courses.course_materials.retrieve',
+    signature       => q/
+        Returns an array of course materials.
+        @params args     : Supplied object to filter search.
+    /);
+
+__PACKAGE__->register_method(
+    method          => 'fetch_course_materials',
+    autoritative    => 1,
+    stream          => 1,
+    api_name        => 'open-ils.courses.course_materials.retrieve.fleshed',
+    signature       => q/
+        Returns an array of course materials, each fleshed out with information
+        from the item and the course_material object.
+        @params args     : Supplied object to filter search.
+    /);
+
+sub fetch_course_materials {
+    my ($self, $conn, $args) = @_;
+    my $e = new_editor();
+    my $materials;
+
+    if ($self->api_name =~ /\.fleshed/) {
+        my $fleshing = {
+            'flesh' => 2, 'flesh_fields' => {
+                'acmcm' => ['item', 'record'],
+                'acp' => ['call_number', 'circ_lib', 'location', 'status'],
+                'bre' => ['wide_display_entry'],
+            }
+        };
+        $materials = $e->search_asset_course_module_course_materials([$args, $fleshing]);
+    } else {
+        $materials = $e->search_asset_course_module_course_materials($args);
+    }
+    $conn->respond($_) for @$materials;
+    return undef;
+}
+
+__PACKAGE__->register_method(
+    method          => 'fetch_courses',
+    autoritative    => 1,
+    api_name        => 'open-ils.courses.courses.retrieve',
+    signature       => q/
+        Returns an array of course materials.
+        @params course_id: The id of the course we want to retrieve
+    /);
+
+sub fetch_courses {
+    my ($self, $conn, @course_ids) = @_;
+    my $e = new_editor();
+
+    return unless @course_ids;
+    my $targets = ();
+    foreach my $course_id (@course_ids) {
+        my $target = $e->retrieve_asset_course_module_course($course_id);
+        push @$targets, $target;
+    }
+
+    return $targets;
+}
+
+__PACKAGE__->register_method(
+    method          => 'fetch_course_users',
+    autoritative    => 1,
+    api_name        => 'open-ils.courses.course_users.retrieve',
+    signature       => q/
+        Returns an array of course users.
+        @params course_id: The id of the course we want to retrieve from
+    /);
+__PACKAGE__->register_method(
+    method          => 'fetch_course_users',
+    autoritative    => 1,
+    api_name        => 'open-ils.courses.course_users.retrieve.staff',
+    signature       => q/
+        Returns an array of course users.
+        @params course_id: The id of the course we want to retrieve from
+    /);
+
+sub fetch_course_users {
+    my ($self, $conn, $course_id) = @_;
+    my $e = new_editor();
+    my $filter = {};
+    my $users = {};
+    my %patrons;
+
+    $filter->{course} = $course_id;
+    $filter->{is_public} = 't'
+        unless ($self->api_name =~ /\.staff/) and $e->allowed('MANAGE_RESERVES');
+    $users->{list} =  $e->search_asset_course_module_course_users($filter, {order_by => {acmcu => 'id'}});
+    for my $course_user (@{$users->{list}}) {
+        my $patron = {};
+        $patron->{id} = $course_user->id;
+        $patron->{usr_role} = $course_user->usr_role;
+        $patron->{patron_data} = $e->retrieve_actor_user($course_user->usr);
+        $patrons{$course_user->usr} = $patron;
+    }
+
+    my $targets = ();
+    for my $user (values %patrons) {
+        my $final_user = {};
+        $final_user->{id} = $user->{id};
+        $final_user->{usr_role} = $user->{usr_role};
+        $final_user->{patron_id} = $user->{patron_data}->id;
+        $final_user->{first_given_name} = $user->{patron_data}->first_given_name;
+        $final_user->{second_given_name} = $user->{patron_data}->second_given_name;
+        $final_user->{family_name} = $user->{patron_data}->family_name;
+        $final_user->{pref_first_given_name} = $user->{patron_data}->pref_first_given_name;
+        $final_user->{pref_family_name} = $user->{patron_data}->pref_family_name;
+        $final_user->{pref_second_given_name} = $user->{patron_data}->pref_second_given_name;
+        $final_user->{pref_suffix} = $user->{patron_data}->pref_suffix;
+        $final_user->{pref_prefix} = $user->{patron_data}->pref_prefix;
+
+        push @$targets, $final_user;
+    }
+
+    return $targets;
+
+}
+
+
+
+1;
+
index 42160c1..eaffd44 100644 (file)
@@ -22,20 +22,20 @@ sub load_course {
         unless $course_id and $course_id =~ /^\d+$/;
 
     $ctx->{course} = $U->simplereq(
-        'open-ils.circ',
-        'open-ils.circ.courses.retrieve',
+        'open-ils.courses',
+        'open-ils.courses.courses.retrieve',
         [$course_id]
     )->[0];
     
     $ctx->{instructors} = $U->simplereq(
-        'open-ils.circ',
-        'open-ils.circ.course_users.retrieve',
+        'open-ils.courses',
+        'open-ils.courses.course_users.retrieve',
         $course_id
     );
 
     $ctx->{course_materials} = $U->simplereq(
-        'open-ils.circ',
-        'open-ils.circ.course_materials.retrieve.fleshed',
+        'open-ils.courses',
+        'open-ils.courses.course_materials.retrieve.fleshed.atomic',
         {course => $course_id}
     );
     return Apache2::Const::OK;
@@ -394,4 +394,4 @@ sub _prepare_query {
     }
 
     return ($full_query, @queries);
-}
\ No newline at end of file
+}
index 2cb5699..494dded 100644 (file)
@@ -118,8 +118,8 @@ sub load_record {
         );
         if ($ctx->{course_module_opt_in}) {
             $copy->{course_materials} = $U->simplereq(
-                'open-ils.circ',
-                'open-ils.circ.course_materials.retrieve',
+                'open-ils.courses',
+                'open-ils.courses.course_materials.retrieve',
                 {item => $copy->{id}}
             );
             my %course_ids;
@@ -128,8 +128,8 @@ sub load_record {
             }
 
             $copy->{courses} = $U->simplereq(
-                'open-ils.circ',
-                'open-ils.circ.courses.retrieve',
+                'open-ils.courses',
+                'open-ils.courses.courses.retrieve',
                 keys %course_ids
             );
         }
index 4170b39..adcff2a 100644 (file)
@@ -579,8 +579,8 @@ sub load_rresults {
         $rec->{popularity} = $res_rec->[2];
         if ($course_module_opt_in) {
             $rec->{course_materials} = $U->simplereq(
-                'open-ils.circ',
-                'open-ils.circ.course_materials.retrieve',
+                'open-ils.courses',
+                'open-ils.courses.course_materials.retrieve',
                 {record => $rec->{id}}
             );
             my %course_ids;
@@ -589,8 +589,8 @@ sub load_rresults {
             }
 
             $rec->{courses} = $U->simplereq(
-                'open-ils.circ',
-                'open-ils.circ.courses.retrieve',
+                'open-ils.courses',
+                'open-ils.courses.courses.retrieve',
                 keys %course_ids
             );
         }
@@ -921,4 +921,4 @@ sub staff_save_search {
     return ($cache_key, $list);
 }
 
-1;
\ No newline at end of file
+1;
diff --git a/Open-ILS/src/perlmods/live_t/30-courses.t b/Open-ILS/src/perlmods/live_t/30-courses.t
new file mode 100644 (file)
index 0000000..480206c
--- /dev/null
@@ -0,0 +1,23 @@
+#!perl
+use Test::More tests => 1;
+
+diag("Test the course materials module.");
+
+use strict; use warnings;
+
+use OpenILS::Utils::TestUtils;
+my $script = OpenILS::Utils::TestUtils->new();
+$script->bootstrap;
+
+our $apputils   = "OpenILS::Application::AppUtils";
+
+is(1, 1, 'placeholder');
+
+
+# Test: can attach a bib record with located URI
+# Test: cannot attach a bib record without a located URI
+
+# Test: can detach an item (just delete this)
+# Test: can detach a record that is not temporary (just delete this)
+# Test: can detach a record that is temporary (delete this, and delete the record too)
index 5a45e2a..0207797 100644 (file)
@@ -1125,22 +1125,15 @@ CREATE TABLE asset.course_module_course_users (
 CREATE TABLE asset.course_module_course_materials (
     id              SERIAL PRIMARY KEY,
     course          INT NOT NULL REFERENCES asset.course_module_course (id),
-    item            INT NOT NULL REFERENCES asset.copy (id),
+    item            INT REFERENCES asset.copy (id),
     relationship    TEXT,
     record          INT REFERENCES biblio.record_entry (id),
+    temporary_record       BOOLEAN,
     original_location      INT REFERENCES asset.copy_location,
     original_status        INT REFERENCES config.copy_status,
     original_circ_modifier TEXT, --REFERENCES config.circ_modifier
     original_callnumber    INT REFERENCES asset.call_number,
-    unique (course, item)
-);
-
-CREATE TABLE asset.course_module_non_cat_course_materials (
-    id              SERIAL PRIMARY KEY,
-    course          INT NOT NULL REFERENCES asset.course_module_course (id),
-    item            TEXT NOT NULL,
-    url             TEXT,
-    relationship    TEXT
+    unique (course, item, record)
 );
 
 COMMIT;
index 4ed29ae..b62ea32 100644 (file)
@@ -3854,6 +3854,17 @@ INSERT into config.org_unit_setting_type
         'coust', 'description'),
     'bool', null)
 
+,( 'circ.course_materials_brief_record_bib_source', 'circ',
+    oils_i18n_gettext(
+        'circ.course_materials_brief_record_bib_source',
+        'Bib source for brief records created in the course materials module',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'circ.course_materials_brief_record_bib_source',
+        'The course materials module will use this bib source for any new brief bibliographic records made inside that module. For best results, use a transcendant bib source.',
+        'coust', 'description'),
+    'link', 'cbs')
+
 
 ,( 'circ.password_reset_request_per_user_limit', 'sec',
     oils_i18n_gettext('circ.password_reset_request_per_user_limit',
@@ -20620,6 +20631,14 @@ VALUES (
         'cwst', 'label'
     )
 );
+INSERT INTO config.bib_source (quality, source, transcendant) VALUES
+    (1, oils_i18n_gettext(1, 'Course materials module', 'cbs', 'source'), TRUE);
+
+INSERT INTO actor.org_unit_setting (org_unit, name, value)
+    SELECT 1, 'circ.course_materials_brief_record_bib_source', id
+    FROM config.bib_source
+    WHERE source='Course materials module';
+
 
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
index b934d09..a1fc120 100644 (file)
@@ -22,22 +22,15 @@ CREATE TABLE asset.course_module_course_users (
 CREATE TABLE asset.course_module_course_materials (
     id              SERIAL PRIMARY KEY,
     course          INT NOT NULL REFERENCES asset.course_module_course (id),
-    item            INT NOT NULL REFERENCES asset.copy (id),
+    item            INT REFERENCES asset.copy (id),
     relationship    TEXT,
     record          INT REFERENCES biblio.record_entry (id),
+    temporary_record         BOOLEAN,
     original_location        INT REFERENCES asset.copy_location,
     original_status          INT REFERENCES config.copy_status,
     original_circ_modifier   TEXT, --REFERENCES config.circ_modifier,
     original_callnumber      INT REFERENCES asset.call_number,
-    unique (course, item)
-);
-
-CREATE TABLE asset.course_module_non_cat_course_materials (
-    id              SERIAL PRIMARY KEY,
-    course          INT NOT NULL REFERENCES asset.course_module_course (id),
-    item            TEXT NOT NULL,
-    url             TEXT,
-    relationship    TEXT
+    unique (course, item, record)
 );
 
 INSERT INTO permission.perm_list(id, code, description)
@@ -55,7 +48,7 @@ INSERT INTO permission.perm_list(id, code, description)
 INSERT INTO permission.grp_perm_map(perm, grp, depth) VALUES (624, 9, 0), (624, 11, 0), (624, 12, 0), (624, 13, 0);
 
 INSERT INTO config.org_unit_setting_type 
-    (grp, name, datatype, label, description)
+    (grp, name, datatype, label, description, fm_class)
 VALUES (
     'circ',
     'circ.course_materials_opt_in', 'bool',
@@ -70,7 +63,7 @@ VALUES (
         'If enabled, the Org Unit will utilize Course Material functionality.'
         'coust',
         'description'
-    )
+    ), null
 ), (
     'circ',
     'circ.course_materials_browse_by_instructor', 'bool',
@@ -85,7 +78,29 @@ VALUES (
         'If enabled, the Org Unit will allow OPAC users to browse Courses by instructor name.'
         'coust',
         'description'
-    )
+    ), null
+), (
+    'circ',
+    'circ.course_materials_brief_record_bib_source', 'link',
+    oils_i18n_gettext(
+        'circ.course_materials_brief_record_bib_source',
+        'Bib source for brief records created in the course materials module',
+        'coust', 'label'
+    ),
+    oils_i18n_gettext(
+        'circ.course_materials_brief_record_bib_source',
+        'The course materials module will use this bib source for any new brief bibliographic records made inside that module. For best results, use a transcendant bib source.',
+        'coust', 'description'
+    ), 'cbs'
+
 );
 
+INSERT INTO config.bib_source (quality, source, transcendant) VALUES
+    (1, oils_i18n_gettext(1, 'Course materials module', 'cbs', 'source'), TRUE);
+
+INSERT INTO actor.org_unit_setting (org_unit, name, value)
+    SELECT 1, 'circ.course_materials_brief_record_bib_source', id
+    FROM config.bib_source
+    WHERE source='Course materials module';
+
 COMMIT;
index d48ce11..2d80caf 100644 (file)
             </tr>
           </thead>
           <tbody>
-            [% FOREACH copy_info IN ctx.course_materials %]
+            [% FOREACH material IN ctx.course_materials %]
               <tr>
-                <td>[%- INCLUDE "opac/parts/library_name_link.tt2"; -%]
-                    <link property="businessFunction" href="http://purl.org/goodrelations/v1#LeaseOut">
-                    <meta property="price" content="0.00">
+                <td>
+                  [% IF material.item %]
+                    [%- fleshed_ou = material.item.circ_lib -%]
+                    [%- INCLUDE "opac/parts/library_name_link_from_ou.tt2"; -%]
+                  [% ELSE %]
+                    [% l('Online') %]
+                  [% END %]
+                  <link property="businessFunction" href="http://purl.org/goodrelations/v1#LeaseOut">
+                  <meta property="price" content="0.00">
+                </td>
+                <td>
+                  [% IF material.item %]
+                    [% material.item.call_number.label %]
+                  [% END %]
                 </td>
-                <td>[% copy_info.call_number %]</td>
                 <td>
-                  <a href="[% mkurl(ctx.opac_root _ '/record/' _ copy_info.record) %]">
-                    [% copy_info.title %]
+                  <a href="[% mkurl(ctx.opac_root _ '/record/' _ material.record.id) %]">
+                    [% material.record.wide_display_entry.title %]
                   </a>
                 </td>
-                <td>[% copy_info.barcode %]</td>
-                <td>[% copy_info.relationship %]</td>
-                <td>[% copy_info.status.name %]</td>
-                <td>[% copy_info.location.name %]</td>
+                <td>
+                  [% IF material.item %]
+                    [% material.item.barcode %]
+                  [% END %]
+                </td>
+                <td> [% material.relationship %] </td>
+                <td>
+                  [% IF material.item %]
+                    [% material.item.status.name %]
+                  [% END %]
+                </td>
+                <td>
+                  [% IF material.item %]
+                    [% material.item.location.name %]
+                  [% END %]
+                </td>
               </tr>
             [% END %]
           </tbody>