lp1922388 Academic Departments user/khuckins/lp1922388-course-academic-departments-submodule
authorKyle Huckins <khuckins@catalyte.io>
Sun, 13 Mar 2022 09:15:38 +0000 (09:15 +0000)
committerKyle Huckins <khuckins@catalyte.io>
Mon, 14 Mar 2022 04:09:45 +0000 (04:09 +0000)
- Add DB and IDL structure for Academic Departments, Department Contacts, and Course/Department Map
- Add Departments to Course List
- Add Edit page for Departments similar to Course Edit Page, including Department Contacts Tab

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/examples/fm_IDL.xml
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.html
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list.component.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-reserves.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/routing.module.ts
modified:   Open-ILS/src/eg2/src/app/staff/share/course.service.ts
modified:   Open-ILS/src/sql/Pg/040.schema.asset.sql
modified:   Open-ILS/src/sql/Pg/950.data.seed-values.sql
new file:   Open-ILS/src/sql/Pg/upgrade/xxxx.schema.lp1922388-academic-departments.sql

15 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.ts [new file with mode: 0644]
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-reserves.module.ts
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/routing.module.ts
Open-ILS/src/eg2/src/app/staff/share/course.service.ts
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.lp1922388-academic-departments.sql [new file with mode: 0644]

index 6d8199f..08d3c04 100644 (file)
@@ -3253,16 +3253,20 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <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" oils_obj:required="true" />
+            <field reporter:label="Department" name="department" 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="Is Archived?" name="is_archived" reporter:datatype="bool" />
                        <field reporter:label="Terms Taught" name="terms_map" oils_persist:virtual="true" reporter:datatype="link" config_field="true" />
+                       <field reporter:label="Departments" name="departments_map" oils_persist:virtual="true" reporter:datatype="link" config_field="true" />
         </fields>
         <links>
             <link field="owning_lib" reltype="has_a" key="id" map="" class="aou" />
+            <link field="department" reltype="has_a" key="id" map="" class="acmd" />
             <link field="members" reltype="has_many" key="course" map="" class="acmcu" />
             <link field="materials" reltype="has_many" key="course" map="" class="acmcm" />
                        <link field="terms_map" reltype="has_many" key="course" map="" class="acmtcm" />
+                       <link field="departments_map" reltype="has_many" key="course" map="" class="acmtcm" />
         </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -3401,6 +3405,84 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        </actions>
                </permacrud>
        </class>
