LP1849212: Allow users to detach all types of materials from courses
authorJane Sandberg <sandbej@linnbenton.edu>
Fri, 24 Jul 2020 20:16:25 +0000 (13:16 -0700)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 14 Sep 2020 22:17:13 +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>
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/perlmods/lib/OpenILS/Application/Courses.pm
Open-ILS/src/perlmods/live_t/30-courses.t [deleted file]
Open-ILS/src/perlmods/live_t/31-courses.t [new file with mode: 0644]

index 8c76cab..87288f5 100644 (file)
@@ -3114,13 +3114,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
     <class id="acmc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_course" oils_persist:tablename="asset.course_module_course" reporter:label="Course">
         <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_course_id_seq">
             <field reporter:label="ID" name="id" reporter:datatype="id" />
-            <field reporter:label="Title" name="name" reporter:datatype="text" />
-            <field reporter:label="Course Number" name="course_number" reporter:datatype="text" />
+            <field reporter:label="Course Name" name="name" reporter:datatype="text" oils_obj:required="true" />
+            <field reporter:label="Course Number" name="course_number" reporter:datatype="text" oils_obj:required="true" />
             <field reporter:label="Section Number" name="section_number" reporter:datatype="text" />
-            <field reporter:label="Owning Library" name="owning_lib" reporter:datatype="link" />
+            <field reporter:label="Owning Library" name="owning_lib" reporter:datatype="link" oils_obj:required="true" />
             <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="bool" />
         </fields>
         <links>
index 724545a..242357b 100644 (file)
                   placeholder="e.g. Required" 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">
-                  <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-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>
               <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>
+                    <label for="bib-id" class="input-group-text" i18n>Bibliographic Record ID</label>
                   </div>
-                  <input type="text" [(ngModel)]="relationshipInput"
+                  <input type="text" [(ngModel)]="bibId" id="bib-id"
                     [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>
+                    <span class="input-group-text" i18n>Relationship</span>
                   </div>
-                  <input type="text" [(ngModel)]="bibId" id="bib-id"
+                  <input type="text" [(ngModel)]="relationshipInput"
                     [disabled]="currentCourse && currentCourse.is_archived() == 't'" class="flex-grow-1" />
                 </div>
               </div>
       </ngb-tabset>
 
       <div class="mt-3" [ngClass]="isDialog() ? 'col-md-12' : 'col-md-8'">
-        <eg-grid #materialsGrid [dataSource]="materialsDataSource">
+        <eg-grid #materialsGrid [dataSource]="materialsDataSource" [useLocalSort]="true">
           <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 label="Barcode" i18n-label name="barcode" [cellTemplate]="barcodeCellTemplate"></eg-grid-column>
+          <eg-grid-column label="Title" i18n-label name="title" flex="3" [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>
index b50634f..541ea1a 100644 (file)
@@ -1,7 +1,6 @@
 import {Component, Input, ViewChild, OnInit, TemplateRef} from '@angular/core';
 import {ActivatedRoute} from '@angular/router';
-import {from, Observable} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
+import {from, merge, Observable} from 'rxjs';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 import {AuthService} from '@eg/core/auth.service';
 import {NetService} from '@eg/core/net.service';
@@ -29,7 +28,7 @@ export class CourseAssociateMaterialComponent extends DialogComponent implements
     @Input() displayMode: String;
     materials: any[] = [];
     @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
-    @ViewChild('materialsGrid', {static: true}) materialsGrid: GridComponent;
+    @ViewChild('materialsGrid', {static: false}) materialsGrid: GridComponent;
     @ViewChild('materialDeleteFailedString', { static: true })
         materialDeleteFailedString: StringComponent;
     @ViewChild('materialDeleteSuccessString', { static: true })
@@ -206,24 +205,20 @@ export class CourseAssociateMaterialComponent extends DialogComponent implements
     }
 
     deleteSelectedMaterials(items) {
-        const item_ids = [];
+        const deleteRequest$ = [];
         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));
-                }
-            );
+            deleteRequest$.push(this.net.request(
+                'open-ils.courses', 'open-ils.courses.detach_material',
+                this.auth.token(), item.id()));
         });
