lp1845240 port of Surveys UI from DOJO to Angular
authorMike Risher <mrisher@catalyte.io>
Tue, 24 Sep 2019 17:41:27 +0000 (17:41 +0000)
committerBill Erickson <berickxx@gmail.com>
Wed, 26 Feb 2020 19:17:37 +0000 (14:17 -0500)
idlClass asv holds the surveys, asvq holds their questions, and
asva holds the answers to those questions. The surveys are in
their own module and are lazy loaded

Signed-off-by: Mike Risher <mrisher@catalyte.io>

Modernize the survey create API by migrating it to cstore.
Additionally, make it possible to modify an existing survey top-level
object by setting 'ischanged' to the inbound survey.

Signed-off-by: Bill Erickson <berickxx@gmail.com>

LP1845240 Migrate survey create API to cstore

Modernize the survey create API by migrating it to cstore.
New API supports full range of isnew / ischanged / isdeleted actions on
the survey, questions, and answers.

Signed-off-by: Bill Erickson <berickxx@gmail.com>

LP1845240 Survey API returns updated fleshed survey

Signed-off-by: Bill Erickson <berickxx@gmail.com>

 Changes to be committed:
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts
modified:   Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Survey.pm
modified:   Open-ILS/tests/datasets/sql/surveys.sql

Signed-off-by: Bill Erickson <berickxx@gmail.com>

Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Survey.pm

index e051d37..223d181 100644 (file)
     <eg-link-table-link i18n-label label="Statistical Popularity Badges" 
       routerLink="/staff/admin/local/rating/badge"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Surveys" 
-      url="/eg/staff/admin/local/action/survey"></eg-link-table-link>
+      routerLink="/staff/admin/local/action/survey"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Transit List" 
       url="/eg/staff/circ/transits/list"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Work Log" 
       url="/eg/staff/admin/workstation/log"></eg-link-table-link>
-
   </eg-link-table>
 </div>
