فهرست منبع

feat: event test

ali 2 سال پیش
والد
کامیت
f0a8da6328

+ 2 - 2
package.json

@@ -48,8 +48,8 @@
     "process": "^0.11.10",
     "quasar": "^2.14.2",
     "quasar-app-extension-q-page-builder": "^0.4.4",
-    "quasar-crud": "^1.0.5",
-    "quasar-form-builder": "^1.0.2",
+    "quasar-crud": "^1.0.7",
+    "quasar-form-builder": "^1.0.4",
     "quasar-template-builder": "^1.0.23",
     "quasar-ui-q-page-builder": "^0.4.4",
     "socket.io-client": "^4.6.1",

+ 2 - 0
src/api/APIGateway.js

@@ -27,6 +27,7 @@ import MediaHashtagAPI from 'src/api/models/MediaHashtag.js'
 import RegistrationAPI from 'src/api/models/Registration.js'
 import UnitCategoryAPI from 'src/api/models/UnitCategory.js'
 import MediaCategoryAPI from 'src/api/models/MediaCategory.js'
+import QuestionChoiceApi from 'src/api/models/QuestionChoice.js'
 import TicketCategoryAPI from 'src/api/models/TicketCategory.js'
 import SessionTemplateAPI from 'src/api/models/SessionTemplate.js'
 import ContentCategoryAPI from 'src/api/models/ContentCategory.js'