+        merge(...deleteRequest$).subscribe(
+            val => {
+                this.materialDeleteSuccessString.current().then(str => this.toast.success(str));
+            },
+            err => {
+                this.materialDeleteFailedString.current()
+                    .then(str => this.toast.danger(str));
+            }
+        );
     }
 }
index fe32164..a12831c 100644 (file)
@@ -45,7 +45,7 @@ sub attach_electronic_resource_to_course {
         }
     ]);
     return $e->event unless (($bib->source && $bib->source->transcendant) || $located_uris);
-    _attach_bib($e, $course, $record, $relationship);
+    _attach_bib($e, $course, $record, $relationship, 0);
 
     return 1;
 }
@@ -84,26 +84,22 @@ sub attach_brief_bib_to_course {
         ->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);
+    _attach_bib($e, $course, $bib_create->id, $relationship, 1) if ($bib_create);
     return 1;
 }
 
 # Shared logic for both e-resources and brief bibs
 sub _attach_bib {
-    my ($e, $course, $record, $relationship) = @_;
+    my ($e, $course, $record, $relationship, $temporary) = @_;
     my $acmcm = Fieldmapper::asset::course_module_course_materials->new;
     $acmcm->course($course);
     $acmcm->record($record);
     $acmcm->relationship($relationship);
+    $acmcm->temporary_record($temporary);
     $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,
@@ -229,6 +225,79 @@ sub fetch_course_users {
 
 }
 
+__PACKAGE__->register_method(
+    method          => 'detach_material',
+    api_name        => 'open-ils.courses.detach_material',
+    signature => {
+        desc => 'Detaches a material from a course',
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'Course material id', type => 'number'},
+        ],
+        return => {desc => '1 on success, event on failure'}
+    });
+sub detach_material {
+    my ($self, $conn, $authtoken, $acmcm_id) = @_;
+    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 $acmcm = $e->retrieve_asset_course_module_course_materials($acmcm_id)
+        or return $e->die_event;
+    my $bre_id_to_delete = $acmcm->temporary_record ? $acmcm->record : 0;
+    if ($bre_id_to_delete) {
+        # delete any attached located URIs
+        my $located_uri_cn_ids = $e->search_asset_call_number(
+            {record=>$bre_id_to_delete}, {idlist=>1});
+
+        for my $cn_id (@$located_uri_cn_ids) {
+            $e->delete_asset_call_number(
+                $e->retrieve_asset_call_number($cn_id))
+                or return $e->die_event;
+        }
+        OpenSRF::AppSession
+            ->create('open-ils.cat')
+            ->request('open-ils.cat.biblio.record_entry.delete',
+                $authtoken, $bre_id_to_delete);
+    }
+    if ($acmcm->item) {
+        _resetItemFields($e, $authtoken, $acmcm);
+    } 
+
+    $e->delete_asset_course_module_course_materials($acmcm) or return $e->die_event;
+    $e->commit;
+    return 1;
+}
+
+sub _resetItemFields {
+    my ($e, $authtoken, $acmcm) = @_;
+    my $cat_sess = OpenSRF::AppSession->connect('open-ils.cat');
+    my $acp = $e->retrieve_asset_copy($acmcm->item);
+    my $course_lib = $e->retrieve_asset_course_module_course($acmcm->course)->owning_lib;
+    if ($acmcm->original_status) {
+        $acp->status($acmcm->orginal_status);
+    }
+    if ($acmcm->original_circ_modifier) {
+        $acp->status($acmcm->orginal_circ_modifier);
+    }
+    if ($acmcm->original_location) {
+        $acp->status($acmcm->orginal_location);
+    }
+    $e->update_asset_copy($acmcm);
+    if ($acmcm->original_callnumber) {
+        my $existing_acn = $e->retrieve_asset_call_number($acp->call_number);
+        # Let's attach to an existing call number, if one exists with the original label
+        # and other appropriate specifications
+        my $acn_id = cat_sess->request('open-ils.cat.call_number.find_or_create',
+            $authtoken, $acmcm->original_callnumber,
+            $existing_acn->record, $course_lib,
+            $existing_acn->prefix, $existing_acn->suffix,
+            $existing_acn->label_class)->acn_id;
+        cat_sess->request('open-ils.cat.transfer_copies_to_volume',
+            $authtoken, $acn_id, [$acp->id]);
+    }
+}
+
 
 
 1;