index 39c6be7..15a9153 100644 (file)
@@ -23,6 +23,9 @@ const routes: Routes = [{
     path: 'config/standing_penalty',
     component: StandingPenaltyComponent
 }, {
+    path: 'action/survey',
+    loadChildren: '@eg/staff/admin/local/survey/survey.module#SurveyModule'
+}, {
     path: ':schema/:table',
     component: BasicAdminPageComponent
 }];
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.html
new file mode 100644 (file)
index 0000000..86f2f20
--- /dev/null
@@ -0,0 +1,124 @@
+<eg-staff-banner bannerText="Survey ID # {{surveyId}}" i18n-bannerText
+                class="mb-3"></eg-staff-banner>
+<ngb-tabset #surveyTabs [activeId]="surveyTab" (tabChange)="onTabChange($event)" class="mb-3">
+    <ngb-tab title="Edit Survey" i18n-title id="edit">
+        <ng-template ngbTabContent>
+            <div class="col-lg-6 offset-lg-3 mt-3">
+                <div style="text-align: center;">
+                    <button class="p-2 mb-3 btn btn-danger btn-lg" 
+                    (click)="endSurvey()" i18n>
+                        End Survey Now
+                    </button>
+                </div>
+                <eg-fm-record-editor displayMode="inline" 
+                    hiddenFieldsList="id"
+                    datetimeFieldsList="start_date,end_date"
+                    idlClass="asv" 
+                    mode="update" 
+                    [record]="surveyObj">
+                </eg-fm-record-editor>
+            </div>
+        </ng-template>
+    </ngb-tab>
+    <ngb-tab title="Questions and Answers" i18n-title id="qanda">
+        <ng-template ngbTabContent>
+            <div class="col-lg-8 offset-lg-2 mt-3">
+                <eg-staff-banner bannerText="Questions & Answers" i18n-bannerText>
+                    </eg-staff-banner>
+                <div *ngFor="let question of localArray; let questionIndex = index;">
+                    <div class="mb-3 mt-3 p-2 bg-light input-group">
+                        <label class="input-group-text">
+                            <b>Question</b>
+                        </label>
+                        <input type="text" [(ngModel)]="question.words" class="form-control"
+                            name="question-{{questionIndex}}">
+                        <span class="input-group-append">
+                            <button class="ml-2 btn btn-info" 
+                                (click)="updateQuestion(question)" i18n>
+                                Save
+                            </button>
+                            <button class="ml-1 btn btn-danger"
+                                (click)="deleteQuestion(question)" i18n>
+                                Delete Question & Answers
+                            </button>
+                        </span>
+                    </div>
+                    <div *ngFor="let answer of question.answers; let answerIndex = index;" 
+                        class="mb-2 input-group">
+                        <input class="form-control" type="text" 
+                            [(ngModel)]="answer.words"
+                            name="answer-{{questionIndex}}-{{answerIndex}}">
+                        <span class="input-group-append">
+                            <button class="ml-2 btn btn-info" 
+                                (click)="updateAnswer(answer, question, questionIndex, answerIndex)"
+                                i18n>
+                                Save
+                            </button>
+                            <button class="ml-1 btn btn-danger" (click)="deleteAnswer(answer)"
+                                i18n>
+                                Delete
+                            </button>
+                        </span>
+                    </div>
+                    <div class="mb-2 input-group">
+                        <input class="form-control" type="text" 
+                            [(ngModel)]="newAnswerArray[questionIndex].inputText"
+                                value="">
+                        <span class="input-group-append">
+                            <button class="ml-2 btn btn-info" 
+                                (click)="createAnswer(newAnswerArray[questionIndex].inputText, question)"
+                                i18n>
+                                Add Answer
+                            </button>
+                        </span>
+                    </div>
+                </div>
+                <div class="mb-3 mt-3 p-2 bg-light input-group">
+                    <label class="input-group-text">
+                        <b>New Question</b>
+                    </label>
+                    <input #newQuestionInput 
+                        class="form-control" 
+                        type="text" 
+                        [(ngModel)]="newQuestionText"
+                        name="question-new" value="">
+                    <span class="input-group-append">
+                        <button class="ml-2 btn btn-info"
+                            (click)="createQuestion(newQuestionText)" i18n>
+                            Save Question & Add Answer
+                        </button>
+                    </span>
+                </div>
+            </div>
+        </ng-template>
+    </ngb-tab>
+</ngb-tabset>
+
+<eg-string #createAnswerString i18n-text text="New Answer Added"></eg-string>
+<eg-string #createAnswerErrString i18n-text text="Failed to Create New Answer">
+    </eg-string>
+<eg-string #createQuestionString i18n-text text="New Question Added"></eg-string>
+<eg-string #createQuestionErrString i18n-text text="Failed to Create New Question">
+    </eg-string>
+<eg-string #delAnswerSuccessStr i18n-text text="Survey Answer deleted">
+    </eg-string>
+<eg-string #delAnswerFailStr i18n-text text="Survey Answer deletion failed">
+    </eg-string>
+<eg-string #delQuestionSuccessStr i18n-text text="Survey Question deleted">
+    </eg-string>
+<eg-string #delQuestionFailStr i18n-text text="Survey Question deletion failed">
+    </eg-string>
+<eg-string #updateAnswerSuccessStr i18n-text text="Survey Answer updated">
+    </eg-string>
+<eg-string #updateAnswerFailStr i18n-text text="Survey Answer update failed">
+    </eg-string>
+<eg-string #updateQuestionSuccessStr i18n-text text="Survey Question updated">
+    </eg-string>
+<eg-string #updateQuestionFailStr i18n-text text="Survey Question update failed">
+    </eg-string>
+<eg-string #endSurveyFailedString i18n-text 
+    text="Ending Survey failed or was not allowed"></eg-string>
+<eg-string #endSurveySuccessString i18n-text text="Survey ended"></eg-string>
+<eg-string #questionAlreadyStartedErrString i18n-text 
+    text="The survey Start Date must be set for the future to add new questions or modify existing questions.">
+    </eg-string>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-edit.component.ts
new file mode 100644 (file)
index 0000000..73b3a7b
--- /dev/null
@@ -0,0 +1,307 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject, IdlService } from '@eg/core/idl.service';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+    templateUrl: './survey-edit.component.html'
+})
+
+export class SurveyEditComponent implements OnInit {
+    surveyId: number;
+    surveyObj: IdlObject;
+    localArray: any;
+    newAnswerArray: object[];
+    newQuestionText: string;
+    surveyTab: string;
+
+    @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
+
+    @ViewChild('createAnswerString', { static: true })
+        createAnswerString: StringComponent;
+    @ViewChild('createAnswerErrString', { static: true })
+        createAnswerErrString: StringComponent;
+    @ViewChild('createQuestionString', { static: true })
+        createQuestionString: StringComponent;
+    @ViewChild('createQuestionErrString', { static: true })
+        createQuestionErrString: StringComponent;
+
+    @ViewChild('updateQuestionSuccessStr', { static: true })
+        updateQuestionSuccessStr: StringComponent;
+    @ViewChild('updateQuestionFailStr', { static: true })
+        updateQuestionFailStr: StringComponent;
+    @ViewChild('updateAnswerSuccessStr', { static: true })
+        updateAnswerSuccessStr: StringComponent;
+    @ViewChild('updateAnswerFailStr', { static: true })
+        updateAnswerFailStr: StringComponent;
+
+    @ViewChild('delAnswerSuccessStr', { static: true })
+        delAnswerSuccessStr: StringComponent;
+    @ViewChild('delAnswerFailStr', { static: true })
+        delAnswerFailStr: StringComponent;
+    @ViewChild('delQuestionSuccessStr', { static: true })
+        delQuestionSuccessStr: StringComponent;
+    @ViewChild('delQuestionFailStr', { static: true })
+        delQuestionFailStr: StringComponent;
+
+    @ViewChild('endSurveyFailedString', { static: true })
+        endSurveyFailedString: StringComponent;
+    @ViewChild('endSurveySuccessString', { static: true })
+        endSurveySuccessString: StringComponent;
+    @ViewChild('questionAlreadyStartedErrString', { static: true })
+        questionAlreadyStartedErrString: StringComponent;
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+        private route: ActivatedRoute,
+        private toast: ToastService,
+        private idl: IdlService,
+    ) {
+    }
+
+    ngOnInit() {
+        this.surveyId = parseInt(this.route.snapshot.paramMap.get('id'), 10);
+        this.updateData();
+    }
+
+    updateData() {
+        this.newQuestionText = '';
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.fleshed.retrieve',
+            this.surveyId
+        ).subscribe(res => {
+            this.surveyObj = res;
+            this.buildLocalArray(res);
+            return res;
+        });
+    }
+
+    onTabChange(event: NgbTabChangeEvent) {
+        this.surveyTab = event.nextId;
+    }
+
+    buildLocalArray(res) {
+        this.localArray = [];
+        this.newAnswerArray = [];
+        const allQuestions = res.questions();
+        allQuestions.forEach((question, index) => {
+            this.newAnswerArray.push({inputText: ''});
+            question.words = question.question();
+            question.answers = question.answers();
+            this.localArray.push(question);
+            question.answers.forEach(answer => {
+                answer.words = answer.answer();
+            });
+            this.sortAnswers(index);
+        });
+        this.sortQuestions();
+    }
+
+    sortQuestions() {
+        this.localArray.sort(function(a, b) {
+            const q1 = a.question().toUpperCase();
+            const q2 = b.question().toUpperCase();
+            return (q1 < q2) ? -1 : (q1 > q2) ? 1 : 0;
+        });
+    }
+
+    sortAnswers(questionIndex) {
+        this.localArray[questionIndex].answers.sort(function(a, b) {
+            const a1 = a.answer().toUpperCase();
+            const a2 = b.answer().toUpperCase();
+            return (a1 < a2) ? -1 : (a1 > a2) ? 1 : 0;
+        });
+    }
+
+    updateQuestion(questionToChange) {
+        if (this.surveyHasBegun()) {
+            return;
+        }
+        questionToChange.question(questionToChange.words);
+        questionToChange.ischanged(true);
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.update',
+            this.auth.token(), this.surveyObj
+        ).subscribe(res => {
+            if (res.debug) {
+                this.updateQuestionFailStr.current().then(msg => this.toast.warning(msg));
+                return res;
+            } else {
+                this.surveyObj = res;
+                this.buildLocalArray(this.surveyObj);
+                this.updateQuestionSuccessStr.current().then(msg => this.toast.success(msg));
+                return res;
+            }
+        });
+    }
+
+    deleteQuestion(questionToDelete) {
+        if (this.surveyHasBegun()) {
+            return;
+        }
+        questionToDelete.isdeleted(true);
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.update',
+            this.auth.token(), this.surveyObj
+        ).subscribe(res => {
+            if (res.debug) {
+                this.delQuestionFailStr.current().then(msg => this.toast.warning(msg));
+                return res;
+            } else {
+                this.surveyObj = res;
+                this.buildLocalArray(this.surveyObj);
+                this.delQuestionSuccessStr.current().then(msg => this.toast.success(msg));
+                return res;
+            }
+
+        });
+    }
+
+    createQuestion(newQuestionText) {
+        if (this.surveyHasBegun()) {
+            return;
+        }
+        const newQuestion = this.idl.create('asvq');
+        newQuestion.question(newQuestionText);
+        newQuestion.isnew(true);
+        let questionObjects = [];
+        questionObjects = this.surveyObj.questions();
+        questionObjects.push(newQuestion);
+        this.surveyObj.questions(questionObjects);
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.update',
+            this.auth.token(), this.surveyObj
+        ).subscribe(res => {
+            if (res.debug) {
+                this.newQuestionText = '';
+                this.createQuestionErrString.current().then(msg => this.toast.warning(msg));
+                return res;
+            } else {
+                this.surveyObj = res;
+                this.buildLocalArray(this.surveyObj);
+                this.newQuestionText = '';
+                this.createQuestionString.current().then(msg => this.toast.success(msg));
+                return res;
+            }
+
+        });
+    }
+
+    deleteAnswer(answerObj) {
+        if (this.surveyHasBegun()) {
+            return;
+        }
+        answerObj.isdeleted(true);
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.update',
+            this.auth.token(), this.surveyObj
+        ).subscribe(res => {
+            if (res.debug) {
+                this.delAnswerFailStr.current().then(msg => this.toast.warning(msg));
+                return res;
+            } else {
+                this.surveyObj = res;
+                this.buildLocalArray(this.surveyObj);
+                this.delAnswerSuccessStr.current().then(msg => this.toast.success(msg));
+                return res;
+            }
+        });
+    }
+
+    updateAnswer(answerObj) {
+        if (this.surveyHasBegun()) {
+            return;
+        }
+        answerObj.answer(answerObj.words);
+        answerObj.ischanged(true);
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.update',
+            this.auth.token(), this.surveyObj
+        ).subscribe(res => {
+            if (res.debug) {
+                this.updateAnswerFailStr.current().then(msg => this.toast.warning(msg));
+                return res;
+            } else {
+                this.surveyObj = res;
+                this.buildLocalArray(this.surveyObj);
+                this.updateAnswerSuccessStr.current().then(msg => this.toast.success(msg));
+                return res;
+            }
+        });
+    }
+
+    createAnswer(newAnswerText, questionObj) {
+        // Create answer *is* allowed if survey has already begun
+        const questionId = questionObj.id();
+        const newAnswer = this.idl.create('asva');
+        newAnswer.answer(newAnswerText);
+        newAnswer.question(questionId);
+        newAnswer.isnew(true);
+        questionObj.answers.push(newAnswer);
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.update',
+            this.auth.token(), this.surveyObj
+        ).subscribe(res => {
+            if (res.debug) {
+                this.createAnswerErrString.current().then(msg => this.toast.warning(msg));
+                return res;
+            } else {
+                this.surveyObj = res;
+                this.buildLocalArray(this.surveyObj);
+                this.createAnswerString.current().then(msg => this.toast.success(msg));
+                return res;
+            }
+        });
+    }
+
+    endSurvey() {
+        const today = new Date().toISOString();
+        this.surveyObj.end_date(today);
+        this.surveyObj.ischanged(true);
+        // to get fm-editor to display changed date we need to set
+        // this.surveyObj to null temporarily
+        const surveyClone = this.idl.clone(this.surveyObj);
+        this.surveyObj = null;
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.survey.update',
+            this.auth.token(), surveyClone
+        ).subscribe(res => {
+            if (res.debug) {
+                this.endSurveyFailedString.current().then(msg => this.toast.warning(msg));
+                return res;
+            } else {
+                this.surveyObj = res;
+                this.surveyObj.ischanged(false);
+                this.buildLocalArray(this.surveyObj);
+                this.endSurveySuccessString.current().then(msg => this.toast.success(msg));
+                return res;
+            }
+        });
+    }
+
+    surveyHasBegun() {
+        const surveyStartDate = new Date(this.surveyObj.start_date());
+        const now = new Date();
+        if (surveyStartDate <= now) {
+            this.questionAlreadyStartedErrString.current().then(msg =>
+                this.toast.warning(msg));
+            return true;
+        }
+        return false;
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey-routing.module.ts
new file mode 100644 (file)
index 0000000..cd36869
--- /dev/null
@@ -0,0 +1,22 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {SurveyComponent} from './survey.component';
+import {SurveyEditComponent} from './survey-edit.component';
+
+const routes: Routes = [{
+    path: '',
+    component: SurveyComponent
+}, {
+    path: ':id',
+    component: SurveyEditComponent
+}];
+
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class SurveyRoutingModule {}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.html
new file mode 100644 (file)
index 0000000..394d837
--- /dev/null
@@ -0,0 +1,37 @@
+<eg-staff-banner bannerText="Survey Configuration" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-grid #grid idlClass="asv" [dataSource]="gridDataSource" 
+[sortable]="true">
+    <eg-grid-toolbar-button label="New Survey" i18n-label [action]="createNew">
+    </eg-grid-toolbar-button>
+    <eg-grid-toolbar-action label="Edit Selected" i18n-label [action]="editSelected">
+    </eg-grid-toolbar-action>
+    <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+    (onClick)="deleteSelected($event)"></eg-grid-toolbar-action>
+    <eg-grid-toolbar-action label="End Survey Now" i18n-label 
+    (onClick)="endSurvey($event)"></eg-grid-toolbar-action>
+</eg-grid>
+
+<eg-fm-record-editor 
+    datetimeFieldsList="start_date,end_date"
+    hiddenFieldsList="id"
+    #editDialog 
+    idlClass="asv">
+</eg-fm-record-editor>
+
+<eg-string #createString i18n-text text="New Survey Added"></eg-string>
+<eg-string #createErrString i18n-text text="Failed to Create New Survey">
+    </eg-string>
+<eg-string #endSurveyFailedString i18n-text 
+    text="Ending Survey failed or was not allowed"></eg-string>
+<eg-string #endSurveySuccessString i18n-text text="Survey ended">
+    </eg-string>
+<eg-string #deleteFailedString i18n-text 
+    text="Delete of Survey failed or was not allowed"></eg-string>
+<eg-string #deleteSuccessString i18n-text text="Delete of Survey succeeded">
+    </eg-string>
+<eg-string #successString i18n-text text="Update of Survey succeeded">
+    </eg-string>
+<eg-string #updateFailedString i18n-text text="Update of Survey succeeded">
+    </eg-string>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.component.ts
new file mode 100644 (file)
index 0000000..9865a4b
--- /dev/null
@@ -0,0 +1,122 @@
+import {Pager} from '@eg/share/util/pager';
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Router} from '@angular/router';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+
+@Component({
+    templateUrl: './survey.component.html'
+})
+
+export class SurveyComponent implements OnInit {
+
+    gridDataSource: GridDataSource;
+
+    @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
+    @ViewChild('grid', { static: true }) grid: GridComponent;
+    @ViewChild('successString', { static: true }) successString: StringComponent;
+    @ViewChild('createString', { static: true }) createString: StringComponent;
+    @ViewChild('createErrString', { static: true }) createErrString: StringComponent;
+    @ViewChild('updateFailedString', { static: true }) updateFailedString: StringComponent;
+    @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;
+    @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
+    @ViewChild('endSurveyFailedString', { static: true }) endSurveyFailedString: StringComponent;
+    @ViewChild('endSurveySuccessString', { static: true }) endSurveySuccessString: StringComponent;
+
+    @Input() dialogSize: 'sm' | 'lg' = 'lg';
+
+    constructor(
+        private auth: AuthService,
+        private net: NetService,
+        private pcrud: PcrudService,
+        private toast: ToastService,
+        private router: Router
+    ) {
+        this.gridDataSource = new GridDataSource();
+    }
+
+    ngOnInit() {
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return this.pcrud.retrieveAll('asv', {});
+        };
+
+        this.grid.onRowActivate.subscribe(
+            (idlThing: IdlObject) => {
+                const idToEdit = idlThing.id();
+                this.navigateToEditPage(idToEdit);
+            }
+        );
+    }
+
+    showEditDialog(idlThing: IdlObject): Promise<any> {
+        return;
+    }
+
+    editSelected = (surveys: IdlObject[]) => {
+        const idToEdit = surveys[0].id();
+        this.navigateToEditPage(idToEdit);
+    }
+
+    endSurvey = (surveys: IdlObject[]) => {
+        const today = new Date().toISOString();
+        for (let i = 0; i < surveys.length; i++) {
+            surveys[i].end_date(today);
+            this.pcrud.update(surveys[i]).toPromise().then(
+                async (ok) => {
+                    this.toast.success(await this.endSurveySuccessString.current());
+                },
+                async (err) => {
+                    this.toast.warning(await this.endSurveyFailedString.current());
+                }
+            );
+        }
+    }
+
+    deleteSelected = (surveys: IdlObject[]) => {
+        for (let i = 0; i < surveys.length; i++) {
+            const idToDelete = surveys[i].id();
+            this.net.request(
+                'open-ils.circ',
+                'open-ils.circ.survey.delete.cascade.override',
+                this.auth.token(), idToDelete
+            ).subscribe(res => {
+                this.deleteSuccessString.current()
+                    .then(str => this.toast.success(str));
+                this.grid.reload();
+                return res;
+            }, (err) => {
+                this.deleteFailedString.current()
+                    .then(str => this.toast.success(str));
+            });
+        }
+    }
+
+    navigateToEditPage(id: any) {
+        this.router.navigate(['/staff/admin/local/action/survey/' + id]);
+    }
+
+    createNew = () => {
+        this.editDialog.mode = 'create';
+        this.editDialog.datetimeFields = 'start_date,end_date';
+        this.editDialog.open({size: this.dialogSize}).subscribe(
+            ok => {
+                this.createString.current()
+                    .then(str => this.toast.success(str));
+                this.grid.reload();
+            },
+            rejection => {
+                if (!rejection.dismissed) {
+                    this.createErrString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            }
+        );
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/survey/survey.module.ts
new file mode 100644 (file)
index 0000000..21ba53f
--- /dev/null
@@ -0,0 +1,25 @@
+import {NgModule} from '@angular/core';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {SurveyComponent} from './survey.component';
+import {FormsModule} from '@angular/forms';
+import {SurveyEditComponent} from './survey-edit.component';
+import {SurveyRoutingModule} from './survey-routing.module';
+
+@NgModule({
+  declarations: [
+    SurveyComponent,
+    SurveyEditComponent
+  ],
+  imports: [
+    AdminCommonModule,
+    SurveyRoutingModule,
+    FormsModule,
+  ],
+  exports: [
+  ],
+  providers: [
+  ]
+})
+
+export class SurveyModule {
+}
index 3b83e36..2f684c4 100644 (file)
@@ -22,9 +22,155 @@ use Data::Dumper;
 use OpenILS::Event;
 use Time::HiRes qw(time);
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenSRF::Utils::Logger qw/$logger/;
 
 my $apputils = "OpenILS::Application::AppUtils";
 
+__PACKAGE__->register_method(
+    method => "update_survey",
+    api_name => "open-ils.circ.survey.update",
+    signature => {
+        desc => q/Create, update, delete surveys, survey questions, and
+            survey answers.  Relies on isnew ; isnchanged ; isdeleted
+            attributes of provided objects to determine outcome.
+        /,
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'Fleshed survey (asv) object', type => 'object'}
+        ],
+        return => '1 on success, event on error'
+    }
+);
+
+
+sub update_survey {
+    my ($self, $client, $auth, $survey) = @_;
+
+    my $e = new_editor(authtoken => $auth, xact => 1);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed('ADMIN_SURVEY', $survey->owner);
+
+    my $questions = $survey->questions || [];
+
+    if ($survey->isdeleted) {
+
+        $questions = $e->search_action_survey_question({survey => $survey->id});
+        $_->isdeleted(1) for @$questions;
+        $survey->questions($questions);
+
+        # Remove dependent data first.
+        return $e->die_event if update_questions($e, $survey);
+        return $e->die_event unless $e->delete_action_survey($survey);
+
+    } else {
+
+        if ($survey->isnew) {
+
+            $survey->clear_id;
+            return $e->die_event unless $e->create_action_survey($survey);
+
+            $_->isnew(1) for @$questions;
+
+        } elsif ($survey->ischanged) {
+
+            return $e->die_event unless $e->update_action_survey($survey);
+        }
+
+        return $e->die_event if update_questions($e, $survey);
+    }
+
+    $e->commit;
+
+    $e->xact_begin;
+    $survey = $e->retrieve_action_survey([
+        $survey->id, 
+        {   flesh => 2, 
+            flesh_fields => {asv => ['questions'], 'asvq' => ['answers']}
+        }
+    ]);
+    $e->rollback;
+
+    return $survey;
+}
+
+# returns undef on success, event on error
+sub update_questions {
+    my ($e, $survey) = @_;
+
+    for my $question (@{$survey->questions}) {
+
+        if ($question->isdeleted) {
+
+            # Get the full set
+            my $answers = 
+                $e->search_action_survey_answer({question => $question->id});
+            $_->isdeleted(1) for @$answers;
+            $question->answers($answers);
+
+            # Delete linked objects first.
+            return 1 if update_answers($e, $question);
+            return $e->die_event 
+                unless $e->delete_action_survey_question($question);
+
+        } else {
+
+            if ($question->ischanged) {
+
+                return $e->die_event 
+                    unless $e->update_action_survey_question($question);
+
+            } elsif ($question->isnew) {
+
+                $question->survey($survey->id);
+                $question->clear_id;
+
+                return $e->die_event unless $e->create_action_survey_question($question);
+            }
+
+            return 1 if update_answers($e, $question);
+        }
+    }
+
+    return undef;
+}
+
+sub update_answers {
+    my ($e, $question) = @_;
+
+    return undef unless $question->answers;
+
+    for my $answer (@{$question->answers}) {
+
+        if ($answer->isdeleted) {
+            my $responses = 
+                $e->search_action_survey_response({answer => $answer->id});
+
+            for my $response (@$responses) {
+                return $e->die_event unless 
+                    $e->delete_action_survey_response($response);
+            }
+
+            return $e->die_event unless $e->delete_action_survey_answer($answer);
+
+        } elsif ($answer->isnew) {
+
+            $answer->clear_id;
+            $answer->question($question->id);
+
+            return $e->die_event 
+                unless $e->create_action_survey_answer($answer);
+
+        } elsif ($answer->ischanged) {
+            return $e->die_event
+                unless $e->update_action_survey_answer($answer);
+        }
+    }
+
+    return undef;
+}
+
+
+
 # - creates a new survey
 # expects a survey complete with questions and answers
 __PACKAGE__->register_method(
@@ -78,10 +224,6 @@ sub _add_survey {
     return $survey;
 }
 
-sub _update_survey {
-    my($session, $survey) = @_;
-}
-
 sub _add_questions {
     my($session, $survey) = @_;
 
@@ -423,4 +565,4 @@ sub delete_survey {
 
 
 
-1;
+1;
\ No newline at end of file