<commit_before><template>

  <div>
    <KGrid :gridStyle="gridStyle">
      <!-- this.$refs.questionListWrapper is referenced inside AnswerHistory for scrolling -->
      <KGridItem
        v-if="windowIsLarge"
        ref="questionListWrapper"
        :layout12="{ span: 4 }"
        class="column-pane"
      >
        <div class="column-contents-wrapper">
          <KPageContainer>
            <AnswerHistory
              :questionNumber="questionNumber"
              :wrapperComponentRefs="this.$refs"
              @goToQuestion="goToQuestion"
            />
          </KPageContainer>
        </div>
      </KGridItem>
      <KGridItem :layout12="{ span: 8 }" class="column-pane">
        <div :class="{'column-contents-wrapper' : !windowIsSmall}">
          <KPageContainer>
            <h1>
              {{ $tr('question', { num: questionNumber + 1, total: exam.question_count }) }}
            </h1>
            <KContentRenderer
              v-if="content && itemId"
              ref="contentRenderer"
              :kind="content.kind"
              :files="content.files"
              :available="content.available"
              :extraFields="content.extra_fields"
              :itemId="itemId"
              :assessment="true"
              :allowHints="false"
              :answerState="currentAttempt.answer"
              @interaction="saveAnswer"
            />
            <UiAlert v-else :dismissible="false" type="error">
              {{ $tr('noItemId') }}
            </UiAlert>
          </KPageContainer>

          <!-- contents displayed in reverse-order for semantic ordering -->
          <BottomAppBar :dir="isRtl ? 'ltr' : 'rtl'" :maxWidth="null">
            <KButton
              :disabled="questionNumber===exam.question_count-1"
              :primary="true"
              class="footer-button"
              @click="goToQuestion(questionNumber + 1)"
            >
              {{ $tr('nextQuestion') }}
              <KIcon icon="forward" color="white" class="forward-icon" />
            </KButton>
            <KButton
              :disabled="questionNumber===0"
              :primary="true"
              class="footer-button"
              :class="{ 'left-align': windowIsSmall }"
              @click="goToQuestion(questionNumber - 1)"
            >
              <KIcon icon="back" color="white" class="back-icon" />
              {{ $tr('previousQuestion') }}
            </KButton>

            <!-- below prev/next buttons in tab and DOM order, in footer -->
            <div v-if="windowIsLarge" class="left-align" dir="auto">
              <div class="answered">
                {{ answeredText }}
              </div>
              <KButton
                :text="$tr('submitExam')"
                :primary="false"
                appearance="flat-button"
                @click="toggleModal"
              />
            </div>

          </BottomAppBar>

          <!-- below prev/next buttons in tab and DOM order, in page -->
          <KPageContainer v-if="!windowIsLarge">
            <div
              class="bottom-block"
              :class="{ windowIsSmall }"
            >
              <div class="answered">
                {{ answeredText }}
              </div>
              <KButton
                :text="$tr('submitExam')"
                :primary="false"
                appearance="flat-button"
                style="margin-left: 0"
                @click="toggleModal"
              />
            </div>
          </KPageContainer>
        </div>
      </KGridItem>
    </KGrid>


    <KModal
      v-if="submitModalOpen"
      :title="$tr('submitExam')"
      :submitText="$tr('submitExam')"
      :cancelText="coreString('goBackAction')"
      @submit="finishExam"
      @cancel="toggleModal"
    >
      <p>{{ $tr('areYouSure') }}</p>
      <p v-if="questionsUnanswered">
        {{ $tr('unanswered', { numLeft: questionsUnanswered } ) }}
      </p>
    </KModal>
  </div>

</template>