diff --git a/Open-ILS/src/perlmods/live_t/30-courses.t b/Open-ILS/src/perlmods/live_t/30-courses.t
deleted file mode 100644 (file)
index 480206c..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-#!perl
-use Test::More tests => 1;
-
-diag("Test the course materials module.");
-
-use strict; use warnings;
-
-use OpenILS::Utils::TestUtils;
-my $script = OpenILS::Utils::TestUtils->new();
-$script->bootstrap;
-
-our $apputils   = "OpenILS::Application::AppUtils";
-
-is(1, 1, 'placeholder');
-
-
-# Test: can attach a bib record with located URI
-# Test: cannot attach a bib record without a located URI
-
-# Test: can detach an item (just delete this)
-# Test: can detach a record that is not temporary (just delete this)
-# Test: can detach a record that is temporary (delete this, and delete the record too)
diff --git a/Open-ILS/src/perlmods/live_t/31-courses.t b/Open-ILS/src/perlmods/live_t/31-courses.t
new file mode 100644 (file)
index 0000000..da14e65
--- /dev/null
@@ -0,0 +1,82 @@
+#!perl
+use strict; use warnings;
+use Test::More tests => 3;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+
+diag("Test the course materials module.");
+
+my $script = OpenILS::Utils::TestUtils->new();
+our $apputils = 'OpenILS::Application::AppUtils';
+
+# we need auth to access protected APIs
+$script->authenticate({
+    username => 'admin',
+    password => 'demo123',
+    type => 'staff'});
+
+my $authtoken = $script->authtoken;
+ok($authtoken, 'Have an authtoken');
+
+my $e = new_editor(xact => 1);
+$e->init;
+
+
+# -----------------------------------------------------------------------------
+# 1. Let's attach an existing biblio record entry to course #1, then delete it
+# -----------------------------------------------------------------------------
+
+my $acmcm = Fieldmapper::asset::course_module_course_materials->new;
+$acmcm->course(1);
+$acmcm->id(9999);
+$acmcm->record(55);
+$acmcm->temporary_record(0);
+$e->create_asset_course_module_course_materials( $acmcm ); # associated this bib record with a course
+$e->commit;
+
+$apputils->simplereq('open-ils.courses', 'open-ils.courses.detach.material', $authtoken, 9999);
+
+my $results = $e->search_asset_course_module_course_materials({id => 9999});
+is(scalar(@$results), 0, 'Successfully deleted acmcm');
+
+$results = $e->search_biblio_record_entry({id => 55, deleted => 0});
+
+is(scalar(@$results), 1,
+    'Did not inadvertantly delete bre');
+
+
+# -----------------------------------------------------------------------------
+# 2. Let's create a brief temporary bib record, attach it to course #1, then detach it
+# -----------------------------------------------------------------------------
+
+my $temp_tcn_source = 'temporary bib record for course materials module test';
+
+$e->xact_begin;
+my $bre = Fieldmapper::biblio::record_entry->new;
+$bre->marc('<record></record>');
+$bre->tcn_source($temp_tcn_source); #Use the tcn_source field, since Cstore rewrites the last_xact_id field
+$e->create_biblio_record_entry($bre);
+$e->commit;
+
+my $bib_id = $e->search_biblio_record_entry({tcn_source=>$temp_tcn_source}, {idlist=>1})->[0];
+
+$e->xact_begin;
+$acmcm = Fieldmapper::asset::course_module_course_materials->new;
+$acmcm->course(1);
+$acmcm->id(9998);
+$acmcm->record($bib_id);
+$acmcm->temporary_record(1); # this one is temporary, like brief records created in the course module interface
+$e->create_asset_course_module_course_materials( $acmcm ); # associated this bib record with a course
+$e->commit;
+
+$apputils->simplereq('open-ils.courses', 'open-ils.courses.detach.material', $authtoken, 9998);
+
+$results = $e->search_asset_course_module_course_materials({id => 9998});
+is(scalar(@$results), 0, 'Successfully deleted acmcm');
+
+$results = $e->search_biblio_record_entry({tcn_source=>$temp_tcn_source,deleted=>0});
+is(scalar(@$results), 0, 'Successfully deleted bre');
+
+