+    <class id="acmd" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_department" oils_persist:tablename="asset.course_module_department" reporter:label="Academic Department">
+        <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_department_id_seq">
+            <field reporter:label="ID" name="id" reporter:datatype="id" />
+            <field reporter:label="Name" name="name" reporter:datatype="text" oils_obj:required="true" />
+            <field reporter:label="Owning Library" name="owning_lib" reporter:datatype="link" oils_obj:required="true" />
+            <field reporter:label="Members" name="members" oils_persist:virtual="true" reporter:datatype="link" />
+                       <field reporter:label="Courses" name="courses" reporter:datatype="link" oils_persist:virtual="true"/>
+                       <field reporter:label="Course Maps" name="maps" reporter:datatype="link" oils_persist:virtual="true"/>
+        </fields>
+        <links>
+            <link field="owning_lib" reltype="has_a" key="id" map="" class="aou" />
+            <link field="members" reltype="has_many" key="course" map="" class="acmc" />
+            <link field="materials" reltype="has_many" key="course" map="" class="acmcm" />
+                       <link field="terms_map" reltype="has_many" key="course" map="" class="acmtcm" />
+                       <link field="courses" reltype="has_many" key="department" map="course" class="acmtcm"/>
+                       <link field="maps" reltype="has_many" key="department" map="" class="acmtcm"/>
+        </links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="MANAGE_RESERVES" context_field="owning_lib"/>
+                <retrieve/>
+                <update permission="MANAGE_RESERVES" context_field="owning_lib"/>
+                <delete permission="MANAGE_RESERVES" context_field="owning_lib"/>
+            </actions>
+        </permacrud>
+    </class>
+    <class id="acmdc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_department_contacts" oils_persist:tablename="asset.course_module_department_contacts" reporter:label="Department Contacts">
+        <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_department_contacts_id_seq">
+            <field reporter:label="ID" name="id" reporter:datatype="id" />
+            <field reporter:label="Department" name="department" reporter:datatype="link" />
+            <field reporter:label="User" name="usr" reporter:datatype="link" />
+            <field reporter:label="User Role" name="usr_role" reporter:datatype="link" />
+        </fields>
+        <links>
+            <link field="department" reltype="has_a" key="id" map="" class="acmd" />
+            <link field="usr" reltype="has_a" key="id" map="" class="au" />
+            <link field="usr_role" reltype="has_a" key="id" map="" class="acmr" />
+        </links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="MANAGE_RESERVES">
+                                       <context link="department" field="owning_lib" />
+                </create>
+                <retrieve/>
+                <update permission="MANAGE_RESERVES">
+                                       <context link="department" field="owning_lib" />
+                </update>
+                <delete permission="MANAGE_RESERVES">
+                                       <context link="department" field="owning_lib" />
+                </delete>
+            </actions>
+        </permacrud>
+    </class>
+    <class id="acmdcm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::course_module_department_course_map" oils_persist:tablename="asset.course_module_department_course_map" reporter:label="Course Department Map" oils_persist:field_safe="true">
+        <fields oils_persist:primary="id" oils_persist:sequence="asset.course_module_department_course_map_id_seq">
+            <field reporter:label="Course Term Map ID" name="id" reporter:datatype="id"/>
+            <field reporter:label="Department" name="department" oils_obj:required="true" reporter:datatype="link"/>
+            <field reporter:label="Course" name="course" reporter:datatype="link" oils_obj:required="true"/>
+        </fields>
+        <links>
+            <link field="department" reltype="has_a" key="id" map="" class="acmd"/>
+            <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">
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.html
new file mode 100644 (file)
index 0000000..dd5f034
--- /dev/null
@@ -0,0 +1,29 @@
+<ng-container>\r
+  <div class="row">\r
+    <div class="col-lg-6">\r
+      <eg-org-family-select\r
+        [limitPerms]="['MANAGE_RESERVES']"\r
+        [selectedOrgId]="defaultOuId"\r
+        [(ngModel)]="searchOrgs"\r
+        (ngModelChange)="grid.reload()">\r
+      </eg-org-family-select>\r
+      </div>\r
+  </div>\r
+  <hr/>\r
+\r
+  <div class="w-100 mt-2 mb-2">\r
+    <eg-grid #grid idlClass={{idlClass}}\r
+      [dataSource]="grid_source"\r
+      [sortable]="true">\r
+      <eg-grid-toolbar-button\r
+        label="Create {{tableName}}" (onClick)="createNew()" i18n-label>\r
+      </eg-grid-toolbar-button>\r
+      <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)">\r
+      </eg-grid-toolbar-action>\r
+      <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">\r
+      </eg-grid-toolbar-action>\r
+      <eg-grid-column label="ID" path="id" [index]=true [hidden]="true" i18n-label></eg-grid-column>\r
+      <eg-grid-column label="Department Name" name="name" i18n-label></eg-grid-column>\r
+    </eg-grid>\r
+  </div>\r
+</ng-container>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/course-list-department.component.ts
new file mode 100644 (file)
index 0000000..e47f20c
--- /dev/null
@@ -0,0 +1,167 @@
+import {Component, Input, ViewChild, OnInit, AfterViewInit} from '@angular/core';\r
+import {Router} from '@angular/router';\r
+import {IdlObject, IdlService} from '@eg/core/idl.service';\r
+import {PcrudService} from '@eg/core/pcrud.service';\r
+import {CourseService} from '@eg/staff/share/course.service';\r
+import {GridComponent} from '@eg/share/grid/grid.component';\r
+import {Pager} from '@eg/share/util/pager';\r
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';\r
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';\r
+import {StringComponent} from '@eg/share/string/string.component';\r
+import {ToastService} from '@eg/share/toast/toast.service';\r
+import {LocaleService} from '@eg/core/locale.service';\r
+import {AuthService} from '@eg/core/auth.service';\r
+import {OrgService} from '@eg/core/org.service';\r
+import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';\r
+\r
+import {DepartmentContactGridComponent\r
+    } from './department-contact-grid.component';\r
+\r
+@Component({\r
+    selector: 'eg-department-list',\r
+    templateUrl: './course-list-department.component.html'\r
+})\r
+\r
+export class DepartmentListComponent implements OnInit, AfterViewInit {\r
+\r
+    @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;\r
+    @ViewChild('grid') grid: GridComponent;\r
+    @ViewChild('successString', { static: true }) successString: StringComponent;\r
+    @ViewChild('createString') createString: StringComponent;\r
+    @ViewChild('createErrString') createErrString: StringComponent;\r
+    @ViewChild('updateFailedString') updateFailedString: StringComponent;\r
+    @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;\r
+    @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;\r
+    @ViewChild('departmentContactGrid', {static: true})\r
+        private departmentContactGrid: DepartmentContactGridComponent;\r
+\r
+    @Input() sortField: string;\r
+    @Input() idlClass = 'acmd';\r
+    @Input() dialog_size: 'sm' | 'lg' = 'lg';\r
+    @Input() tableName = 'Academic Department';\r
+    grid_source: GridDataSource = new GridDataSource();\r
+    search_value = '';\r
+    defaultOuId: number;\r
+    searchOrgs: OrgFamily;\r
+\r
+\r
+    constructor(\r
+        private courseSvc: CourseService,\r
+        private locale: LocaleService,\r
+        private auth: AuthService,\r
+        private idl: IdlService,\r
+        private org: OrgService,\r
+        private pcrud: PcrudService,\r
+        private router: Router,\r
+        private toast: ToastService\r
+    ) {}\r
+\r
+    ngOnInit() {\r
+        this.getSource();\r
+        this.defaultOuId = this.auth.user().ws_ou() || this.org.root().id();\r
+        this.searchOrgs = {primaryOrgId: this.defaultOuId};\r
+    }\r
+\r
+    ngAfterViewInit() {\r
+        this.grid.onRowActivate.subscribe((department: IdlObject) => {\r
+            const idToEdit = department.id();\r
+            this.navigateToDepartmentPage(idToEdit);\r
+        });\r
+\r
+    }\r
+\r
+    /**\r
+     * Gets the data, specified by the class, that is available.\r
+     */\r
+    getSource() {\r
+        this.grid_source.getRows = (pager: Pager, sort: any[]) => {\r
+            const orderBy: any = {};\r
+            if (sort.length) {\r
+                // Sort specified from grid\r
+                orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;\r
+            } else if (this.sortField) {\r
+                // Default sort field\r
+                orderBy[this.idlClass] = this.sortField;\r
+            }\r
+            const search: any = new Array();\r
+            const orgFilter: any = {};\r
+            orgFilter['owning_lib'] =\r
+                this.searchOrgs.orgIds || [this.defaultOuId];\r
+            search.push(orgFilter);\r
+            const searchOps = {\r
+                offset: pager.offset,\r
+                limit: pager.limit,\r
+                order_by: orderBy\r
+            };\r
+            return this.pcrud.search(this.idlClass, search, searchOps, {fleshSelectors: true});\r
+        };\r
+    }\r
+\r
+    navigateToDepartmentPage(id_arr: IdlObject[]) {\r
+        if (typeof id_arr === 'number') { id_arr = [id_arr]; }\r
+        const urls = [];\r
+        id_arr.forEach(id => {console.log(this.router.url);\r
+            urls.push([this.locale.currentLocaleCode() + this.router.url + '/' +  id]);\r
+        });\r
+        if (id_arr.length === 1) {\r
+        this.router.navigate([this.router.url + '/department/' + id_arr[0]]);\r
+        } else {\r
+            urls.forEach(url => {\r
+                window.open(url);\r
+            });\r
+        }\r
+    }\r
+\r
+    createNew() {\r
+        this.editDialog.mode = 'create';\r
+        const course_module_department = this.idl.create('acmd');\r
+        course_module_department.owning_lib(this.auth.user().ws_ou());\r
+        this.editDialog.recordId = null;\r
+        this.editDialog.record = course_module_department;\r
+        this.editDialog.open({size: this.dialog_size}).subscribe(\r
+            ok => {\r
+                this.createString.current()\r
+                    .then(str => this.toast.success(str));\r
+                this.grid.reload();\r
+            },\r
+            rejection => {\r
+                if (!rejection.dismissed) {\r
+                    this.createErrString.current()\r
+                        .then(str => this.toast.danger(str));\r
+                }\r
+            }\r
+        );\r
+    }\r
+\r
+    editSelected(fields: IdlObject[]) {\r
+        // Edit each IDL thing one at a time\r
+        const department_ids = [];\r
+        fields.forEach(field => {\r
+            if (typeof field['id'] === 'function') {\r
+                department_ids.push(field.id());\r
+            } else {\r
+                department_ids.push(field['id']);\r
+            }\r
+        });\r
+        this.navigateToDepartmentPage(department_ids);\r
+    }\r
+\r
+    deleteSelected(idlObject: IdlObject[]) {\r
+        idlObject.forEach(object => {\r
+            object.isdeleted(true);\r
+        });\r
+        this.pcrud.autoApply(idlObject).subscribe(\r
+            val => {\r
+                console.debug('deleted: ' + val);\r
+                this.deleteSuccessString.current()\r
+                    .then(str => this.toast.success(str));\r
+            },\r
+            err => {\r
+                this.deleteFailedString.current()\r
+                    .then(str => this.toast.danger(str));\r
+            },\r
+            () => this.grid.reload()\r
+        );\r
+    }\r
+}\r
+\r
index 6fb0972..84edd67 100644 (file)
       <eg-admin-page idlClass="acmr"></eg-admin-page>
     </ng-template>
   </li>