@@ -71,6 +72,7 @@ export const APIGateway = {
   mediaHashtag: new MediaHashtagAPI(),
   mediaCategory: new MediaCategoryAPI(),
   ticketCategory: new TicketCategoryAPI(),
+  questionChoice: new QuestionChoiceApi(),
   sessionTemplate: new SessionTemplateAPI(),
   contentCategory: new ContentCategoryAPI(),
   instalmentOffer: new InstalmentOfferAPI(),

+ 57 - 0
src/api/models/QuestionChoice.js

@@ -0,0 +1,57 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { QuestionChoice, QuestionChoiceList } from 'src/models/QuestionChoice.js'
+
+export default class QuestionChoiceAPI extends APIRepository {
+  constructor() {
+    super('question_choices', appApi, '/lma/question_choices', QuestionChoice)
+    this.APIAdresses = {
+      base: '/lma/question_choices',
+      byId: (id) => '/lma/question_choices/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new QuestionChoice(response.data) },
+      post: (response) => { return new QuestionChoice(response.data) },
+      put: (response) => { return new QuestionChoice(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new QuestionChoiceList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 1 - 0
src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue

@@ -213,5 +213,6 @@ export default {
 
 <style lang="scss" scoped>
 .ContentMedias {
+  width: 100%;
 }
 </style>

+ 94 - 0
src/components/FormBuilderCustumComponents/QuestionChoices/ChoicesItem.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="ChoicesItem">
+    <div class="title">
+      <q-input v-model="choiceText"
+               class="full-width"
+               type="textarea"
+               @update:model-value="onChangeChoiceText" />
+    </div>
+    <div class="actions">
+      <q-btn icon="edit"
+             outline
+             class="viewBtn"
+             color="primary"
+             @click="onEdit" />
+      <q-btn v-if="canDelete"
+             class="deleteBtn"
+             icon="delete"
+             outline
+             color="red"
+             @click="onDelete" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { QuestionChoice } from 'src/models/QuestionChoice.js'
+
+export default {
+  name: 'ChoicesItem',
+  props: {
+    choice: {
+      type: QuestionChoice,
+      default: new QuestionChoice()
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['update:choice', 'onEdit', 'onDelete'],
+  data () {
+    return {
+      choiceText: null,
+      entityCreateLoading: false
+    }
+  },
+  mounted() {
+    this.choiceText = this.choice.text.replace(/<br>/g, '\n')
+  },
+  methods: {
+    onChangeChoiceText () {
+      const oldChoice = this.choice
+      oldChoice.text = this.choiceText.replace(/\n/g, '<br>')
+      this.$emit('update:choice', oldChoice)
+    },
+    onEdit () {
+      this.$emit('onEdit')
+    },
+    onDelete () {
+      this.$emit('onDelete')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.ChoicesItem {
+  background: #F9F9F9;
+  border: 1px solid #DFE1EC;
+  border-radius: 10px;
+  padding: 10px 16px 10px 21px;
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  align-items: center;
+  .title {
+    width: calc( 100% - 120px );
+  }
+  .actions {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    .q-btn {
+      width: 42px;
+      height: 42px;
+      &.viewBtn {
+      }
+      &.deleteBtn {
+        margin-left: 16px;
+      }
+    }
+  }
+}
+</style>

+ 75 - 0
src/components/FormBuilderCustumComponents/QuestionChoices/MediaCategory.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="MediaCategory">
+    <div class="title">
+      <q-icon :name="icon"
+              size="19px" />
+      {{ title }}
+    </div>
+    <div class="media-list row q-col-gutter-md">
+      <div v-for="media in medias.list"
+           :key="media.id"
+           class="media-item col-md-3 col-sm-6 col-12">
+        <media-item :media="media"
+                    :can-delete="canDelete"
+                    @onView="onView(media)"
+                    @onDelete="onDelete(media)" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { MediaList } from 'src/models/Media.js'
+import MediaItem from 'src/components/FormBuilderCustumComponents/ContentMedias/MediaItem.vue'
+
+export default {
+  name: 'MediaCategory',
+  components: { MediaItem },
+  props: {
+    medias: {
+      type: MediaList,
+      default: new MediaList()
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    icon: {
+      type: String,
+      default: ''
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['update:value', 'onView', 'onDelete'],
+  data () {
+    return {
+      entityCreateLoading: false
+    }
+  },
+  methods: {
+    onView (media) {
+      this.$emit('onView', media)
+    },
+    onDelete (media) {
+      this.$emit('onDelete', media)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.MediaCategory {
+  .title {
+    font-size: 16px;
+    letter-spacing: 0;
+    color: #212121;
+    margin-bottom: 26px;
+    .q-icon {
+      margin-left: 9px;
+    }
+  }
+}
+</style>

+ 192 - 0
src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue

@@ -0,0 +1,192 @@
+<template>
+  <div class="QuestionChoices">
+    <q-linear-progress v-if="fetchLoading"
+                       indeterminate />
+    <div v-if="canAddChoice && !readonly"
+         class="q-mb-md">
+      <entity-create ref="entityCreate"
+                     v-model:value="inputs"
+                     title="تعریف گزینه"
+                     :api="api"
+                     :entity-id-key="entityIdKey"
+                     :entity-param-key="entityParamKey"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-reload-button="false" />
+      <q-btn color="primary"
+             class="full-width q-mt-md"
+             :loading="entityCreateLoading"
+             @click="entityCreate">
+        افزودن گزینه
+      </q-btn>
+    </div>
+    <div v-for="(choice, choiceIndex) in choices.list"
+         :key="choice.id"
+         class="choice-item">
+      <div class="choice-item-radio">
+        <q-radio v-model="localCorrectChoiceIndex"
+                 :val="choiceIndex"
+                 @update:model-value="onChangeCorrectChoiceIndex" />
+      </div>
+      <div class="choice-item-component">
+        <choices-item :choice="choice"
+                      :can-delete="canDelete"
+                      @onEdit="onEdit(choice)"
+                      @onDelete="onDelete(choice)" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { EntityCreate } from 'quasar-crud'
+import ChoicesItem from './ChoicesItem.vue'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { QuestionChoice, QuestionChoiceList } from 'src/models/QuestionChoice.js'
+import { FormBuilderAssist } from 'quasar-form-builder'
+
+export default {
+  name: 'QuestionChoices',
+  components: { ChoicesItem, EntityCreate },
+  props: {
+    value: {
+      default: () => [],
+      type: [Array, String, Number, Boolean]
+    },
+    correctChoiceIndex: {
+      default: 0,
+      type: Number
+    },
+    disable: {
+      default: false,
+      type: Boolean
+    },
+    canDelete: {
+      type: Boolean,
+      default: true
+    },
+    canAddChoice: {
+      type: Boolean,
+      default: true
+    },
+    sourceType: {
+      type: String,
+      default: 'CONTENT'
+    },
+    readonly: {
+      default: false,
+      type: Boolean
+    },
+    showPageLink: {
+      default: true,
+      type: Boolean
+    },
+    showDownloadLink: {
+      default: false,
+      type: Boolean
+    }
+  },
+  emits: ['update:value'],
+  data () {
+    return {
+      fetchLoading: false,
+      localCorrectChoiceIndex: 0,
+      choices: new QuestionChoiceList(),
+      api: APIGateway.questionChoice.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      inputs: [
+        { type: 'input', name: 'text', responseKey: 'text', label: 'متن', inputType: 'textarea', placeholder: ' ', col: 'col-12' }
+      ],
+      entityCreateLoading: false
+    }
+  },
+  watch: {
+    value: {
+      handler () {
+        this.inputData = this.value
+        if (this.value.length > 0 && this.value[0].id) {
+          this.choices = new QuestionChoiceList(this.value)
+          this.emitValue(this.choices.list.map(item => item.id))
+        }
+      },
+      immediate: true
+    },
+    correctChoiceIndex () {
+      this.localCorrectChoiceIndex = this.correctChoiceIndex
+    }
+  },
+  created () {
+    FormBuilderAssist.setAttributeByName(this.inputs, 'source_type', 'value', this.sourceType)
+    this.inputData = this.value
+    this.localCorrectChoiceIndex = this.correctChoiceIndex
+  },
+  methods: {
+    emitValue (value) {
+      this.$emit('update:value', value)
+    },
+    onChangeCorrectChoiceIndex () {
+      // this.$emit('update:correctChoiceIndex', this.localCorrectChoiceIndex)
+      this.$bus.emit('onChangeCorrectChoiceIndexInQuestionChoicesInput', this.localCorrectChoiceIndex)
+    },
+    onEdit (choice) {
+      this.editChoice(choice)
+    },
+    onDelete (choice) {
+      this.removeChoice(choice.id)
+    },
+    editChoice (choice) {
+      const choiceIndex = this.choices.list.findIndex(item => item.id === choice.id)
+      if (choiceIndex === -1) {
+        return
+      }
+      this.entityCreateLoading = true
+      APIGateway.questionChoice.put({ data: choice })
+        .then((choice) => {
+          this.choices.list[choiceIndex].text = choice.text.replace(/<br>/g, '\n')
+          this.entityCreateLoading = false
+        })
+        .catch(() => {
+          this.entityCreateLoading = false
+        })
+    },
+    removeChoice (choiceId) {
+      const choiceIndex = this.choices.list.findIndex(item => item.id === choiceId)
+      if (choiceIndex === -1) {
+        return
+      }
+      this.choices.list.splice(choiceIndex, 1)
+      this.emitValue(this.choices.list.map(item => item.id))
+    },
+    entityCreate () {
+      this.entityCreateLoading = true
+      this.$refs.entityCreate.createEntity(false)
+        .then(response => {
+          this.choices.list.push(new QuestionChoice(response.data))
+          this.entityCreateLoading = false
+          this.emitValue(this.choices.list.map(item => item.id))
+        })
+        .catch(() => {
+          this.entityCreateLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.QuestionChoices {
+  width: 100%;
+  .choice-item {
+    display: flex;
+    .choice-item-radio {
+      width: 60px;
+    }
+    .choice-item-component {
+      width: calc( 100% - 60px );
+    }
+  }
+}
+</style>

+ 2 - 2
src/components/FormBuilderCustumComponents/QuestionsSelector/QuestionsSelector.vue

@@ -164,8 +164,8 @@ export default {
       },
       inputs: [
         { type: 'hidden', name: 'type', value: this.questionType },
-        { type: 'hidden', name: 'session_template_questions__session_template', value: null },
-        { type: 'hidden', name: 'session_template_questions__session_template__unit', value: this.unitId }
+        { type: 'hidden', name: 'session_template_questions__session_template', value: null }
+        // { type: 'hidden', name: 'session_template_questions__session_template__unit', value: this.unitId }
       ],
       table: {
         columns: []

+ 3 - 0
src/components/Widgets/SessionTemplate/AdminEventQuestionCreate/AdminEventQuestionCreate.vue

@@ -35,8 +35,10 @@ import { Question } from 'src/models/Question.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import BtnControl from 'src/components/Control/btn.vue'
 import { FormBuilderAssist } from 'quasar-form-builder'
+import QuestionChoices from 'src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
+const QuestionChoicesComp = shallowRef(QuestionChoices)
 
 export default {
   name: 'AdminEventQuestionCreate',
@@ -56,6 +58,7 @@ export default {
       inputs: [
         { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
         { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: QuestionChoicesComp, name: 'choices', placeholder: ' ', col: 'col-12 flex justify-end' },
         { type: BtnControlComp, name: 'btn', label: 'ذخیره سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
         { type: 'hidden', name: 'type', responseKey: 'type', value: 'EVENT' }
       ]

+ 10 - 2
src/components/Widgets/SessionTemplate/AdminEventQuestionShow/AdminEventQuestionShow.vue

@@ -23,7 +23,7 @@
                  :show-save-button="false"
                  :show-reload-button="false"
                  :redirect-after-edit="false"
-                 :after-load-input-data="afterLoadInputData" />
+                 :before-load-input-data="beforeLoadInputData" />
   </div>
 </template>
 
@@ -34,8 +34,10 @@ import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import { FormBuilderAssist } from 'quasar-form-builder'
 import BtnControl from 'src/components/Control/btn.vue'
+import QuestionChoices from 'src/components/FormBuilderCustumComponents/QuestionChoices/QuestionChoices.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
+const QuestionChoicesComp = shallowRef(QuestionChoices)
 
 export default {
   name: 'AdminEventQuestionShow',
@@ -54,7 +56,9 @@ export default {
       inputs: [
         { type: 'input', name: 'text', responseKey: 'text', label: 'سوال', placeholder: ' ', col: 'col-12' },
         { type: 'inputEditor', name: 'correct_answer', responseKey: 'correct_answer', label: 'پاسخ', placeholder: ' ', col: 'col-12' },
+        { type: QuestionChoicesComp, name: 'choices', responseKey: 'choices_info', correctChoiceIndex: 0, col: 'col-12 flex justify-end' },
         { type: BtnControlComp, name: 'btn', label: 'ویرایش سوال', placeholder: ' ', atClick: () => {}, col: 'col-12 flex justify-end' },
+        { type: 'hidden', name: 'correct_choice_index', responseKey: 'correct_choice_index' },
         { type: 'hidden', name: 'type', responseKey: 'type', value: 'EVENT' }
       ]
     }
@@ -62,12 +66,16 @@ export default {
   mounted() {
     this.setActionBtn()
     this.mounted = true
+    this.$bus.on('onChangeCorrectChoiceIndexInQuestionChoicesInput', (correctChoiceIndex) => {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'correct_choice_index', 'value', correctChoiceIndex)
+    })
   },
   methods: {
     setActionBtn () {
       FormBuilderAssist.setAttributeByName(this.inputs, 'btn', 'atClick', this.edit)
     },
-    afterLoadInputData () {
+    beforeLoadInputData (responseData) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'choices', 'correctChoiceIndex', responseData.correct_choice_index)
       this.entityLoading = false
     },
     edit() {

+ 17 - 0
src/models/Question.js

@@ -8,6 +8,9 @@ class Question extends Model {
       { key: 'correct_answer' },
       { key: 'mark' },
       { key: 'level' },
+      { key: 'model_type' },
+      { key: 'choices' },
+      { key: 'correct_choice_index' },
       {
         key: 'type',
         default: 'QUESTION_BANK'
@@ -48,6 +51,20 @@ class Question extends Model {
             value: 'QUESTION_BANK'
           }
         ]
+      },
+      model_type: {
+        infoKey: 'model_type_info',
+        enumListKey: 'model_typeEnums',
+        enums: [
+          {
+            label: 'تشریحی',
+            value: 'OPEN_ENDED'
+          },
+          {
+            label: 'چند گزینه ای',
+            value: 'MULTI_CHOICE'
+          }
+        ]
       }
     })
   }

+ 19 - 0
src/models/QuestionChoice.js

@@ -0,0 +1,19 @@
+import { Collection, Model } from 'js-abstract-model'
+
+class QuestionChoice extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      // { key: 'question' },
+      { key: 'text' }
+    ])
+  }
+}
+
+class QuestionChoiceList extends Collection {
+  model () {
+    return QuestionChoice
+  }
+}
+
+export { QuestionChoice, QuestionChoiceList }

+ 9 - 9
yarn.lock

@@ -5572,22 +5572,22 @@ quasar-app-extension-q-page-builder@^0.4.4:
   dependencies:
     quasar-ui-q-page-builder "^0.4.4"
 
-quasar-crud@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-1.0.5.tgz#64952c120bc6a28b4461776a24c961479e0f6fa7"
-  integrity sha512-u/3WsiJpOJT3EYRklo4mE9O2ZnJSHvWmDLhW4UaXyAumATrNb5rf58V3xckBXG81kKFqBC8uC9TovfN7NQm3tw==
+quasar-crud@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/quasar-crud/-/quasar-crud-1.0.7.tgz#33ddaab6d21fda2f7d141fb2f28d4d720c2dd9e3"
+  integrity sha512-8cwY7YwLJyHF2/RmsTC0+t+J5/Dg3e9i464BCTw2HpIoLBTN/0S+IF1DixWA3yFc4jiMjb1ZSctVgNJgm7LPoQ==
   dependencies:
     "@quasar/extras" "^1.16.9"
     axios "^1.6.5"
     moment-jalaali "^0.10.0"
     quasar "^2.14.2"
-    quasar-form-builder "^1.0.2"
+    quasar-form-builder "^1.0.4"
     shvl "^3.0.0"
 
-quasar-form-builder@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-1.0.2.tgz#fe4b04165561f24a79cd39aa45659343b79a92c8"
-  integrity sha512-yRtU6UWNLOhnxW525Unpw32nq0wuZoRXigGeJ/G0TOB93kHcyR2Aoz+FyRypPJyQiYw+SCTK84yIrA1OXjMo0Q==
+quasar-form-builder@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/quasar-form-builder/-/quasar-form-builder-1.0.4.tgz#18f199f59298facc0808eeeda1780b49a4f8b83a"
+  integrity sha512-t1Es7Ol1j1xzhNXlaKkf0xjmkUJ3D/RtP9FQGeRZMfamtfnb6KMJqh2OmPAVP0xyDBCzRTZlVyy05yFZPFy7Pg==
   dependencies:
     "@quasar/extras" "^1.16.9"
     core-js "^3.34.0"