<script>

  import { mapState, mapActions } from 'vuex';
  import { InteractionTypes } from 'kolibri.coreVue.vuex.constants';
  import isEqual from 'lodash/isEqual';
  import { now } from 'kolibri.utils.serverClock';
  import debounce from 'lodash/debounce';
  import BottomAppBar from 'kolibri.coreVue.components.BottomAppBar';
  import UiAlert from 'kolibri.coreVue.components.UiAlert';
  import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin';
  import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
  import { ClassesPageNames } from '../../constants';
  import AnswerHistory from './AnswerHistory';

  export default {
    name: 'ExamPage',
    metaInfo() {
      return {
        title: this.exam.title,
      };
    },
    components: {
      AnswerHistory,
      UiAlert,
      BottomAppBar,
    },
    mixins: [responsiveWindowMixin, commonCoreStrings],
    data() {
      return {
        submitModalOpen: false,
      };
    },
    computed: {
      ...mapState('examViewer', [
        'exam',
        'content',
        'itemId',
        'questionNumber',
        'currentAttempt',
        'questionsAnswered',
      ]),
      gridStyle() {
        if (!this.windowIsSmall) {
          return {
            position: 'fixed',
            top: '64px',
            right: '16px',
            bottom: '72px',
            left: '16px',
          };
        }
        return {};
      },
      answeredText() {
        return this.$tr('questionsAnswered', {
          numAnswered: this.questionsAnswered,
          numTotal: this.exam.question_count,
        });
      },
      backPageLink() {
        return {
          name: ClassesPageNames.CLASS_ASSIGNMENTS,
        };
      },
      questionsUnanswered() {
        return this.exam.question_count - this.questionsAnswered;
      },
      debouncedSetAndSaveCurrentExamAttemptLog() {
        // So as not to share debounced functions between instances of the same component
        // and also to allow access to the cancel method of the debounced function
        // best practice seems to be to do it as a computed property and not a method:
        // https://github.com/vuejs/vue/issues/2870#issuecomment-219096773
        return debounce(this.setAndSaveCurrentExamAttemptLog, 5000);
      },
    },
    watch: {
      itemId(newVal, oldVal) {
        // HACK: manually dismiss the perseus renderer message when moving
        // to a different item (fixes #3853)
        if (newVal !== oldVal) {
          this.$refs.contentRenderer.$refs.contentView.dismissMessage();
        }
      },
    },
    methods: {
      ...mapActions('examViewer', ['setAndSaveCurrentExamAttemptLog', 'closeExam']),
      checkAnswer() {
        if (this.$refs.contentRenderer) {
          return this.$refs.contentRenderer.checkAnswer();
        }
        return null;
      },
      saveAnswer(force = false) {
        const answer = this.checkAnswer();
        if (answer && !isEqual(answer.answerState, this.currentAttempt.answer)) {
          const attempt = Object.assign({}, this.currentAttempt);
          // Copy the interaction history separately, as otherwise we
          // will still be modifying the underlying object
          attempt.interaction_history = Array(...attempt.interaction_history);<commit_msg>fixes related to reverse-order layout<commit_after><template>

  <div>
    <KGrid :gridStyle="gridStyle">
      <!-- this.$refs.questionListWrapper is referenced inside AnswerHistory for scrolling -->
      <KGridItem
        v-if="windowIsLarge"
        ref="questionListWrapper"
        :layout12="{ span: 4 }"
        class="column-pane"
      >
        <div class="column-contents-wrapper">
          <KPageContainer>
            <AnswerHistory
              :questionNumber="questionNumber"
              :wrapperComponentRefs="this.$refs"
              @goToQuestion="goToQuestion"
            />
          </KPageContainer>
        </div>
      </KGridItem>
      <KGridItem :layout12="{ span: 8 }" class="column-pane">
        <div :class="{'column-contents-wrapper' : !windowIsSmall}">
          <KPageContainer>
            <h1>
              {{ $tr('question', { num: questionNumber + 1, total: exam.question_count }) }}
            </h1>
            <KContentRenderer
              v-if="content && itemId"
              ref="contentRenderer"
              :kind="content.kind"
              :files="content.files"
              :available="content.available"
              :extraFields="content.extra_fields"
              :itemId="itemId"
              :assessment="true"
              :allowHints="false"
              :answerState="currentAttempt.answer"
              @interaction="saveAnswer"
            />
            <UiAlert v-else :dismissible="false" type="error">
              {{ $tr('noItemId') }}
            </UiAlert>
          </KPageContainer>

          <BottomAppBar :dir="bottomBarLayoutDirection" :maxWidth="null">
            <KButton
              :disabled="questionNumber===exam.question_count-1"
              :primary="true"
              class="footer-button"
              :dir="layoutDirReset"
              @click="goToQuestion(questionNumber + 1)"
            >
              {{ $tr('nextQuestion') }}
              <KIcon icon="forward" color="white" class="forward-icon" />
            </KButton>
            <KButton
              :disabled="questionNumber===0"
              :primary="true"
              class="footer-button"
              :dir="layoutDirReset"
              :class="{ 'left-align': windowIsSmall }"
              @click="goToQuestion(questionNumber - 1)"
            >
              <KIcon icon="back" color="white" class="back-icon" />
              {{ $tr('previousQuestion') }}
            </KButton>

            <!-- below prev/next buttons in tab and DOM order, in footer -->
            <div
              v-if="windowIsLarge"
              :dir="layoutDirReset"
              class="left-align"
            >
              <div class="answered">
                {{ answeredText }}
              </div>
              <KButton
                :text="$tr('submitExam')"
                :primary="false"
                appearance="flat-button"
                @click="toggleModal"
              />
            </div>

          </BottomAppBar>

          <!-- below prev/next buttons in tab and DOM order, in page -->
          <KPageContainer v-if="!windowIsLarge">
            <div
              class="bottom-block"
              :class="{ windowIsSmall }"
            >
              <div class="answered">
                {{ answeredText }}
              </div>
              <KButton
                :text="$tr('submitExam')"
                :primary="false"
                appearance="flat-button"
                style="margin-left: 0"
                @click="toggleModal"
              />
            </div>
          </KPageContainer>
        </div>
      </KGridItem>
    </KGrid>


    <KModal
      v-if="submitModalOpen"
      :title="$tr('submitExam')"
      :submitText="$tr('submitExam')"
      :cancelText="coreString('goBackAction')"
      @submit="finishExam"
      @cancel="toggleModal"
    >
      <p>{{ $tr('areYouSure') }}</p>
      <p v-if="questionsUnanswered">
        {{ $tr('unanswered', { numLeft: questionsUnanswered } ) }}
      </p>
    </KModal>
  </div>

</template>


<script>

  import { mapState, mapActions } from 'vuex';
  import { InteractionTypes } from 'kolibri.coreVue.vuex.constants';
  import isEqual from 'lodash/isEqual';
  import { now } from 'kolibri.utils.serverClock';
  import debounce from 'lodash/debounce';
  import BottomAppBar from 'kolibri.coreVue.components.BottomAppBar';
  import UiAlert from 'kolibri.coreVue.components.UiAlert';
  import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin';
  import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
  import { ClassesPageNames } from '../../constants';
  import AnswerHistory from './AnswerHistory';

  export default {
    name: 'ExamPage',
    metaInfo() {
      return {
        title: this.exam.title,
      };
    },
    components: {
      AnswerHistory,
      UiAlert,
      BottomAppBar,
    },
    mixins: [responsiveWindowMixin, commonCoreStrings],
    data() {
      return {
        submitModalOpen: false,
      };
    },
    computed: {
      ...mapState('examViewer', [
        'exam',
        'content',
        'itemId',
        'questionNumber',
        'currentAttempt',
        'questionsAnswered',
      ]),
      gridStyle() {
        if (!this.windowIsSmall) {
          return {
            position: 'fixed',
            top: '64px',
            right: '16px',
            bottom: '72px',
            left: '16px',
          };
        }
        return {};
      },
      answeredText() {
        return this.$tr('questionsAnswered', {
          numAnswered: this.questionsAnswered,
          numTotal: this.exam.question_count,
        });
      },
      backPageLink() {
        return {
          name: ClassesPageNames.CLASS_ASSIGNMENTS,
        };
      },
      questionsUnanswered() {
        return this.exam.question_count - this.questionsAnswered;
      },
      debouncedSetAndSaveCurrentExamAttemptLog() {
        // So as not to share debounced functions between instances of the same component
        // and also to allow access to the cancel method of the debounced function
        // best practice seems to be to do it as a computed property and not a method:
        // https://github.com/vuejs/vue/issues/2870#issuecomment-219096773
        return debounce(this.setAndSaveCurrentExamAttemptLog, 5000);
      },
      bottomBarLayoutDirection() {
        // Allows contents to be displayed visually in reverse-order,
        // but semantically in correct order.
        return this.isRtl ? 'ltr' : 'rtl';
      },
      layoutDirReset() {
        // Overrides bottomBarLayoutDirection reversal
        return this.isRtl ? 'rtl' : 'ltr';
      },
    },
    watch: {
      itemId(newVal, oldVal) {
        // HACK: manually dismiss the perseus renderer message when moving
        // to a different item (fixes #3853)
        if (newVal !== oldVal) {
          this.$refs.contentRenderer.$refs.contentView.dismissMessage();
        }
      },
    },
    methods: {
      ...mapActions('examViewer', ['setAndSaveCurrentExamAttemptLog', 'closeExam']),
      checkAnswer() {
        if (this.$refs.contentRenderer) {
          return this.$refs.contentRenderer.checkAnswer();
        }
        return null;
      },
      saveAnswer(force = false) {
        const answer = this.checkAnswer();
        if (answer && !isEqual(answer.answerState, this.currentAttempt.answer)) {
          const attempt = Object.assign({}, this.currentAttempt);
          // Copy the interaction history separately, as otherwise we
          // will still be modifying the underlying object
          attempt.interaction_history = Array(...attempt.interaction_history);
{'fraction_non_alphanumeric': 0.12767232767232767, 'fraction_numerical': 0.005727605727605728, 'mean_word_length': 1.8151481064866892, 'pattern_counts': {'":': 0, '<': 112, '<?xml version=': 0, '>': 112, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2765645', 'n_tokens_mistral': 4281, 'n_tokens_neox': 4080, 'n_words': 808}