+  <li ngbNavItem>
+    <a ngbNavLink i18n>Departments</a>
+    <ng-template ngbNavContent>
+      <eg-department-list></eg-department-list>
+    </ng-template>
+  </li>
 </ul>
 <div [ngbNavOutlet]="courseListNav"></div>
 
index 2ba9d40..78f6355 100644 (file)
@@ -14,6 +14,9 @@ import {AuthService} from '@eg/core/auth.service';
 import {OrgService} from '@eg/core/org.service';
 import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
 
+import {DepartmentListComponent
+    } from './course-list-department.component';
+
 import {CourseAssociateMaterialComponent
     } from './course-associate-material.component';
 
index 149a39a..7083328 100644 (file)
@@ -12,6 +12,9 @@ import {MarcSimplifiedEditorModule} from '@eg/staff/share/marc-edit/simplified-e
 import {PatronModule} from '@eg/staff/share/patron/patron.module';
 import {CourseTermMapComponent} from './course-term-map.component';
 import {CourseTermMapGridComponent} from './course-term-map-grid.component';
+import {DepartmentContactGridComponent} from './department-contact-grid.component';
+import {DepartmentPageComponent} from './department-page.component';
+import {DepartmentListComponent} from './course-list-department.component';
 
 @NgModule({
   declarations: [
@@ -20,7 +23,10 @@ import {CourseTermMapGridComponent} from './course-term-map-grid.component';
     CourseAssociateMaterialComponent,
     CourseAssociateUsersComponent,
     CourseTermMapComponent,
-    CourseTermMapGridComponent
+    CourseTermMapGridComponent,
+    DepartmentContactGridComponent,
+    DepartmentPageComponent,
+    DepartmentListComponent
   ],
   imports: [
     StaffCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.html
new file mode 100644 (file)
index 0000000..82f773f
--- /dev/null
@@ -0,0 +1,80 @@
+<eg-string #userDeleteFailedString i18n-text text="Removal of Contact failed or was not allowed"></eg-string>\r
+<eg-string #userDeleteSuccessString i18n-text text="Removal of Contact succeeded"></eg-string>\r
+<eg-string #userAddSuccessString i18n-text text="Addition of Contact succeeded"></eg-string>\r
+<eg-string #userAddFailedString i18n-text text="Addition of Contact failed or was not allowed"></eg-string>\r
+<eg-string #userEditSuccessString i18n-text text="Update of Contact succeeded"></eg-string>\r
+<eg-string #userEditFailedString i18n-text text="Update of Contact failed or was not allowed"></eg-string>\r
+\r
+<eg-patron-search-dialog #patronSearch>\r
+</eg-patron-search-dialog>\r
+\r
+<div [ngClass]="isDialog() ? 'modal-body' : ''">\r
+  <div class="row">\r
+    <div [ngClass]="isDialog() ? 'col-md-12' : 'col-md-4'">\r
+      <div class="row" [ngClass]="isDialog() ? '' : 'mt-3'">\r
+        <div class="d-flex"  [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12'">\r
+          <div class="input-group">\r
+            <div class="input-group-prepend">\r
+              <label for="associate-user-barcode" class="input-group-text" i18n>Patron Barcode</label>\r
+            </div>\r
+            <input type="text" class="flex-grow-1" id="associate-user-barcode"\r
+              [(ngModel)]="contactBarcode" (click)="$event.target.select()"\r
+              (keyup.enter)="associateContacts(contactBarcode)" />\r
+            <button class="btn btn-outline-dark btn-sm" (click)="searchPatrons()">\r
+              <span class="material-icons mat-icon-in-button align-middle"\r
+              i18n-title title="Search for Patron">search</span>\r
+              <span class="align-middle" i18n>Search for Patron</span>\r
+            </button>\r
+          </div>\r
+        </div>\r
+        <div class="d-flex" [ngClass]="isDialog() ? 'col-md-6' : 'col-md-12 mt-3'">\r
+          <div class="input-group">\r
+            <div class="input-group-prepend">\r
+              <label for="associate-user-role" class="input-group-text" i18n>Role</label>\r
+            </div>\r
+            <eg-combobox idlClass="acmr" [(ngModel)]="contactRoleInput"></eg-combobox>\r
+          </div>\r
+        </div>\r
+      </div>\r
+      <div class="row mt-3">\r
+        <div class="text-right" [ngClass]="isDialog() ? 'col-md-2' : 'col-md-6'">\r
+          <button class="btn btn-primary"\r
+            i18n [disabled]="!contactBarcode" (click)="associateContacts(contactBarcode)">\r
+            Add Contact\r
+          </button>\r
+        </div>\r
+      </div>\r
+    </div>\r
+    <div class="mt-3" [ngClass]="isDialog() ? 'col-md-12' : 'col-md-8'">\r
+      <eg-grid #contactsGrid [dataSource]="contactsDataSource" [useLocalSort]="true">\r
+        <eg-grid-toolbar-action label="Remove Selected" i18n-label (onClick)="deleteSelectedContacts($event)">\r
+        </eg-grid-toolbar-action>\r
+        <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelectedContacts($event)">\r
+        </eg-grid-toolbar-action>\r
+        <eg-grid-column label="Contact ID" path="usr.id" [index]=true [hidden]="true" i18n-label></eg-grid-column>\r
+        <eg-grid-column label="First Name" path="usr.first_given_name" i18n-label></eg-grid-column>\r
+        <eg-grid-column label="Second Name" path="usr.second_given_name" i18n-label></eg-grid-column>\r
+        <eg-grid-column label="Last Name" path="usr.family_name" i18n-label></eg-grid-column>\r
+        <eg-grid-column label="Prefix" path="usr.pref_prefix" [hidden]="true" i18n-label></eg-grid-column>\r
+        <eg-grid-column label="Preferred First Name" path="usr.pref_first_given_name"[hidden]="true"  i18n-label></eg-grid-column>\r
+        <eg-grid-column label="Preferred Second Name" path="usr.pref_second_given_name"[hidden]="true"  i18n-label></eg-grid-column>\r
+        <eg-grid-column label="Preferred Family Name" path="usr.pref_family_name"[hidden]="true"  i18n-label></eg-grid-column>\r
+        <eg-grid-column label="Preferred Suffix" path="usr.pref_suffix" [hidden]="true" i18n-label></eg-grid-column>\r
+        <eg-grid-column label="User Role" path="usr_role.name" i18n-label></eg-grid-column>\r
+      </eg-grid>\r
+    </div>\r
+  </div>\r
+</div>\r
+\r
+<ng-container *ngIf="!isDialog()">\r
+  <!-- in "inline" mode, render the grid pane right here -->\r
+  <ng-container *ngTemplateOutlet="dialogContent">\r
+  </ng-container>\r
+</ng-container>\r
+\r
+<eg-fm-record-editor #editDialog\r
+  idlClass='acmdc'\r
+  [fieldOptions]="{department: {linkedSearchField: 'name'}}"\r
+  [preloadLinkedValues]="true"\r
+  hiddenFields="id,usr">\r
+</eg-fm-record-editor>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-contact-grid.component.ts
new file mode 100644 (file)
index 0000000..aeb7531
--- /dev/null
@@ -0,0 +1,157 @@
+import {Component, Input, ViewChild, OnInit} from '@angular/core';\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 {PcrudService} from '@eg/core/pcrud.service';\r
+import {Pager} from '@eg/share/util/pager';\r
+import {NgbModal} 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} from '@eg/core/idl.service';\r
+import {StringComponent} from '@eg/share/string/string.component';\r
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';\r
+import {PatronSearchDialogComponent} from '@eg/staff/share/patron/search-dialog.component';\r
+import {ToastService} from '@eg/share/toast/toast.service';\r
+import {CourseService} from '@eg/staff/share/course.service';\r
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';\r
+\r
+@Component({\r
+    selector: 'eg-department-contact-grid',\r
+    templateUrl: './department-contact-grid.component.html'\r
+})\r
+\r
+export class DepartmentContactGridComponent extends DialogComponent implements OnInit {\r
+    @Input() currentDepartment: IdlObject;\r
+    @Input() departmentId: number;\r
+    @Input() displayMode: String;\r
+    users: any[] = [];\r
+    @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;\r
+    @ViewChild('patronSearch') patronSearch: PatronSearchDialogComponent;\r
+    @ViewChild('contactsGrid') contactsGrid: GridComponent;\r
+    @ViewChild('userDeleteFailedString', { static: true })\r
+        userDeleteFailedString: StringComponent;\r
+    @ViewChild('userDeleteSuccessString', { static: true })\r
+        userDeleteSuccessString: StringComponent;\r
+    @ViewChild('userAddSuccessString', { static: true })\r
+        userAddSuccessString: StringComponent;\r
+    @ViewChild('userAddFailedString', { static: true })\r
+        userAddFailedString: StringComponent;\r
+    @ViewChild('userEditSuccessString', { static: true })\r
+        userEditSuccessString: StringComponent;\r
+    @ViewChild('userEditFailedString', { static: true })\r
+        userEditFailedString: StringComponent;\r
+    contactsDataSource: GridDataSource;\r
+    contactBarcode: String;\r
+    contactRoleInput: ComboboxEntry;\r
+\r
+    constructor(\r
+        private auth: AuthService,\r
+        private course: CourseService,\r
+        private net: NetService,\r
+        private pcrud: PcrudService,\r
+        private toast: ToastService,\r
+        private modal: NgbModal\r
+    ) {\r
+        super(modal);\r
+        this.contactsDataSource = new GridDataSource();\r
+    }\r
+\r
+    ngOnInit() {\r
+        this.contactsDataSource.getRows = (pager: Pager, sort: any[]) => {\r
+            return this.course.getDepartmentContacts([this.departmentId]);\r
+        };\r
+    }\r
+\r
+    isDialog(): boolean {\r
+        return this.displayMode === 'dialog';\r
+    }\r
+\r
+    editSelectedContacts(contactFields: IdlObject[]) {\r
+        // Edit each IDL thing one at a time\r
+        const editOneThing = (contact: IdlObject) => {\r
+            if (!contact) { return; }\r
+\r
+            this.showEditDialog(contact).then(\r
+                () => editOneThing(contactFields.shift()));\r
+        };\r
+\r
+        editOneThing(contactFields.shift());\r
+    }\r
+\r
+    searchPatrons() {\r
+        this.patronSearch.open({size: 'xl'}).toPromise().then(\r
+            patrons => {\r
+                if (!patrons || patrons.length === 0) { return; }\r
+                const contact = patrons[0];\r
+                this.contactBarcode = contact.card().barcode();\r
+            }\r
+        );\r
+    }\r
+\r
+    associateContacts(barcode) {\r
+        if (barcode) {\r
+            const args = {\r
+                currentDepartment: this.currentDepartment,\r
+                barcode: barcode.trim(),\r
+            };\r
+\r
+            if (this.contactRoleInput) {\r
+                args['role'] = this.contactRoleInput.id;\r
+            }\r
+\r
+            this.contactBarcode = null;\r
+\r
+            this.net.request(\r
+                'open-ils.actor',\r
+                'open-ils.actor.user.retrieve_id_by_barcode_or_username',\r
+                this.auth.token(), barcode.trim()\r
+            ).subscribe(patron => {\r
+                    this.course.associateContacts(patron, args)\r
+                    .then(() => this.contactsGrid.reload());\r
+                }, err => {\r
+                    this.userAddFailedString.current().then(str => this.toast.danger(str));\r
+                }\r
+            );\r
+        }\r
+        \r
+    }\r
+\r
+    showEditDialog(contact: IdlObject): Promise<any> {\r
+        this.editDialog.mode = 'update';\r
+        this.editDialog.recordId = contact.id();\r
+        return new Promise((resolve, reject) => {\r
+            this.editDialog.open({size: 'lg'}).subscribe(\r
+                result => {\r
+                    this.userEditSuccessString.current()\r
+                        .then(str => this.toast.success(str));\r
+                    this.contactsGrid.reload();\r
+                    resolve(result);\r
+                },\r
+                error => {\r
+                    this.userEditFailedString.current()\r
+                        .then(str => this.toast.danger(str));\r
+                    reject(error);\r
+                }\r
+            );\r
+        });\r
+    }\r
+\r
+    deleteSelectedContacts(contacts) {\r
+        const acmdc_ids = contacts.map(u => u.id());\r
+        this.pcrud.search('acmdc', {id: acmdc_ids}).subscribe(contact => {\r
+            contact.isdeleted(true);\r
+            this.pcrud.autoApply(contact).subscribe(\r
+                val => {\r
+                    console.debug('deleted: ' + val);\r
+                    this.userDeleteSuccessString.current().then(str => this.toast.success(str));\r
+                    this.contactsGrid.reload();\r
+                },\r
+                err => {\r
+                    this.userDeleteFailedString.current()\r
+                        .then(str => this.toast.danger(str));\r
+                }\r
+            );\r
+        }).add(() => this.contactsGrid.reload());\r
+    }\r
+\r
+}\r
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.html
new file mode 100644 (file)
index 0000000..1f932c2
--- /dev/null
@@ -0,0 +1,48 @@
+<eg-staff-banner\r
+  bannerText=" {{currentDepartment.name()}}"\r
+  i18n-bannerText class="mb-3" *ngIf="currentDepartment"\r
+  [bannerStyle]="alert-secondary">\r
+</eg-staff-banner>\r
+\r
+<div class="row">\r
+  <div class="col text-right">\r
+    <a class="btn btn-warning ml-3" routerLink="/staff/admin/local/asset/course_list" i18n>\r
+      <i class="material-icons align-middle">keyboard_return</i>\r
+      <span class="align-middle">Return to Course List</span>\r
+    </a>\r
+  </div>\r
+</div>\r
+<ul ngbNav #coursePageNav="ngbNav" class="nav-tabs">\r
+\r
+  <!-- Edit Tab -->\r
+  <li [ngbNavItem]="'edit'">\r
+    <a ngbNavLink i18n>Edit department</a>\r
+    <ng-template ngbNavContent>\r
+      <div class="row">\r
+        <div class="col-lg-3 mt-3">\r
+        </div>\r
+        <div class="col-lg-6 mt-3">\r
+          <eg-fm-record-editor displayMode="inline"\r
+            mode="update"\r
+            hiddenFieldsList="id"\r
+            idlClass="acmd"\r
+            fieldOrder="name,owning_lib"\r
+            [preloadLinkedValues]="true"\r
+            [record]="currentDepartment">\r
+          </eg-fm-record-editor>\r
+        </div>\r
+      </div>\r
+    </ng-template>\r
+  </li>\r
+\r
+  <!-- Department Contacts Tab -->\r
+  <li [ngbNavItem]="'departmentContacts'">\r
+    <a ngbNavLink i18n>Department contacts</a>\r
+    <ng-template ngbNavContent>\r
+      <eg-department-contact-grid [departmentId]="departmentId"\r
+        [currentDepartment]="currentDepartment" displayMode="inline">\r
+      </eg-department-contact-grid>\r
+    </ng-template>\r
+  </li>\r
+</ul>\r
+<div [ngbNavOutlet]="coursePageNav" class="mb-3"></div>\r
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/course-reserves/department-page.component.ts
new file mode 100644 (file)
index 0000000..cb01167
--- /dev/null
@@ -0,0 +1,39 @@
+import {Component, ViewChild, OnInit} from '@angular/core';\r
+import {ActivatedRoute} from '@angular/router';\r
+import {PcrudService} from '@eg/core/pcrud.service';\r
+import {IdlObject} 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
+import {DepartmentContactGridComponent} from './department-contact-grid.component';\r
+\r
+@Component({\r
+    selector: 'eg-department-page',\r
+    templateUrl: './department-page.component.html'\r
+})\r
+\r
+export class DepartmentPageComponent implements OnInit {\r
+\r
+    currentDepartment: IdlObject;\r
+    departmentId: any;\r
+\r
+    // Supplemental Tabs - possibly add a tab for a grid displaying all courses associated with the department here\r
+    @ViewChild('departmentContactGrid', {static: true})\r
+        private departmentContactGrid: DepartmentContactGridComponent;\r
+\r
+    constructor(\r
+        private course: CourseService,\r
+        private pcrud: PcrudService,\r
+        private route: ActivatedRoute,\r
+        private toast: ToastService\r
+    ) {\r
+    }\r
+\r
+    ngOnInit() {\r
+        this.departmentId = +this.route.snapshot.paramMap.get('id');\r
+        this.course.getDepartments([this.departmentId]).then(department => {\r
+            this.currentDepartment = department[0];\r
+        });\r
+    }\r
+\r
+}\r
index fcd9cd5..eab1e64 100644 (file)
@@ -2,6 +2,7 @@ import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {CourseListComponent} from './course-list.component';
 import {CoursePageComponent} from './course-page.component';
+import {DepartmentPageComponent} from './department-page.component';
 
 const routes: Routes = [{
     path: ':id',
@@ -9,6 +10,9 @@ const routes: Routes = [{
 }, {
     path: '',
     component: CourseListComponent
+}, {
+    path: 'department/:id',
+    component: DepartmentPageComponent
 }];
 
 @NgModule({
index 080626b..0d18d76 100644 (file)
@@ -64,6 +64,33 @@ export class CourseService {
         }
     }
 
+    getDepartments(department_ids?: Number[]): Promise<IdlObject> {
+        const flesher = {flesh: 2, flesh_fields: {
+            'acmc': ['owning_lib'],
+            'aou': ['ou_type']}};
+        if (!department_ids) {
+            return this.pcrud.retrieveAll('acmd',
+                flesher, {atomic: true}).toPromise();
+        } else {
+            return this.pcrud.search('acmd', {id: department_ids},
+                flesher, {atomic: true}).toPromise();
+        }
+    }
+
+    getDepartmentContacts(department_ids?: Number[]): Observable<IdlObject> {
+        const flesher = {
+            flesh: 1,
+            flesh_fields: {'acmdc': ['usr', 'usr_role']}
+        };
+        if (!department_ids) {
+            return this.pcrud.retrieveAll('acmdc',
+                flesher);
+        } else {
+            return this.pcrud.search('acmdc', {department: department_ids},
+                flesher);
+        }
+    }
+
     getCoursesFromMaterial(copy_id): Promise<any> {
         const id_list = [];
         return new Promise((resolve, reject) => {
@@ -143,6 +170,14 @@ export class CourseService {
         return this.pcrud.create(new_user).toPromise();
     }
 
+    associateContacts(patron_id, args) {
+        const new_user = this.idl.create('acmdc');
+        if (args.role) { new_user.usr_role(args.role); }
+        new_user.department(args.currentDepartment.id());
+        new_user.usr(patron_id);
+        return this.pcrud.create(new_user).toPromise();
+    }
+
     disassociateMaterials(courses) {
         return new Promise((resolve, reject) => {
             const course_ids = [];
index e269238..ba14bb4 100644 (file)
@@ -1099,6 +1099,7 @@ CREATE TABLE asset.course_module_course (
     name            TEXT NOT NULL,
     course_number   TEXT NOT NULL,
     section_number  TEXT,
+    department      INT REFERENCES asset.course_module_department (id),
     owning_lib      INT REFERENCES actor.org_unit (id),
     is_archived        BOOLEAN DEFAULT false
 );
@@ -1145,5 +1146,24 @@ CREATE TABLE asset.course_module_term_course_map (
     course          INT     NOT NULL REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
 );
 
+CREATE TABLE asset.course_module_department (
+    id          SERIAL PRIMARY KEY,
+    name        TEXT NOT NULL,
+    owning_lib  INT REFERENCES actor.org_unit (id)
+);
+
+CREATE TABLE asset.course_module_department_contacts (
+    id              SERIAL PRIMARY KEY,
+    department          INT NOT NULL REFERENCES asset.course_module_department (id),
+    usr             INT NOT NULL REFERENCES actor.usr (id),
+    usr_role        INT REFERENCES asset.course_module_role (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
+CREATE TABLE asset.course_module_department_course_map (
+    id              BIGSERIAL  PRIMARY KEY,
+    department            INT     NOT NULL REFERENCES asset.course_module_department (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    course          INT     NOT NULL REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
 COMMIT;
 
index 58f4a18..e634195 100644 (file)
@@ -20962,7 +20962,8 @@ INSERT INTO actor.org_unit_setting (org_unit, name, value)
 INSERT INTO asset.course_module_role (id, name, is_public) VALUES
 (1, oils_i18n_gettext(1, 'Instructor', 'acmr', 'name'), true),
 (2, oils_i18n_gettext(2, 'Teaching assistant', 'acmr', 'name'), true),
-(3, oils_i18n_gettext(3, 'Student', 'acmr', 'name'), false);
+(3, oils_i18n_gettext(3, 'Student', 'acmr', 'name'), false),
+(4, oils_i18n_gettext(4, 'Department Head', 'acmr', 'name'), true);
 SELECT SETVAL('asset.course_module_role_id_seq'::TEXT, 100);
 
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.lp1922388-academic-departments.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.lp1922388-academic-departments.sql
new file mode 100644 (file)
index 0000000..81817ec
--- /dev/null
@@ -0,0 +1,30 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TABLE asset.course_module_department (
+    id          SERIAL PRIMARY KEY,
+    name        TEXT NOT NULL,
+    owning_lib  INT REFERENCES actor.org_unit (id)
+);
+
+CREATE TABLE asset.course_module_department_contacts (
+    id              SERIAL PRIMARY KEY,
+    department      INT NOT NULL REFERENCES asset.course_module_department (id),
+    usr             INT NOT NULL REFERENCES actor.usr (id),
+    usr_role        INT REFERENCES asset.course_module_role (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
+CREATE TABLE asset.course_module_department_course_map (
+    id              BIGSERIAL  PRIMARY KEY,
+    department      INT     NOT NULL REFERENCES asset.course_module_department (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    course          INT     NOT NULL REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
+ALTER TABLE asset.course_module_course
+    ADD COLUMN department   INT REFERENCES asset.course_module_department (id);
+
+INSERT INTO asset.course_module_role (id, name, is_public) VALUES
+(4, oils_i18n_gettext(4, 'Department Head', 'acmr', 'name'), true);
+
+COMMIT;
\ No newline at end of file