Browse Source

refs 113224: able to answer and skip questionnaire

Futa Arai 3 years ago
parent
commit
0d8825b8c3

+ 1 - 0
packages/app/.env.development

@@ -17,6 +17,7 @@ HACKMD_URI="http://localhost:3010"
 HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 OGP_URI="http://ogp:8088"
 GROWI_QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
+GROWI_QUESTIONNAIRE_SERVER_ORIGIN_CLIENT_SIDE="http://localhost:3003"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # S2SMSG_PUBSUB_SERVER_TYPE=nchan
 # PUBLISH_OPEN_API=true

+ 3 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -816,7 +816,9 @@
     "no_answer": "No answer",
     "settings": "Questionnaire settings",
     "failed_to_send": "Failed to send feedback",
-    "failed_to_get_user_info": "Feedback couldn't be sent because user info couldn't be achieved"
+    "failed_to_get_user_info": "Feedback couldn't be sent because user info couldn't be achieved",
+    "failed_to_update_answer_status": "Failed to update questionnaire answer status",
+    "skipped": "The questionnaire was skipped and won't be shown again"
   },
   "tag_edit_modal": {
     "edit_tags": "Edit Tags",

+ 3 - 1
packages/app/public/static/locales/ja_JP/translation.json

@@ -816,7 +816,9 @@
     "no_answer": "無回答",
     "settings": "アンケート設定",
     "failed_to_send": "回答送信に失敗しました",
-    "failed_to_get_user_info": "ユーザ情報の取得に失敗したため、回答を送信できませんでした"
+    "failed_to_get_user_info": "ユーザ情報の取得に失敗したため、回答を送信できませんでした",
+    "failed_to_update_answer_status": "アンケートの回答状態の更新に失敗しました",
+    "skipped": "アンケートはスキップされ、今後表示されません"
   },
   "tag_edit_modal": {
     "edit_tags": "タグの編集",

+ 47 - 21
packages/app/src/components/Questionnaire/QuestionnaireModal.tsx

@@ -3,12 +3,14 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
+import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { IAnswer } from '~/interfaces/questionnaire/answer';
 import { IGrowiInfo } from '~/interfaces/questionnaire/growi-info';
 import { IQuestionnaireAnswer } from '~/interfaces/questionnaire/questionnaire-answer';
 import { IQuestionnaireOrderHasId } from '~/interfaces/questionnaire/questionnaire-order';
 import { IUserInfo } from '~/interfaces/questionnaire/user-info';
+import { IUserHasId } from '~/interfaces/user';
 import { useCurrentUser, useGrowiVersion } from '~/stores/context';
 import { useQuestionnaireModal } from '~/stores/modal';
 import axios from '~/utils/axios';
@@ -26,6 +28,7 @@ type QuestionnaireModalProps = {
 const QuestionnaireModal = ({ questionnaireOrder, growiQuestionnaireServerOrigin }: QuestionnaireModalProps): JSX.Element => {
   const { data: currentUser } = useCurrentUser();
   const lang = currentUser?.lang;
+  const currentUserHasId = currentUser as IUserHasId;
 
   const { data: questionnaireModalData, close: closeQuestionnaireModal } = useQuestionnaireModal();
   const isOpened = questionnaireModalData?.openedQuestionnaireId === questionnaireOrder._id;
@@ -69,14 +72,36 @@ const QuestionnaireModal = ({ questionnaireOrder, growiQuestionnaireServerOrigin
     return null;
   };
 
+  const sendQuestionnaireAnswer = (questionnaireAnswer: IQuestionnaireAnswer) => {
+    axios.post(`${growiQuestionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswer)
+      .then(() => {
+        toastSuccess(
+          <>
+            <div className="font-weight-bold">{t('questionnaire.thank_you_for_answering')}</div>
+            <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
+          </>,
+          {
+            autoClose: 3000,
+            closeButton: true,
+          },
+        );
+        apiv3Put('/questionnaire/answer', {
+          user: currentUserHasId._id,
+          questionnaireOrderId: questionnaireOrder._id,
+        }).catch((e) => {
+          logger.error(e);
+          toastError(t('questionnaire.failed_to_update_answer_status'));
+        });
+      })
+      .catch((e) => {
+        logger.error(e);
+        toastError(t('questionnaire.failed_to_send'));
+      });
+  };
+
   const submitHandler = (event) => {
     event.preventDefault();
 
-    const toastOptions = {
-      autoClose: 3000,
-      closeButton: true,
-    };
-
     const growiInfo = getGrowiInfo();
     const userInfo = getUserInfo();
     const answers: IAnswer[] = questionnaireOrder.questions.map((question) => {
@@ -92,28 +117,29 @@ const QuestionnaireModal = ({ questionnaireOrder, growiQuestionnaireServerOrigin
         answeredAt: new Date(),
       };
 
-      axios.post('http://localhost:3003/questionnaire-answer', questionnaireAnswer)
-        .then(() => {
-          toastSuccess(
-            <>
-              <div className="font-weight-bold">{t('questionnaire.thank_you_for_answering')}</div>
-              <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
-            </>,
-            toastOptions,
-          );
-        })
-        .catch((e) => {
-          logger.error(e);
-          toastError(t('questionnaire.failed_to_send'), toastOptions);
-        });
+      sendQuestionnaireAnswer(questionnaireAnswer);
     }
     else {
-      toastError(t('questionnaire.failed_to_get_user_info'), toastOptions);
+      toastError(t('questionnaire.failed_to_get_user_info'));
     }
 
     closeQuestionnaireModal();
   };
 
+  const skipHandler = async() => {
+    apiv3Put('/questionnaire/skip', {
+      user: currentUserHasId._id,
+      questionnaireOrderId: questionnaireOrder._id,
+    }).then(() => {
+      toastSuccess(t('questionnaire.skipped'));
+    }).catch((e) => {
+      logger.error(e);
+      toastError(t('questionnaire.failed_to_update_answer_status'));
+    });
+
+    closeQuestionnaireModal();
+  };
+
   const questionnaireOrderTitle = lang === 'en_US' ? questionnaireOrder.title.en_US : questionnaireOrder.title.ja_JP;
 
   return (<Modal
@@ -148,7 +174,7 @@ const QuestionnaireModal = ({ questionnaireOrder, growiQuestionnaireServerOrigin
         {currentUser?.admin
         && <a href="" className="mr-auto d-flex align-items-center"><i className="material-icons mr-1">settings</i>{t('questionnaire.settings')}</a>}
         <>
-          <button type="button" className="btn btn-outline-secondary mr-3">{t('questionnaire.dont_show_again')}</button>
+          <button type="button" className="btn btn-outline-secondary mr-3" onClick={skipHandler}>{t('questionnaire.dont_show_again')}</button>
           <button type="submit" className="btn btn-primary">{t('questionnaire.answer')}</button>
         </>
       </ModalFooter>

+ 13 - 0
packages/app/src/interfaces/questionnaire/questionnaire-answer-status.ts

@@ -0,0 +1,13 @@
+import { Types } from 'mongoose';
+
+export const StatusType = {
+  not_answered: 'not_answered', answered: 'answered', skipped: 'skipped', denied: 'denied',
+} as const;
+
+export type StatusType = typeof StatusType[keyof typeof StatusType];
+
+export interface IQuestionnaireAnswerStatus {
+  user: Types.ObjectId | string // user that answered questionnaire
+  questionnaireOrderId: string
+  status: StatusType
+}

+ 2 - 1
packages/app/src/pages/[[...path]].page.tsx

@@ -621,7 +621,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
 
-  props.growiQuestionnaireServerOrigin = configManager.getConfig('crowi', 'app:growiQuestionnaireServerOrigin');
+  props.growiQuestionnaireServerOrigin = configManager.getConfig('crowi', 'app:growiQuestionnaireServerOriginClientSide')
+    || configManager.getConfig('crowi', 'app:growiQuestionnaireServerOrigin');
 }
 
 /**

+ 16 - 0
packages/app/src/server/models/questionnaire/questionnaire-answer-status.ts

@@ -0,0 +1,16 @@
+import { Model, Schema, Document } from 'mongoose';
+
+import { IQuestionnaireAnswerStatus, StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
+import { getOrCreateModel } from '~/server/util/mongoose-utils';
+
+export interface QuestionnaireAnswerStatusDocument extends IQuestionnaireAnswerStatus, Document {}
+
+export type QuestionnaireAnswerStatusModel = Model<QuestionnaireAnswerStatusDocument>
+
+const questionnaireOrderSchema = new Schema<QuestionnaireAnswerStatusDocument>({
+  user: { type: Schema.Types.ObjectId, required: true },
+  questionnaireOrderId: { type: String, required: true },
+  status: { type: String, enum: Object.values(StatusType), required: true },
+}, { timestamps: true });
+
+export default getOrCreateModel<QuestionnaireAnswerStatusDocument, QuestionnaireAnswerStatusModel>('QuestionnaireAnswerStatus', questionnaireOrderSchema);

+ 1 - 1
packages/app/src/server/routes/apiv3/index.js

@@ -108,7 +108,7 @@ module.exports = (crowi, app) => {
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
-  router.use('/questionnaire-orders', require('./questionnaire-orders')());
+  router.use('/questionnaire', require('./questionnaire')());
 
 
   return [router, routerForAdmin, routerForAuth];

+ 0 - 33
packages/app/src/server/routes/apiv3/questionnaire-orders.ts

@@ -1,33 +0,0 @@
-import express, { Request } from 'express';
-
-import QuestionnaireOrder from '~/server/models/questionnaire/questionnaire-order';
-import loggerFactory from '~/utils/logger';
-
-import { ApiV3Response } from './interfaces/apiv3-response';
-
-const logger = loggerFactory('growi:routes:apiv3:questionnaire');
-
-const router = express.Router();
-
-module.exports = () => {
-
-  router.get('/', async(req: Request, res: ApiV3Response) => {
-    const currentDate = new Date();
-    try {
-      const questionnaireOrders = await QuestionnaireOrder.find({
-        showUntil: {
-          $gte: currentDate,
-        },
-      });
-
-      return res.apiv3({ questionnaireOrders });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  return router;
-
-};

+ 74 - 0
packages/app/src/server/routes/apiv3/questionnaire.ts

@@ -0,0 +1,74 @@
+import express, { Request } from 'express';
+
+import { StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
+import QuestionnaireAnswerStatus from '~/server/models/questionnaire/questionnaire-answer-status';
+import QuestionnaireOrder from '~/server/models/questionnaire/questionnaire-order';
+import loggerFactory from '~/utils/logger';
+
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:questionnaire');
+
+const router = express.Router();
+
+const changeAnswerStatus = async(user, questionnaireOrderId, status: StatusType): Promise<number> => {
+  const result = await QuestionnaireAnswerStatus.updateOne({
+    user,
+    questionnaireOrderId,
+  }, {
+    status,
+  }, { upsert: true });
+
+  if (result.modifiedCount === 1) {
+    return 204;
+  }
+  if (result.upsertedCount === 1) {
+    return 201;
+  }
+  return 404;
+};
+
+module.exports = () => {
+
+  router.get('/questionnaire-orders', async(req: Request, res: ApiV3Response) => {
+    const currentDate = new Date();
+    try {
+      const questionnaireOrders = await QuestionnaireOrder.find({
+        showUntil: {
+          $gte: currentDate,
+        },
+      });
+
+      return res.apiv3({ questionnaireOrders });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  router.put('/answer', async(req: Request, res: ApiV3Response) => {
+    try {
+      const status = await changeAnswerStatus(req.body.user, req.body.questionnaireOrderId, StatusType.answered);
+      return res.apiv3({}, status);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  router.put('/skip', async(req: Request, res: ApiV3Response) => {
+    try {
+      const status = await changeAnswerStatus(req.body.user, req.body.questionnaireOrderId, StatusType.skipped);
+      return res.apiv3({}, status);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  return router;
+
+};

+ 6 - 0
packages/app/src/server/service/config-loader.ts

@@ -646,6 +646,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type: ValueType.STRING,
     default: null,
   },
+  GROWI_QUESTIONNAIRE_SERVER_ORIGIN_CLIENT_SIDE: {
+    ns: 'crowi',
+    key: 'app:growiQuestionnaireServerOriginClientSide',
+    type: ValueType.STRING,
+    default: null,
+  },
   QUESTIONNAIRE_CRON_SCHEDULE: {
     ns: 'crowi',
     key: 'app:questionnaireCronSchedule',

+ 1 - 1
packages/app/src/stores/questionnaire.tsx

@@ -5,7 +5,7 @@ import { IQuestionnaireOrderHasId } from '~/interfaces/questionnaire/questionnai
 
 export const useSWRxQuestionnaireOrders = (): SWRResponse<IQuestionnaireOrderHasId[], Error> => {
   return useSWR(
-    '/questionnaire-orders',
+    '/questionnaire/questionnaire-orders',
     endpoint => apiv3Get(endpoint).then((response) => {
       return response.data.questionnaireOrders;
     }),