<commit_before>// Copyright 2014 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview End-to-end tests of the full exploration editor.
 */

var general = require('../protractor_utils/general.js');
var forms = require('../protractor_utils/forms.js');
var users = require('../protractor_utils/users.js');
var workflow = require('../protractor_utils/workflow.js');
var editor = require('../protractor_utils/editor.js');
var ExplorationPlayerPage =
  require('../protractor_utils/ExplorationPlayerPage.js');

describe('Full exploration editor', function() {
  var explorationPlayerPage = null;

  beforeAll(function() {
    explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
  });

  it('should navigate multiple states correctly, with parameters', function() {
    users.createUser('user4@editorAndPlayer.com', 'user4EditorAndPlayer');
    users.login('user4@editorAndPlayer.com');

    workflow.createExploration();
    editor.setStateName('card 1');
    editor.setContent(forms.toRichText('this is card 1'));
    editor.setInteraction('NumericInput');
    editor.addResponse('NumericInput', null, 'final card', true, 'Equals', 21);
    editor.ResponseEditor(0).setDestination('card 2', true);

    editor.moveToState('card 2');
    editor.setContent(forms.toRichText(
      'this is card 2 with previous answer {{answer}}'));
    editor.setInteraction(
      'MultipleChoiceInput',
      [forms.toRichText('return'), forms.toRichText('complete')]);
    editor.addResponse('MultipleChoiceInput', null, 'card 1', false,
      'Equals', 'return');
    editor.setDefaultOutcome(null, 'final card', false);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.expectContentToMatch(
      forms.toRichText('this is card 1'));
    explorationPlayerPage.submitAnswer('NumericInput', 19);
    explorationPlayerPage.submitAnswer('NumericInput', 21);
    explorationPlayerPage.expectContentToMatch(forms.toRichText(
      'this is card 2 with previous answer 21'));
    explorationPlayerPage.submitAnswer('MultipleChoiceInput', 'return');
    explorationPlayerPage.expectContentToMatch(forms.toRichText(
      'this is card 1'));
    explorationPlayerPage.submitAnswer('NumericInput', 21);
    explorationPlayerPage.expectContentToMatch(forms.toRichText(
      'this is card 2 with previous answer 21'));
    explorationPlayerPage.expectExplorationToNotBeOver();
    explorationPlayerPage.submitAnswer('MultipleChoiceInput', 'complete');
    explorationPlayerPage.expectExplorationToBeOver();
    users.logout();
  });

  it('should handle discarding changes, navigation, deleting states, ' +
      'changing the first state, displaying content, deleting responses and ' +
      'switching to preview mode', function() {
    users.createUser('user5@editorAndPlayer.com', 'user5EditorAndPlayer');
    users.login('user5@editorAndPlayer.com');

    workflow.createExploration();
    general.getExplorationIdFromEditor().then(function(explorationId) {
      // Check discarding of changes
      editor.setStateName('card1');
      editor.expectCurrentStateToBe('card1');
      editor.setContent(forms.toRichText('card1 content'));
      editor.setInteraction('TextInput');
      editor.setDefaultOutcome(null, 'final card', true);
      editor.ResponseEditor('default').setDestination('card2', true);
      editor.moveToState('card2');
      // NOTE: we must move to the state before checking state names to avoid
      // inexplicable failures of the protractor utility that reads state names
      // (the user-visible names are fine either way). See issue 732 for more.
      editor.expectStateNamesToBe(['final card', 'card1', 'card2']);
      editor.setInteraction('EndExploration');

      editor.discardChanges();
      editor.expectCurrentStateToBe(general.FIRST_STATE_DEFAULT_NAME);
      editor.setStateName('first');
      editor.expectCurrentStateToBe('first');
      editor.setContent(forms.toRichText('card1 content'));

      // Check deletion of states and changing the first state
      editor.setInteraction('TextInput');
      editor.setDefaultOutcome(null, 'final card', true);
      editor.ResponseEditor('default').setDestination('second', true);
      editor.moveToState('second');
      editor.expectStateNamesToBe(['final card', 'first', 'second']);
      editor.expectCurrentStateToBe('second');
      editor.expectAvailableFirstStatesToBe(['final card', 'first', 'second']);
      editor.setFirstState('second');
      editor.moveToState('first');
      editor.deleteState('first');
      editor.expectCurrentStateToBe('second');
      editor.expectStateNamesToBe(['final card', 'second']);

      // Check behaviour of the back button
      editor.setObjective('do some stuff here');
      expect(browser.getCurrentUrl()).toEqual(
        general.SERVER_URL_PREFIX + general.EDITOR_URL_SLICE +
        explorationId + '#/gui/second');
      browser.navigate().back();
      expect(browser.getCurrentUrl()).toEqual(
        general.SERVER_URL_PREFIX + general.EDITOR_URL_SLICE +
        explorationId + '#/settings');
      browser.navigate().back();
      expect(browser.getCurrentUrl()).toEqual(
        general.SERVER_URL_PREFIX + general.EDITOR_URL_SLICE +
        explorationId + '#/gui/second');

      // Check display of content & interaction in the editor
      editor.setContent(function(richTextEditor) {
        richTextEditor.appendItalicText('Welcome');
      });
      editor.expectContentToMatch(function(richTextChecker) {
        richTextChecker.readItalicText('Welcome');
      });
      editor.setInteraction('NumericInput');
      editor.expectInteractionToMatch('NumericInput');

      // Check deletion of groups
      editor.setDefaultOutcome(forms.toRichText('Farewell'), null, false);
      editor.ResponseEditor('default').
        expectAvailableDestinationsToBe(['second', 'final card']);
      editor.ResponseEditor('default').setDestination('final card', false);
      editor.ResponseEditor('default').
        expectAvailableDestinationsToBe(['second', 'final card']);
      editor.addResponse('NumericInput', null, 'final card', false,
        'IsGreaterThan', 2);
      // eslint-disable-next-line dot-notation
      editor.ResponseEditor(0).delete();

      // Setup a terminating state
      editor.moveToState('final card');
      editor.setInteraction('EndExploration');

      // Check that preview/editor switch doesn't change state
      editor.navigateToPreviewTab();
      explorationPlayerPage.expectExplorationToBeOver();
      editor.navigateToMainTab();
      editor.expectCurrentStateToBe('final card');
      editor.moveToState('second');

      // Check editor preview tab
      editor.navigateToPreviewTab();
      explorationPlayerPage.expectContentToMatch(function(richTextEditor) {
        richTextEditor.readItalicText('Welcome');
      });
      explorationPlayerPage.expectInteractionToMatch('NumericInput');
      explorationPlayerPage.submitAnswer('NumericInput', 6);
      // This checks the previously-deleted group no longer applies.
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('Farewell'));
      explorationPlayerPage.clickThroughToNextCard();
      explorationPlayerPage.expectExplorationToBeOver();

      editor.discardChanges();
      users.logout();
    });
  });

  it('should handle multiple rules in an answer group and also disallow ' +
      'editing of a read-only exploration', function() {
    users.createUser('user6@editorAndPlayer.com', 'user6EditorAndPlayer');
    users.createUser('user7@editorAndPlayer.com', 'user7EditorAndPlayer');
    users.login('user6@editorAndPlayer.com');
    workflow.createExploration();

    general.getExplorationIdFromEditor().then(function(explorationId) {
      // Create an exploration with multiple groups.
      editor.setStateName('first card');
      editor.setContent(forms.toRichText('How are you feeling?'));
      editor.setInteraction('TextInput');
      editor.addResponse('TextInput', forms.toRichText('You must be happy!'),
        null, false, 'Equals', 'happy');
      editor.addResponse('TextInput', forms.toRichText('No being sad!'),
        null, false, 'Contains', 'sad');
      editor.setDefaultOutcome(forms.toRichText(
        'Okay, now this is just becoming annoying.'), 'final card', true);

      // Now, add multiple rules to a single answer group.
      editor.ResponseEditor(0).addRule('TextInput', 'Contains', 'meh');
      editor.ResponseEditor(0).addRule('TextInput', 'Contains', 'okay');

      // Ensure that the only rule for this group cannot be deleted.
      editor.ResponseEditor(1).expectCannotDeleteRule(0);

      // Setup a terminating state.
      editor.moveToState('final card');
      editor.setInteraction('EndExploration');

      // Save.
      editor.setTitle('Testing multiple rules');
      editor.setCategory('Algebra');
      editor.setObjective('To assess happiness.');
      editor.openAndClosePreviewSummaryTile();
      editor.saveChanges();
      workflow.publishExploration();

      // Login as another user and verify that the exploration editor does not
      // allow the second user to modify the exploration.
      users.logout();
      users.login('user7@editorAndPlayer.com');
      general.openEditor(explorationId);
      editor.exitTutorialIfNecessary();

      // Verify nothing can change with this user.
      editor.expectInteractionToMatch('TextInput');
      editor.expectCannotDeleteInteraction();
      editor.expectCannotAddResponse();
      editor.expectCannotSaveChanges();

      // Check answer group 1.
      var responseEditor = editor.ResponseEditor(0);
      responseEditor.expectCannotSetFeedback();
      responseEditor.expectCannotSetDestination();
      responseEditor.expectCannotDeleteResponse();
      responseEditor.expectCannotAddRule();
      responseEditor.expectCannotDeleteRule(0);
      responseEditor.expectCannotDeleteRule(1);

      // Check answer group 2.
      responseEditor = editor.ResponseEditor(0);
      responseEditor.expectCannotSetFeedback();
      responseEditor.expectCannotSetDestination();
      responseEditor.expectCannotDeleteResponse();
      responseEditor.expectCannotAddRule();
      responseEditor.expectCannotDeleteRule(0);

      // Check default outcome.
      responseEditor = editor.ResponseEditor('default');
      responseEditor.expectCannotSetFeedback();
      responseEditor.expectCannotSetDestination();

      // Check editor preview tab to verify multiple rules are working.
      general.moveToPlayer();
      explorationPlayerPage.expectContentToMatch(
        forms.toRichText('How are you feeling?'));
      explorationPlayerPage.expectInteractionToMatch('TextInput');

      explorationPlayerPage.submitAnswer('TextInput', 'happy');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('You must be happy!'));

      explorationPlayerPage.submitAnswer('TextInput', 'meh, I\'m okay');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('You must be happy!'));

      explorationPlayerPage.submitAnswer('TextInput', 'NO I\'M SAD');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('No being sad!'));

      explorationPlayerPage.submitAnswer('TextInput', 'Fine...I\'m doing okay');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('You must be happy!'));

      // Finish the exploration.
      explorationPlayerPage.submitAnswer('TextInput', 'Whatever...');

      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('Okay, now this is just becoming annoying.'));
      explorationPlayerPage.clickThroughToNextCard();
      explorationPlayerPage.expectExplorationToBeOver();

      users.logout();
    });
  });

  afterEach(function() {
    general.checkForConsoleErrors([]);
  });
});
<commit_msg>Added e2e test<commit_after>// Copyright 2014 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview End-to-end tests of the full exploration editor.
 */

var general = require('../protractor_utils/general.js');
var forms = require('../protractor_utils/forms.js');
var users = require('../protractor_utils/users.js');
var workflow = require('../protractor_utils/workflow.js');
var editor = require('../protractor_utils/editor.js');
var ExplorationPlayerPage =
  require('../protractor_utils/ExplorationPlayerPage.js');

describe('Full exploration editor', function() {
  var explorationPlayerPage = null;
  var parentId2 = null;
  var refresherExplorationId = null;
  var currentExplorationId = null;
  var pathname = null;
  var parentId1 = null;

  beforeAll(function() {
    explorationPlayerPage = new ExplorationPlayerPage.ExplorationPlayerPage();
    users.createUser('user4@editorAndPlayer.com', 'user4EditorAndPlayer');
    users.login('user4@editorAndPlayer.com');

    workflow.createAndPublishExploration('Title 1', 'Algebra',
      'It is a sample exploration.');
    browser.getCurrentUrl().then(function(url) {
      pathname = url.split('/');
      /* In the url a # is added at the end that is not part of exploration
         ID */
      parentId1 = pathname[4].slice(0, -1);
    });

    workflow.createAndPublishExploration('Title 2', 'Algebra',
      'It is a sample exploration.');
    browser.getCurrentUrl().then(function(url) {
      pathname = url.split('/');
      parentId2 = pathname[4].slice(0, -1);
    });

    workflow.createAndPublishExploration('Title 3', 'Algebra',
      'It is a sample exploration.');
    browser.getCurrentUrl().then(function(url) {
      pathname = url.split('/');
      refresherExplorationId = pathname[4].slice(0, -1);
    });
  });

  it('should redirect back to parent exploration correctly when parent id is ' +
     'given as query parameter', function() {
        browser.get('/explore/' + refresherExplorationId + '?parent=' +
          parentId1 + '&parent=' + parentId2);
        browser.waitForAngular();

        /* The summary tile for redirection has to be scrolled down in the
           chrome window to be in view of the automated test,
           hence the scrollTo. */
        browser.executeScript('window.scrollTo(571,700);').then(function() {
          element(by.css('.protractor-test-exp-summary-tile-title')).click();
        });
        browser.waitForAngular();
        browser.getCurrentUrl().then(function(url) {
          currentExplorationId = url.split('/')[4].split('?')[0];
          expect(currentExplorationId).toBe(parentId2);
        });

        browser.executeScript('window.scrollTo(571,700);').then(function() {
          element(by.css('.protractor-test-exp-summary-tile-title')).click();
        });
        browser.waitForAngular();
        browser.getCurrentUrl().then(function(url) {
          currentExplorationId = url.split('/')[4];
          expect(currentExplorationId).toBe(parentId1);
        });

        users.logout();
     });

  it('should navigate multiple states correctly, with parameters', function() {
    users.createUser('user4@editorAndPlayer.com', 'user4EditorAndPlayer');
    users.login('user4@editorAndPlayer.com');

    workflow.createExploration();
    editor.setStateName('card 1');
    editor.setContent(forms.toRichText('this is card 1'));
    editor.setInteraction('NumericInput');
    editor.addResponse('NumericInput', null, 'final card', true, 'Equals', 21);
    editor.ResponseEditor(0).setDestination('card 2', true);

    editor.moveToState('card 2');
    editor.setContent(forms.toRichText(
      'this is card 2 with previous answer {{answer}}'));
    editor.setInteraction(
      'MultipleChoiceInput',
      [forms.toRichText('return'), forms.toRichText('complete')]);
    editor.addResponse('MultipleChoiceInput', null, 'card 1', false,
      'Equals', 'return');
    editor.setDefaultOutcome(null, 'final card', false);

    // Setup a terminating state
    editor.moveToState('final card');
    editor.setInteraction('EndExploration');
    editor.saveChanges();

    general.moveToPlayer();
    explorationPlayerPage.expectContentToMatch(
      forms.toRichText('this is card 1'));
    explorationPlayerPage.submitAnswer('NumericInput', 19);
    explorationPlayerPage.submitAnswer('NumericInput', 21);
    explorationPlayerPage.expectContentToMatch(forms.toRichText(
      'this is card 2 with previous answer 21'));
    explorationPlayerPage.submitAnswer('MultipleChoiceInput', 'return');
    explorationPlayerPage.expectContentToMatch(forms.toRichText(
      'this is card 1'));
    explorationPlayerPage.submitAnswer('NumericInput', 21);
    explorationPlayerPage.expectContentToMatch(forms.toRichText(
      'this is card 2 with previous answer 21'));
    explorationPlayerPage.expectExplorationToNotBeOver();
    explorationPlayerPage.submitAnswer('MultipleChoiceInput', 'complete');
    explorationPlayerPage.expectExplorationToBeOver();
    users.logout();
  });

  it('should handle discarding changes, navigation, deleting states, ' +
      'changing the first state, displaying content, deleting responses and ' +
      'switching to preview mode', function() {
    users.createUser('user5@editorAndPlayer.com', 'user5EditorAndPlayer');
    users.login('user5@editorAndPlayer.com');

    workflow.createExploration();
    general.getExplorationIdFromEditor().then(function(explorationId) {
      // Check discarding of changes
      editor.setStateName('card1');
      editor.expectCurrentStateToBe('card1');
      editor.setContent(forms.toRichText('card1 content'));
      editor.setInteraction('TextInput');
      editor.setDefaultOutcome(null, 'final card', true);
      editor.ResponseEditor('default').setDestination('card2', true);
      editor.moveToState('card2');
      // NOTE: we must move to the state before checking state names to avoid
      // inexplicable failures of the protractor utility that reads state names
      // (the user-visible names are fine either way). See issue 732 for more.
      editor.expectStateNamesToBe(['final card', 'card1', 'card2']);
      editor.setInteraction('EndExploration');

      editor.discardChanges();
      editor.expectCurrentStateToBe(general.FIRST_STATE_DEFAULT_NAME);
      editor.setStateName('first');
      editor.expectCurrentStateToBe('first');
      editor.setContent(forms.toRichText('card1 content'));

      // Check deletion of states and changing the first state
      editor.setInteraction('TextInput');
      editor.setDefaultOutcome(null, 'final card', true);
      editor.ResponseEditor('default').setDestination('second', true);
      editor.moveToState('second');
      editor.expectStateNamesToBe(['final card', 'first', 'second']);
      editor.expectCurrentStateToBe('second');
      editor.expectAvailableFirstStatesToBe(['final card', 'first', 'second']);
      editor.setFirstState('second');
      editor.moveToState('first');
      editor.deleteState('first');
      editor.expectCurrentStateToBe('second');
      editor.expectStateNamesToBe(['final card', 'second']);

      // Check behaviour of the back button
      editor.setObjective('do some stuff here');
      expect(browser.getCurrentUrl()).toEqual(
        general.SERVER_URL_PREFIX + general.EDITOR_URL_SLICE +
        explorationId + '#/gui/second');
      browser.navigate().back();
      expect(browser.getCurrentUrl()).toEqual(
        general.SERVER_URL_PREFIX + general.EDITOR_URL_SLICE +
        explorationId + '#/settings');
      browser.navigate().back();
      expect(browser.getCurrentUrl()).toEqual(
        general.SERVER_URL_PREFIX + general.EDITOR_URL_SLICE +
        explorationId + '#/gui/second');

      // Check display of content & interaction in the editor
      editor.setContent(function(richTextEditor) {
        richTextEditor.appendItalicText('Welcome');
      });
      editor.expectContentToMatch(function(richTextChecker) {
        richTextChecker.readItalicText('Welcome');
      });
      editor.setInteraction('NumericInput');
      editor.expectInteractionToMatch('NumericInput');

      // Check deletion of groups
      editor.setDefaultOutcome(forms.toRichText('Farewell'), null, false);
      editor.ResponseEditor('default').
        expectAvailableDestinationsToBe(['second', 'final card']);
      editor.ResponseEditor('default').setDestination('final card', false);
      editor.ResponseEditor('default').
        expectAvailableDestinationsToBe(['second', 'final card']);
      editor.addResponse('NumericInput', null, 'final card', false,
        'IsGreaterThan', 2);
      // eslint-disable-next-line dot-notation
      editor.ResponseEditor(0).delete();

      // Setup a terminating state
      editor.moveToState('final card');
      editor.setInteraction('EndExploration');

      // Check that preview/editor switch doesn't change state
      editor.navigateToPreviewTab();
      explorationPlayerPage.expectExplorationToBeOver();
      editor.navigateToMainTab();
      editor.expectCurrentStateToBe('final card');
      editor.moveToState('second');

      // Check editor preview tab
      editor.navigateToPreviewTab();
      explorationPlayerPage.expectContentToMatch(function(richTextEditor) {
        richTextEditor.readItalicText('Welcome');
      });
      explorationPlayerPage.expectInteractionToMatch('NumericInput');
      explorationPlayerPage.submitAnswer('NumericInput', 6);
      // This checks the previously-deleted group no longer applies.
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('Farewell'));
      explorationPlayerPage.clickThroughToNextCard();
      explorationPlayerPage.expectExplorationToBeOver();

      editor.discardChanges();
      users.logout();
    });
  });

  it('should handle multiple rules in an answer group and also disallow ' +
      'editing of a read-only exploration', function() {
    users.createUser('user6@editorAndPlayer.com', 'user6EditorAndPlayer');
    users.createUser('user7@editorAndPlayer.com', 'user7EditorAndPlayer');
    users.login('user6@editorAndPlayer.com');
    workflow.createExploration();

    general.getExplorationIdFromEditor().then(function(explorationId) {
      // Create an exploration with multiple groups.
      editor.setStateName('first card');
      editor.setContent(forms.toRichText('How are you feeling?'));
      editor.setInteraction('TextInput');
      editor.addResponse('TextInput', forms.toRichText('You must be happy!'),
        null, false, 'Equals', 'happy');
      editor.addResponse('TextInput', forms.toRichText('No being sad!'),
        null, false, 'Contains', 'sad');
      editor.setDefaultOutcome(forms.toRichText(
        'Okay, now this is just becoming annoying.'), 'final card', true);

      // Now, add multiple rules to a single answer group.
      editor.ResponseEditor(0).addRule('TextInput', 'Contains', 'meh');
      editor.ResponseEditor(0).addRule('TextInput', 'Contains', 'okay');

      // Ensure that the only rule for this group cannot be deleted.
      editor.ResponseEditor(1).expectCannotDeleteRule(0);

      // Setup a terminating state.
      editor.moveToState('final card');
      editor.setInteraction('EndExploration');

      // Save.
      editor.setTitle('Testing multiple rules');
      editor.setCategory('Algebra');
      editor.setObjective('To assess happiness.');
      editor.openAndClosePreviewSummaryTile();
      editor.saveChanges();
      workflow.publishExploration();

      // Login as another user and verify that the exploration editor does not
      // allow the second user to modify the exploration.
      users.logout();
      users.login('user7@editorAndPlayer.com');
      general.openEditor(explorationId);
      editor.exitTutorialIfNecessary();

      // Verify nothing can change with this user.
      editor.expectInteractionToMatch('TextInput');
      editor.expectCannotDeleteInteraction();
      editor.expectCannotAddResponse();
      editor.expectCannotSaveChanges();

      // Check answer group 1.
      var responseEditor = editor.ResponseEditor(0);
      responseEditor.expectCannotSetFeedback();
      responseEditor.expectCannotSetDestination();
      responseEditor.expectCannotDeleteResponse();
      responseEditor.expectCannotAddRule();
      responseEditor.expectCannotDeleteRule(0);
      responseEditor.expectCannotDeleteRule(1);

      // Check answer group 2.
      responseEditor = editor.ResponseEditor(0);
      responseEditor.expectCannotSetFeedback();
      responseEditor.expectCannotSetDestination();
      responseEditor.expectCannotDeleteResponse();
      responseEditor.expectCannotAddRule();
      responseEditor.expectCannotDeleteRule(0);

      // Check default outcome.
      responseEditor = editor.ResponseEditor('default');
      responseEditor.expectCannotSetFeedback();
      responseEditor.expectCannotSetDestination();

      // Check editor preview tab to verify multiple rules are working.
      general.moveToPlayer();
      explorationPlayerPage.expectContentToMatch(
        forms.toRichText('How are you feeling?'));
      explorationPlayerPage.expectInteractionToMatch('TextInput');

      explorationPlayerPage.submitAnswer('TextInput', 'happy');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('You must be happy!'));

      explorationPlayerPage.submitAnswer('TextInput', 'meh, I\'m okay');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('You must be happy!'));

      explorationPlayerPage.submitAnswer('TextInput', 'NO I\'M SAD');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('No being sad!'));

      explorationPlayerPage.submitAnswer('TextInput', 'Fine...I\'m doing okay');
      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('You must be happy!'));

      // Finish the exploration.
      explorationPlayerPage.submitAnswer('TextInput', 'Whatever...');

      explorationPlayerPage.expectLatestFeedbackToMatch(
        forms.toRichText('Okay, now this is just becoming annoying.'));
      explorationPlayerPage.clickThroughToNextCard();
      explorationPlayerPage.expectExplorationToBeOver();

      users.logout();
    });
  });

  afterEach(function() {
    general.checkForConsoleErrors([]);
  });
});
<|endoftext|>


<commit_before>define(["utils", "grid_lib", "datepicker/datepicker", "kladr/kladr"],
function(utils, gridLib, datepicker, kladr) {

    function drawParam(data, for_saving, admin) {
        console.log("drawParam");

        var block;

        if (data["type"] === "region"
            || data["type"] === "district"
            || data["type"] === "city"
            || data["type"] === "street"
            || data["type"] === "building"
            || data["type"] === "text"
            || data["type"] === "email"
            || data["type"] === "password"
            || data["type"] === "phon") {
            block = $("<input/>", {type: data["type"]});

        } else if (data["type"] === "textarea") {
            block = $("<textarea/>", {});

        } else if (data["type"] === "date") {
            block = $("<input/>", {type: "date"});
            datepicker.initDatePicker(block);
        }

        block.attr("id", data["param_id"]);
        block.attr("for-saving", for_saving);
        block.attr("name", data["param_name"]);

        if (data["value"]) {
            block.val(data["value"]);
            block.attr("param_val_id", data["param_val_id"]);
        }

        if (data["required"]) {
            block.attr("required", true);
        }

        var lable = $("", {
            text: data["param_name"],
        });

        if (!data["editable"] && !admin) {
            block.attr("readonly", true);
        }

        return $("
").append(lable).append(block);
    }

    function getFormData(name) {
        console.log("getFormData");

        var values = [];
        var empty = false;
        var pattern = /^[ \t\v\r\n\f]{0,}$/;
        var data = $("#"+name+" [for-saving=true]");
        console.log(data);

        for (var i = 0; i < data.length; ++i) {
            var elem = data[i];
            if (pattern.test($(elem).val()) && $(elem).attr("required")) {
                alert("Поле '" + $(elem).attr("name") + "' обязательное к заполнению.");
                empty = true;
                break;
            }

            values.push({
                "value": $(elem).val(),
                "param_val_id": $(elem).attr("param_val_id"),
                "id": $(elem).attr("id"),
            });
        };

        return empty ? false : values;
    }

    function getListHistoryEvents(historyDiv, F_ids) {
        console.log("getListHistoryEvents: F_ids: ", F_ids);

        utils.postRequest(
            { "form_ids": F_ids, },
            function(response) {
                console.log("getListHistoryEvents: ", response);

                if (response["data"]) {
                    var data = response["data"];
                    for (var i = 0; i < data.length; ++i) {
                        $("#"+historyDiv+" select").append($("<option></option>")
                            .attr("value", data[i]["id"])
                            .text(data[i]["name"])
                        );
                    }
                    $("#"+historyDiv).show();
                }
            },
            "/blankcontroller/getlisthistoryevents"
        );
    }

    function ShowPersonBlankFromGroup(groupRegId, faceId, dialogId) {
        console.log("ShowPersonBlankFromGroup");

        var data = { "group_reg_id": groupRegId, "face_id": faceId };
        console.log("ShowPersonBlankFromGroup: ", data);

        if (!groupRegId || !faceId) {
            return false;
        }

        $("#"+dialogId).empty();

        utils.postRequest(
            data,
            function(data) {
                ShowBlank(data["data"], dialogId, data["role"]);
                $("#"+dialogId+" #history").hide();
            },
            "/blankcontroller/getpersonrequestfromgroup"
        );

        $("#"+dialogId).dialog({
            modal: true,
            toTop: "150",
            height: "auto",
            width: "auto",
            buttons: {
                "Сохранить изменения": function() {
                    var values = getFormData(dialogId+" div");
                    if (!values) {
                        console.log("Не все поля заполнены.");
                        return false;
                    }

                    utils.postRequest(
                        { "data": values },
                        function(data) { gridLib.showServerPromtInDialog($("#"+dialogId), data["result"]); },
                        "/blankcontroller/editparams"
                    );
                },
                "Отмена": function() {
                    $(this).empty();
                    $(this).dialog("close");
                },
            }
        });

        return true;
    }

    function ShowBlank(d, dialogId, role, regId) {
        console.log("ShowBlank data: ", d);
        console.log("ShowBlank role: ", role);

        if (d.length == 0) {
            return false;
        }

        var history = $("
", {id: "history"})
            .append($("", {text: "Ранее заполненные анкеты"})).append("
")
            .append($("<select/>", {}))
            .append($("<input/>", {type: "button", value: "выбрать", id: "send-btn", name: "submit"}));

        $("#"+dialogId).append(history);

        if (regId) {
            $("#"+dialogId)
                .append($("<input/>", {type: "checkbox", id: "edit-history-box", width: "auto"}))
                .append($("", {id: "edit-history", style: "display:inline;"}).text("Информация о редактировании полей"));

            $("#"+dialogId+" #edit-history-box").change(function() {
                console.log("ShowBlank: ", { "reg_id": regId });
                utils.postRequest(
                    { "reg_id": regId },
                    function(response) {
                        if (response["result"] !== "ok") {
                            ShowServerAns(-1, response, "now #server-answer");
                            return false;
                        }

                        if ($("#"+dialogId+" #edit-history-box").is(":checked")) {
                            SetEditHistoryData(response["data"], dialogId);
                        } else {
                            ClearEditHistoryData(response["data"], dialogId);
                        }
                    },
                    "/blankcontroller/getedithistorydata"
                );
            });
        }

        $("#"+dialogId).append($("
")).append($("
"));

        var F_ids = {};

        $("#" + dialogId + " h1").text(d[0]["event_name"]);

        var div_forms = $("
", {id: "event-" + d[0]["event_id"]});
        var ul_forms = $("
", {});

        $(div_forms).append(ul_forms);
        $(div_forms).appendTo("#" + dialogId + " div");

        F_ids["form_id"] = [];

        for (i = 0; i < d.length; ++i) {
            if ($("#" + dialogId +" div#form-" + d[i]["form_id"]).attr("id") == undefined) {
                var li_form = $("
", {});
                var a_form = $("", {href: "#" + "form-" + d[i]["form_id"]}).text(d[i]["form_name"]);

                $(li_form).append(a_form);
                $(ul_forms).append(li_form);

                var div_tab_form = $("
", {id: "form-" + d[i]["form_id"]});
                $(div_forms).append(div_tab_form);

                F_ids["form_id"].push(parseInt(d[i]["form_id"]));

                var div_params = $("
", {id: "params-" + d[i]["form_id"]});
                $(div_tab_form).append(div_params);

                var table = $("
");
                div_params.append(table);

            }

            var tr = $("
").appendTo($("#" + dialogId +" div#form-" + d[i]["form_id"] + " table"));
            var td_1 = $("").appendTo(tr);
            $(td_1).append(drawParam(d[i], true, role));
            tr.append($("", {id: "export-param-"+d[i]["param_id"]}));
            tr.append($("", {id: "export-val-"+d[i]["param_id"]}));
            tr.append($("", {id: "export-edit-history-"+d[i]["param_id"]}));
        }

        $("#" + dialogId + " #" + "event-" + d[0]["event_id"]).tabs();

        console.log("F_ids: ", F_ids);

        $("#"+dialogId+" #history #send-btn").click(function() {
            utils.postRequest(
                {
                    "event_id": $("#"+dialogId+" #history select").find(":selected").attr("value")
                },
                function(response) {
                    if (response["result"] !== "ok") {
                        ShowServerAns(-1, response, "now #server-answer");
                        return false;
                    }
                    ExportDataLoad(response["data"], dialogId);
                },
                "/blankcontroller/gethistoryrequest"
            );
        });

        kladr.kladr();

        return F_ids;
    }

    function ShowServerAns(event_id, data, responseId) {
        console.log("ShowServerAns");

        if (data.result === "ok") {
            var msg = "Запрос успешно выполнен. ";
            if (event_id != 1) {
                msg += "Ваша заявка на участие будет рассмотрена.";
            } else {
                msg += "На вашу электронную почту было отправлено письмо, содержащее ссылку для подтверждения регистрации. "
                + "Воспользуйтесь этой ссылкой, для продолжения работы.";
            }
            $("#"+responseId).text(msg).css("color", "green");

        } else if (data.result === "loginExists") {
            $("#"+responseId).text("Такой логин уже существует.").css("color", "red");

        } else if (data.result === "badLogin") {
            $("#"+responseId).text("Логин может содержать латинские буквы и/или "
                + "цифры и иметь длину от 2 до 36 символов.").css("color", "red");

        } else if (data.result === "badPassword") {
            $("#"+responseId).text("Пароль должен иметь длину от 6 "
                + "до 36 символов.").css("color", "red");

        } else if (data.result === "Unauthorized") {
            $("#"+responseId).text("Пользователь не авторизован.").css("color", "red");

        } else if (data.result === "authorized") {
            $("#"+responseId).text("Пользователь уже авторизован.").css("color", "red");

        } else if (data.result === "badEmail") {
            $("#"+responseId).text("Проверьте правильность введенного Вами email.").css("color", "red");

        } else {
            $("#"+responseId).text(data.result).css("color", "red");
        }
    }

    function ShowPersonBlank(dialogId, regId) {
        console.log("ShowPersonBlank");

        console.log("reg_id", regId)

        $("#"+dialogId).empty();

        utils.postRequest(
            { "reg_id": regId },
            function(data) {
                var f_ids = ShowBlank(data["data"], dialogId, data["role"], regId);
                if (!f_ids) {
                    return false;
                }
                getListHistoryEvents(dialogId+" #history", f_ids);
            },
            "/blankcontroller/getpersonrequest"
        );

        $("#"+dialogId).dialog({
            modal: true,
            toTop: "150",
            height: "auto",
            width: "auto",
            buttons: {
                "Сохранить изменения": function() {
                    var values = getFormData(dialogId+" div");
                    if (values == false) {
                        console.log("Не все поля заполнены.");
                        alert("Не все поля заполнены.");
                        return false;
                    }

                    utils.postRequest(
                        { "data": values },
                        function(data) { gridLib.showServerPromtInDialog($("#"+dialogId), data["result"]); },
                        "/blankcontroller/editparams"
                    );

                },
                "Отмена": function() {
                    $(this).empty();
                    $(this).dialog("close");
                },
            }
        });

        return true;
    }

    function ExportDataLoad(data, dialogId) {
        console.log("ExportDataLoad: ", data);

        for (var i = 0; i < data.length; ++i) {
            var f_id = data[i]["form_id"];
            var p_id = data[i]["param_id"];
            var p_v = data[i]["value"];

            $("#"+dialogId+" #params-"+f_id +" table #export-param-"+p_id+" input").remove();
            $("#"+dialogId+" #params-"+f_id +" table #export-val-"+p_id+" p").remove();
            if (data[i]["value"] != "") {
                $("#"+dialogId+" #params-"+f_id +" table #export-val-"+p_id).append(drawParam(data[i], 0, false));
                $("
").appendTo("#"+dialogId+" #params-"+f_id+" table #export-param-"+p_id);
                $("<input/>", {
                    "id": "export-btn-"+f_id+"-"+p_id,
                    "type": "button",
                    "value": "←",
                    "data-event-type-id": f_id,
                    "data-param-id": p_id,
                    "data-param-val": p_v,
                }).appendTo("#"+dialogId+" #params-"+f_id+" table #export-param-"+p_id);

                $("#export-btn-"+f_id+"-"+p_id).click(function() {
                    var f_id = $(this).attr("data-event-type-id");
                    var p_id = $(this).attr("data-param-id");
                    var p_v = $(this).attr("data-param-val");
                    $("#"+dialogId+" #params-"+f_id+" table #"+p_id).val(p_v);
                });
            }
        }
    }

    function SetEditHistoryData(data, dialogId) {
        console.log("SetEditHistoryData: ", data);

        for (var i = 0; i < data.length; ++i) {
            var f_id = data[i]["form_id"];
            var p_id = data[i]["param_id"];
            var p_v = data[i]["edit_date"].replace(/[T,Z]/g, " ")+" - "+data[i]["login"];

            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").remove();
            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id).append($("
"));
            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").append($("
"));
            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").append($("
", {text: p_v}));
        }
    }

    function ClearEditHistoryData(data, dialogId) {
        console.log("SetEditHistoryData: ", data);

        for (var i = 0; i < data.length; ++i) {
            var f_id = data[i]["form_id"];
            var p_id = data[i]["param_id"];

            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").remove();
        }
    }

    return {
        drawParam: drawParam,
        getFormData: getFormData,
        getListHistoryEvents: getListHistoryEvents,
        ShowBlank: ShowBlank,
        ShowPersonBlank: ShowPersonBlank,
        ShowPersonBlankFromGroup: ShowPersonBlankFromGroup,
        ShowServerAns: ShowServerAns,
    };

});
<commit_msg>blank.js: add checking for edit data<commit_after>define(["utils", "grid_lib", "datepicker/datepicker", "kladr/kladr"],
function(utils, gridLib, datepicker, kladr) {

    function drawParam(data, for_saving, admin) {
        console.log("drawParam");

        var block;

        if (data["type"] === "region"
            || data["type"] === "district"
            || data["type"] === "city"
            || data["type"] === "street"
            || data["type"] === "building"
            || data["type"] === "text"
            || data["type"] === "email"
            || data["type"] === "password"
            || data["type"] === "phon") {
            block = $("<input/>", {type: data["type"]});

        } else if (data["type"] === "textarea") {
            block = $("<textarea/>", {});

        } else if (data["type"] === "date") {
            block = $("<input/>", {type: "date"});
            datepicker.initDatePicker(block);
        }

        block.attr("id", data["param_id"]);
        block.attr("for-saving", for_saving);
        block.attr("name", data["param_name"]);

        if (data["value"]) {
            block.val(data["value"]);
            block.attr("param_val_id", data["param_val_id"]);
        }

        if (data["required"]) {
            block.attr("required", true);
        }

        var lable = $("", {
            text: data["param_name"],
        });

        if (!data["editable"] && !admin) {
            block.attr("readonly", true);
        }

        return $("
").append(lable).append(block);
    }

    function getFormData(name) {
        console.log("getFormData");

        var values = [];
        var empty = false;
        var pattern = /^[ \t\v\r\n\f]{0,}$/;
        var data = $("#"+name+" [for-saving=true]");
        console.log(data);

        for (var i = 0; i < data.length; ++i) {
            var elem = data[i];
            if (pattern.test($(elem).val()) && $(elem).attr("required")) {
                alert("Поле '" + $(elem).attr("name") + "' обязательное к заполнению.");
                empty = true;
                break;
            }

            values.push({
                "value": $(elem).val(),
                "param_val_id": $(elem).attr("param_val_id"),
                "id": $(elem).attr("id"),
            });
        };

        return empty ? false : values;
    }

    function getListHistoryEvents(historyDiv, F_ids) {
        console.log("getListHistoryEvents: F_ids: ", F_ids);

        utils.postRequest(
            { "form_ids": F_ids, },
            function(response) {
                console.log("getListHistoryEvents: ", response);

                if (response["data"]) {
                    var data = response["data"];
                    for (var i = 0; i < data.length; ++i) {
                        $("#"+historyDiv+" select").append($("<option></option>")
                            .attr("value", data[i]["id"])
                            .text(data[i]["name"])
                        );
                    }
                    $("#"+historyDiv).show();
                }
            },
            "/blankcontroller/getlisthistoryevents"
        );
    }

    function ShowPersonBlankFromGroup(groupRegId, faceId, dialogId) {
        console.log("ShowPersonBlankFromGroup");

        var data = { "group_reg_id": groupRegId, "face_id": faceId };
        console.log("ShowPersonBlankFromGroup: ", data);

        if (!groupRegId || !faceId) {
            return false;
        }

        $("#"+dialogId).empty();

        utils.postRequest(
            data,
            function(data) {
                ShowBlank(data["data"], dialogId, data["role"]);
                $("#"+dialogId+" #history").hide();
            },
            "/blankcontroller/getpersonrequestfromgroup"
        );

        $("#"+dialogId).dialog({
            modal: true,
            toTop: "150",
            height: "auto",
            width: "auto",
            buttons: {
                "Сохранить изменения": function() {
                    var values = getFormData(dialogId+" div");
                    if (!values) {
                        console.log("Не все поля заполнены.");
                        return false;
                    }

                    utils.postRequest(
                        { "data": values },
                        function(data) { gridLib.showServerPromtInDialog($("#"+dialogId), data["result"]); },
                        "/blankcontroller/editparams"
                    );
                },
                "Отмена": function() {
                    $(this).empty();
                    $(this).dialog("close");
                },
            }
        });

        return true;
    }

    function ShowBlank(d, dialogId, role, regId) {
        console.log("ShowBlank data: ", d);
        console.log("ShowBlank role: ", role);

        if (d.length == 0) {
            return false;
        }

        var history = $("
", {id: "history"})
            .append($("", {text: "Ранее заполненные анкеты"})).append("
")
            .append($("<select/>", {}))
            .append($("<input/>", {type: "button", value: "выбрать", id: "send-btn", name: "submit"}));

        $("#"+dialogId).append(history);

        if (regId) {
            $("#"+dialogId)
                .append($("<input/>", {type: "checkbox", id: "edit-history-box", width: "auto"}))
                .append($("", {id: "edit-history", style: "display:inline;"}).text("Информация о редактировании полей"));

            $("#"+dialogId+" #edit-history-box").change(function() {
                console.log("ShowBlank: ", { "reg_id": regId });
                utils.postRequest(
                    { "reg_id": regId },
                    function(response) {
                        if (response["result"] !== "ok") {
                            ShowServerAns(-1, response, "now #server-answer");
                            return false;
                        }

                        if ($("#"+dialogId+" #edit-history-box").is(":checked")) {
                            SetEditHistoryData(response["data"], dialogId);
                        } else {
                            ClearEditHistoryData(response["data"], dialogId);
                        }
                    },
                    "/blankcontroller/getedithistorydata"
                );
            });
        }

        $("#"+dialogId).append($("
")).append($("
"));

        var F_ids = {};

        $("#" + dialogId + " h1").text(d[0]["event_name"]);

        var div_forms = $("
", {id: "event-" + d[0]["event_id"]});
        var ul_forms = $("
", {});

        $(div_forms).append(ul_forms);
        $(div_forms).appendTo("#" + dialogId + " div");

        F_ids["form_id"] = [];

        for (i = 0; i < d.length; ++i) {
            if ($("#" + dialogId +" div#form-" + d[i]["form_id"]).attr("id") == undefined) {
                var li_form = $("
", {});
                var a_form = $("", {href: "#" + "form-" + d[i]["form_id"]}).text(d[i]["form_name"]);

                $(li_form).append(a_form);
                $(ul_forms).append(li_form);

                var div_tab_form = $("
", {id: "form-" + d[i]["form_id"]});
                $(div_forms).append(div_tab_form);

                F_ids["form_id"].push(parseInt(d[i]["form_id"]));

                var div_params = $("
", {id: "params-" + d[i]["form_id"]});
                $(div_tab_form).append(div_params);

                var table = $("
");
                div_params.append(table);

            }

            var tr = $("
").appendTo($("#" + dialogId +" div#form-" + d[i]["form_id"] + " table"));
            var td_1 = $("").appendTo(tr);
            $(td_1).append(drawParam(d[i], true, role));
            tr.append($("", {id: "export-param-"+d[i]["param_id"]}));
            tr.append($("", {id: "export-val-"+d[i]["param_id"]}));
            tr.append($("", {id: "export-edit-history-"+d[i]["param_id"]}));
        }

        $("#" + dialogId + " #" + "event-" + d[0]["event_id"]).tabs();

        console.log("F_ids: ", F_ids);

        $("#"+dialogId+" #history #send-btn").click(function() {
            utils.postRequest(
                {
                    "event_id": $("#"+dialogId+" #history select").find(":selected").attr("value")
                },
                function(response) {
                    if (response["result"] !== "ok") {
                        ShowServerAns(-1, response, "now #server-answer");
                        return false;
                    }
                    ExportDataLoad(response["data"], dialogId);
                },
                "/blankcontroller/gethistoryrequest"
            );
        });

        kladr.kladr();

        return F_ids;
    }

    function ShowServerAns(event_id, data, responseId) {
        console.log("ShowServerAns");

        if (data.result === "ok") {
            var msg = "Запрос успешно выполнен. ";
            if (event_id != 1) {
                msg += "Ваша заявка на участие будет рассмотрена.";
            } else {
                msg += "На вашу электронную почту было отправлено письмо, содержащее ссылку для подтверждения регистрации. "
                + "Воспользуйтесь этой ссылкой, для продолжения работы.";
            }
            $("#"+responseId).text(msg).css("color", "green");

        } else if (data.result === "loginExists") {
            $("#"+responseId).text("Такой логин уже существует.").css("color", "red");

        } else if (data.result === "badLogin") {
            $("#"+responseId).text("Логин может содержать латинские буквы и/или "
                + "цифры и иметь длину от 2 до 36 символов.").css("color", "red");

        } else if (data.result === "badPassword") {
            $("#"+responseId).text("Пароль должен иметь длину от 6 "
                + "до 36 символов.").css("color", "red");

        } else if (data.result === "Unauthorized") {
            $("#"+responseId).text("Пользователь не авторизован.").css("color", "red");

        } else if (data.result === "authorized") {
            $("#"+responseId).text("Пользователь уже авторизован.").css("color", "red");

        } else if (data.result === "badEmail") {
            $("#"+responseId).text("Проверьте правильность введенного Вами email.").css("color", "red");

        } else {
            $("#"+responseId).text(data.result).css("color", "red");
        }
    }

    function ShowPersonBlank(dialogId, regId) {
        console.log("ShowPersonBlank");

        console.log("reg_id", regId)

        $("#"+dialogId).empty();

        utils.postRequest(
            { "reg_id": regId },
            function(data) {
                var f_ids = ShowBlank(data["data"], dialogId, data["role"], regId);
                if (!f_ids) {
                    return false;
                }
                getListHistoryEvents(dialogId+" #history", f_ids);
            },
            "/blankcontroller/getpersonrequest"
        );

        $("#"+dialogId).dialog({
            modal: true,
            toTop: "150",
            height: "auto",
            width: "auto",
            buttons: {
                "Сохранить изменения": function() {
                    var values = getFormData(dialogId+" div");
                    if (values == false) {
                        console.log("Не все поля заполнены.");
                        alert("Не все поля заполнены.");
                        return false;
                    }

                    utils.postRequest(
                        { "data": values },
                        function(data) { gridLib.showServerPromtInDialog($("#"+dialogId), data["result"]); },
                        "/blankcontroller/editparams"
                    );

                },
                "Отмена": function() {
                    $(this).empty();
                    $(this).dialog("close");
                },
            }
        });

        return true;
    }

    function ExportDataLoad(data, dialogId) {
        console.log("ExportDataLoad: ", data);

        for (var i = 0; i < data.length; ++i) {
            var f_id = data[i]["form_id"];
            var p_id = data[i]["param_id"];
            var p_v = data[i]["value"];

            $("#"+dialogId+" #params-"+f_id +" table #export-param-"+p_id+" input").remove();
            $("#"+dialogId+" #params-"+f_id +" table #export-val-"+p_id+" p").remove();
            if (data[i]["value"] != "") {
                $("#"+dialogId+" #params-"+f_id +" table #export-val-"+p_id).append(drawParam(data[i], 0, false));
                $("
").appendTo("#"+dialogId+" #params-"+f_id+" table #export-param-"+p_id);
                $("<input/>", {
                    "id": "export-btn-"+f_id+"-"+p_id,
                    "type": "button",
                    "value": "←",
                    "data-event-type-id": f_id,
                    "data-param-id": p_id,
                    "data-param-val": p_v,
                }).appendTo("#"+dialogId+" #params-"+f_id+" table #export-param-"+p_id);

                $("#export-btn-"+f_id+"-"+p_id).click(function() {
                    var f_id = $(this).attr("data-event-type-id");
                    var p_id = $(this).attr("data-param-id");
                    var p_v = $(this).attr("data-param-val");
                    $("#"+dialogId+" #params-"+f_id+" table #"+p_id).val(p_v);
                });
            }
        }
    }

    function SetEditHistoryData(data, dialogId) {
        console.log("SetEditHistoryData: ", data);

        for (var i = 0; i < data.length; ++i) {
            var f_id = data[i]["form_id"];
            var p_id = data[i]["param_id"];
            var p_v = data[i]["edit_date"] ? data[i]["edit_date"].replace(/[T,Z]/g, " ")+" - "+data[i]["login"] : data[i]["login"];

            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").remove();
            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id).append($("
"));
            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").append($("
"));
            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").append($("
", {text: p_v}));
        }
    }

    function ClearEditHistoryData(data, dialogId) {
        console.log("ClearEditHistoryData: ", data);

        for (var i = 0; i < data.length; ++i) {
            var f_id = data[i]["form_id"];
            var p_id = data[i]["param_id"];

            $("#"+dialogId+" #params-"+f_id +" table #export-edit-history-"+p_id+" div").remove();
        }
    }

    return {
        drawParam: drawParam,
        getFormData: getFormData,
        getListHistoryEvents: getListHistoryEvents,
        ShowBlank: ShowBlank,
        ShowPersonBlank: ShowPersonBlank,
        ShowPersonBlankFromGroup: ShowPersonBlankFromGroup,
        ShowServerAns: ShowServerAns,
    };

});
<|endoftext|>


<commit_before>$(document).ready(function () {

    var parse_str = function (str) {
        return str.replace(new RegExp('\r?\n', 'g'), '
');
    };

    var create_questions = function (questions_data) {
        var questions = [];

        for (var index = 0; index < questions_data.length; index++) {
            var question_data = questions_data[index];
            var options = create_options(question_data['options']);
            var question = new Question(question_data['question'], options, question_data['explain']);
            questions.push(question);
        }

        return questions;
    };

    var create_options = function (options_data) {
        var options = [];

        for (var index = 0; index < options_data.length; index++) {
            var option_data = options_data[index];
            var option = new QuestionOption(option_data.title, option_data.correct, false);
            options.push(option);
        }

        return options;
    };

    var update_questions_count = function (quiz) {
        $('.total-questions').text(quiz.getQuestionsCount());
    };

    var update_current_question_number = function (quiz) {
        $('.question-index').text(quiz.getCurrentQuestionNumber());
    };

    var update_question_html = function (quiz) {
        var question = quiz.getCurrentQuestion();

        $('.question-title').html(parse_str(question.title));
        $('.question-explain-text').html(parse_str(question.explain));
    };

    var disable_options = function (question) {
        var options_list = $('.options-list');
        var options = question.options;
        for (var index = 0; index < options.length; index++) {
            options_list.children().eq(index).find('.btn').addClass('disabled');
        }
    };

    var highlight_options = function (question) {
        var options_list = $('.options-list');
        var options = question.options;
        for (var index = 0; index < options.length; index++) {
            var option = options[index];
            if (option.selected && (false === option.correct)) {
                options_list.children().eq(index).find('.btn').addClass('btn-danger');
            }

            if (option.correct) {
                options_list.children().eq(index).find('.btn').addClass('btn-success');
            }
        }
    };

    var hide_show_explain = function (quiz) {
        var question = quiz.getCurrentQuestion();
        if (quiz.isAnswered(question) && (false === quiz.isCorrect(question))) {
            $('.question-explain').show();
        } else {
            $('.question-explain').hide();
        }
    };

    var get_first_not_answered_index = function (quiz) {
        var questions = quiz.getQuestions();

        for (var index = 0; index < questions.length; index++) {
            var question = questions[index];

            if (false === quiz.isAnswered(question)) {
                return index;
            }
        }

        return null;
    };

    var answered_correctly = function (quiz) {
        var questions = quiz.getQuestions();
        var correct = 0;
        for (var index = 0; index < questions.length; index++) {
            var question = questions[index];

            if (quiz.isCorrect(question)) {
                correct++;
            }
        }

        return correct;
    };

    var score_shown = false;

    var show_score = function (quiz) {
        score_shown = true;

        var correct = answered_correctly(quiz);
        var total = quiz.getQuestionsCount();
        var percent = (correct * 100 / total);
        var percent_fixed = Math.round(percent * 100) / 100;

        $('.results .percent').text(percent_fixed);
        $('.results .correct').text(correct);
        $('.results .progress-bar').css('width', percent_fixed + '%');

        $('.quiz').hide();
        $('.results').show();
    };

    var next_action = function (quiz) {
        var index = get_first_not_answered_index(quiz);

        if (quiz.getCurrentQuestionNumber() === quiz.getQuestionsCount()) {
            if (null !== index) {
                quiz.goTo(index);
            } else {
                show_score(quiz);
            }
        } else {
            if ((null === index) && (false === score_shown)) {
                show_score(quiz);
            } else {
                quiz.next();
            }
        }
    };

    var click_answer_disabled = false;

    var click_answer = function (quiz, index, question) {
        return function () {
            if (!click_answer_disabled) {
                click_answer_disabled = true;
                if (0 === $(this).find('.disabled').length) {
                    var answers = [index];

                    quiz.setAnswers(quiz.getCurrentQuestion(), answers);

                    highlight_options(question);
                    disable_options(question);

                    if (quiz.isCorrect(question)) {
                        setTimeout(function () {
                            click_answer_disabled = false;
                            next_action(quiz);
                        }, 500);
                    } else {
                        click_answer_disabled = false;
                    }
                } else {
                    click_answer_disabled = false;
                    next_action(quiz);
                }
            }
        }
    };

    var update_question_options_html = function (quiz) {
        var question = quiz.getCurrentQuestion();
        var options = question.options;

        var options_list = $('.options-list');
        var option_template = options_list.data('option');

        for (var index = 0; index < options.length; index++) {
            var option = options[index];
            var $option = $(option_template.replace('{{ title }}', option.title));

            if (quiz.isAnswered(quiz.getCurrentQuestion())) {
                $option.find('.btn').addClass('disabled');
            }

            $option.click(click_answer(quiz, index, question));

            options_list.append($option);
        }

        if (quiz.isAnswered(question)) {
            highlight_options(question);
        }
    };

    var clear_options = function () {
        $('.options-list').html('');
    };

    var load_quiz = function(data_path) {
        $.getJSON(data_path, function (data) {

            var questions = create_questions(data['questions']);

            var quiz = new Quiz();

            quiz.addPreLoadCallback(clear_options);

            quiz.addPostLoadCallback(update_questions_count);
            quiz.addPostLoadCallback(update_current_question_number);

            quiz.addPreChangeQuestionCallback(clear_options);

            quiz.addPostSetAnswersCallback(hide_show_explain);

            quiz.addPostChangeQuestionCallback(update_current_question_number);
            quiz.addPostChangeQuestionCallback(update_question_html);
            quiz.addPostChangeQuestionCallback(update_question_options_html);
            quiz.addPostChangeQuestionCallback(hide_show_explain);

            quiz.load(questions);

            $('.quiz .next').click(function () {
                next_action(quiz)
            });
            $('.quiz .previous').click(quiz.prev);
        });
    };

    $('.results').hide();
    $('.quiz').hide();

    $('.start .btn').click(function () {
        $('.start').hide();
        var data_file = $("select[name='data']:first").val();
        load_quiz("data/" + data_file);
        $(".quiz").show();
    });

    $('.results .previous').click(function () {
        $('.results').hide();
        $('.quiz').show();
    });
});<commit_msg>Add randomization of quiz questions and question options<commit_after>$(document).ready(function () {

    var parse_str = function (str) {
        return str.replace(new RegExp('\r?\n', 'g'), '
');
    };

    var shuffle = function (array) {
        return array.sort(function () { return 0.5 - Math.random(); });
    };

    var create_questions = function (questions_data) {
        var questions = [];

        for (var index = 0; index < questions_data.length; index++) {
            var question_data = questions_data[index];
            var options = shuffle(create_options(question_data['options']));
            var question = new Question(question_data['question'], options, question_data['explain']);
            questions.push(question);
        }

        return shuffle(questions);
    };

    var create_options = function (options_data) {
        var options = [];

        for (var index = 0; index < options_data.length; index++) {
            var option_data = options_data[index];
            var option = new QuestionOption(option_data.title, option_data.correct, false);
            options.push(option);
        }

        return options;
    };

    var update_questions_count = function (quiz) {
        $('.total-questions').text(quiz.getQuestionsCount());
    };

    var update_current_question_number = function (quiz) {
        $('.question-index').text(quiz.getCurrentQuestionNumber());
    };

    var update_question_html = function (quiz) {
        var question = quiz.getCurrentQuestion();

        $('.question-title').html(parse_str(question.title));
        $('.question-explain-text').html(parse_str(question.explain));
    };

    var disable_options = function (question) {
        var options_list = $('.options-list');
        var options = question.options;
        for (var index = 0; index < options.length; index++) {
            options_list.children().eq(index).find('.btn').addClass('disabled');
        }
    };

    var highlight_options = function (question) {
        var options_list = $('.options-list');
        var options = question.options;
        for (var index = 0; index < options.length; index++) {
            var option = options[index];
            if (option.selected && (false === option.correct)) {
                options_list.children().eq(index).find('.btn').addClass('btn-danger');
            }

            if (option.correct) {
                options_list.children().eq(index).find('.btn').addClass('btn-success');
            }
        }
    };

    var hide_show_explain = function (quiz) {
        var question = quiz.getCurrentQuestion();
        if (quiz.isAnswered(question) && (false === quiz.isCorrect(question))) {
            $('.question-explain').show();
        } else {
            $('.question-explain').hide();
        }
    };

    var get_first_not_answered_index = function (quiz) {
        var questions = quiz.getQuestions();

        for (var index = 0; index < questions.length; index++) {
            var question = questions[index];

            if (false === quiz.isAnswered(question)) {
                return index;
            }
        }

        return null;
    };

    var answered_correctly = function (quiz) {
        var questions = quiz.getQuestions();
        var correct = 0;
        for (var index = 0; index < questions.length; index++) {
            var question = questions[index];

            if (quiz.isCorrect(question)) {
                correct++;
            }
        }

        return correct;
    };

    var score_shown = false;

    var show_score = function (quiz) {
        score_shown = true;

        var correct = answered_correctly(quiz);
        var total = quiz.getQuestionsCount();
        var percent = (correct * 100 / total);
        var percent_fixed = Math.round(percent * 100) / 100;

        $('.results .percent').text(percent_fixed);
        $('.results .correct').text(correct);
        $('.results .progress-bar').css('width', percent_fixed + '%');

        $('.quiz').hide();
        $('.results').show();
    };

    var next_action = function (quiz) {
        var index = get_first_not_answered_index(quiz);

        if (quiz.getCurrentQuestionNumber() === quiz.getQuestionsCount()) {
            if (null !== index) {
                quiz.goTo(index);
            } else {
                show_score(quiz);
            }
        } else {
            if ((null === index) && (false === score_shown)) {
                show_score(quiz);
            } else {
                quiz.next();
            }
        }
    };

    var click_answer_disabled = false;

    var click_answer = function (quiz, index, question) {
        return function () {
            if (!click_answer_disabled) {
                click_answer_disabled = true;
                if (0 === $(this).find('.disabled').length) {
                    var answers = [index];

                    quiz.setAnswers(quiz.getCurrentQuestion(), answers);

                    highlight_options(question);
                    disable_options(question);

                    if (quiz.isCorrect(question)) {
                        setTimeout(function () {
                            click_answer_disabled = false;
                            next_action(quiz);
                        }, 500);
                    } else {
                        click_answer_disabled = false;
                    }
                } else {
                    click_answer_disabled = false;
                    next_action(quiz);
                }
            }
        }
    };

    var update_question_options_html = function (quiz) {
        var question = quiz.getCurrentQuestion();
        var options = question.options;

        var options_list = $('.options-list');
        var option_template = options_list.data('option');

        for (var index = 0; index < options.length; index++) {
            var option = options[index];
            var $option = $(option_template.replace('{{ title }}', option.title));

            if (quiz.isAnswered(quiz.getCurrentQuestion())) {
                $option.find('.btn').addClass('disabled');
            }

            $option.click(click_answer(quiz, index, question));

            options_list.append($option);
        }

        if (quiz.isAnswered(question)) {
            highlight_options(question);
        }
    };

    var clear_options = function () {
        $('.options-list').html('');
    };

    var load_quiz = function(data_path) {
        $.getJSON(data_path, function (data) {

            var questions = create_questions(data['questions']);

            var quiz = new Quiz();

            quiz.addPreLoadCallback(clear_options);

            quiz.addPostLoadCallback(update_questions_count);
            quiz.addPostLoadCallback(update_current_question_number);

            quiz.addPreChangeQuestionCallback(clear_options);

            quiz.addPostSetAnswersCallback(hide_show_explain);

            quiz.addPostChangeQuestionCallback(update_current_question_number);
            quiz.addPostChangeQuestionCallback(update_question_html);
            quiz.addPostChangeQuestionCallback(update_question_options_html);
            quiz.addPostChangeQuestionCallback(hide_show_explain);

            quiz.load(questions);

            $('.quiz .next').click(function () {
                next_action(quiz)
            });
            $('.quiz .previous').click(quiz.prev);
        });
    };

    $('.results').hide();
    $('.quiz').hide();

    $('.start .btn').click(function () {
        $('.start').hide();
        var data_file = $("select[name='data']:first").val();
        load_quiz("data/" + data_file);
        $(".quiz").show();
    });

    $('.results .previous').click(function () {
        $('.results').hide();
        $('.quiz').show();
    });
});
<|endoftext|>


<commit_before>
function renderChartForUri(uri) {

  var chart;
  var selector = '.markdown-chart'; //'#' + uri.replace(/\//g, '\\/');
  var $graphic = $(selector);
  var pymChild = new pym.Child({});

  function drawGraphic() {
    var chartWidth = $graphic.width(); //- margin.left - margin.right;
    var chartHeight = chartWidth * chart.aspectRatio;
    renderChartObject('.markdown-chart', chart, chartHeight, chartWidth);
    pymChild.sendHeight();
  }

  var dataUri = uri + "/data";
  if (dataUri.indexOf('/') !== 0) {
    dataUri = '/' + dataUri;
  }

  $.ajax({
    url: dataUri,
    type: "GET",
    success: function (data) {
      chart = data;
      drawGraphic();
      window.onresize = drawGraphic;
    }
  });
}

// Do the rendering
function renderChartObject(bindTag, chart, chartHeight, chartWidth) {

  // Create our svg
  var svg = d3.select(bindTag + " svg")
    .attr("viewBox", "0 0 " + chartWidth + " " + chartHeight)
    .attr("preserveAspectRatio", "xMinYMin meet");

   //If we are talking time series skip
  if (chart.isTimeSeries && (chart.chartType == 'line')) {
    renderTimeseriesChartObject(bindTag, chart, chartWidth, chartHeight);
    setFontStyle();
    renderChartUnit();
    return;
  }

  // Calculate padding at top (and left) of SVG
  var types = chart.chartType === 'barline' ? chart.chartTypes : {};
  var groups = chart.chartType === 'barline' ? chart.groups : [];
  var type = checkType(chart);
  var rotate = chart.chartType === 'rotated';
  var yLabel = rotate === true ? chart.unit : '';

  // work out position for chart legend
  var seriesCount = chart.series.length;
  var yOffset = (chart.legend == 'bottom-left' || chart.legend == 'bottom-right') ? seriesCount * 20 + 10 : 5;

  var culledLabels = {};
  var labelRotate = 0;
  var labelInterval = chart.labelInterval;
  _.each(chart.data, function (data_point) {
    if( labelInterval == null ) {
      culledLabels[data_point[chart.headers[0]]] = data_point[chart.headers[0]];
    } else {
      if(labelRotate === 0) {
        culledLabels[data_point[chart.headers[0]]] = data_point[chart.headers[0]];
      } else {
        culledLabels[data_point[chart.headers[0]]] = "";
      }
      labelRotate = (labelRotate + 1) % labelInterval;
    }
  });

  // Generate the chart
  var c3Config = {
    bindto: bindTag,
    size: {
      height: chartHeight,
      width: chartWidth
    },
    data: {
      json: chart.data,
      keys: {
        value: chart.series
      },
      type: type,
      types: types,
      groups: groups,
      colors: getColours(chart.series)
    },
    legend: {
      hide: chart.hideLegend,
      position: 'inset',
      inset: {
        anchor: chart.legend,
        x: 10,
        y: yOffset
      },
      title: chart.title,
      subTitle: chart.subtitle
    },
    axis: {
      x: {
        label: chart.xaxis,
        type: 'category',
        categories: chart.categories,
        tick: {
          format: function (x) {
            if( labelInterval == null ) {
              return chart.data[x][chart.headers[0]];
            } else {
              if(x % labelInterval === 0) {
                return chart.data[x][chart.headers[0]];
              } else {
                return "";
              }
            }
          }
        }
      },
      y: {
        label: yLabel,
        position: 'outer-top'
      },
      rotated: rotate
    },
    grid: {
      y: {
        show: true
      }
    },
    tooltip: {
        format: {
          value: function (value, ratio, id, index) {
             if(chart.decimalPlaces == null) {
               return value;
             } else {
             // This line ensures rounding to certain decimal places
               return parseFloat(Math.round(value * Math.pow(10,chart.decimalPlaces)) / Math.pow(10,chart.decimalPlaces)).toFixed(chart.decimalPlaces);
             }
           },
           // This line ensures all data labels are displayed in tooltips when ticks are culled
           title: function (name, ratio, id, index) { return chart.data[name][chart.headers[0]]; }
        }
      }
  };

  c3.generate(c3Config);
  setFontStyle();
  renderChartUnit();

  function setFontStyle() {
    var chartText = d3.select(bindTag + ' svg');
    chartText.style('font-size', '12px')
      .style('font-family', '"Open Sans", sans-serif')
      .style('fill', '#000000');
  }

  function renderChartUnit() {

    var svg = d3.select(bindTag + ' svg');
    var headerGroup = svg.append('g');
    var chartGroup = d3.select('g');

    var transform = chartGroup.attr("transform");
    var chartXOffset = 0;

    if (typeof transform !== 'undefined') {
      var splitParts = transform.split(",");
      chartXOffset = ~~splitParts [0].split("(")[1];
    }
    if (chart.unit && !rotate) {
      headerGroup.append('text') // Unit (if non rotated)
        .attr("transform", "translate(" + (chartXOffset + 10) + "," + 15 + ")")
        .style('font-size', '12px')
        .style('font-family', '"Open Sans", sans-serif')
        .style('fill', '#000000')
        .text(chart.unit);
    }
  }

  function getColours(series) {

    var availableColours = ['#274796','#F5942F','#E73F40','#7BCAE2', '#979796', '#E9E117', '#74B630', '#674796', '#BD5B9E'];
    var colours = {};

    $.each(series, function(index, series) {
      colours[series] = availableColours[index];
    });

    return colours;
  }

  function checkType(chart) {
    if (chart.chartType === 'rotated') {
      type = 'bar';
      return type;
    } else if (chart.chartType === 'barline') {
      type = 'bar';
      return type;
    } else {
      return type = chart.chartType;
    }
  }

  function renderTimeseriesChartObject(bindTag, timechart, chartWidth, chartHeight) {
    var padding = 25;
    var chart = timechart; //timeSubchart(timechart, period);

    // Create a dictionary so we can reverse lookup a tooltip label
    var dates_to_label = {};
    _.each(chart.timeSeries, function (data_point) {
      data_point.date = new Date(data_point.date);
      dates_to_label[data_point.date] = data_point.label;
    });

    // make room for titles if necessary
    if (chart.subtitle != '') {
      padding += 16;
    }
    if (chart.unit != '') {
      padding += 24;
    }

    // should we show
    var showPoints = true;
    if (chart.data.length > 100) {
      showPoints = false;
    }

    // work out position for chart legend
    var seriesCount = chart.series.length;
    var yOffset = (chart.legend == 'bottom-left' || chart.legend == 'bottom-right') ? seriesCount * 20 + 10 : 5;

    // refers to the issue of time axes not being applicable to non continuous charts
    var axisType;
    var keys;

    if (chart.chartType == 'line') { // continuous line charts
      axisType = {
        label: chart.xaxis,
        type: 'timeseries',
      }

      var monthsOnTimeline = (chart.timeSeries[chart.timeSeries.length - 1].date - chart.timeSeries[0].date) / (1000 * 60 * 60 * 24 * 30);
      var tick = {
        format: function (x) {
          return x.getFullYear();
        }
      }
      if (monthsOnTimeline <= 24.5) {
        tick = {
          format: function (x) {
            return formattedMonthYear(x);
          }
        }
      }

      axisType.tick = tick;
      keys = {
        x: 'date',
        value: chart.series
      }
    } else { // bar charts and other
      axisType = {
        label: chart.xaxis,
        type: 'category',
        categories: chart.categories
      }
      keys = {
        x: 'label',
        value: chart.series
      }
    }

    c3.generate({
      bindto: bindTag,
      size: {
        height: chartHeight,
        width: chartWidth
      },
      padding: {
        right: 15
      },
      data: {
        json: chart.timeSeries,
        keys: keys,
        type: chart.chartType,
        xFormat: '%Y-%m-%d %H:%M:%S',
        colors: getColours(chart.series)
      },

      point: {
        show: showPoints
      },

      legend: {
        hide: chart.hideLegend,
        position: 'inset',
        inset: {
          anchor: chart.legend,
          x: 10,
          y: yOffset
        }
      },

      axis: {
        x: axisType
      },
      tooltip: {
        format: {
          title: function (x) {
            return dates_to_label[x];
          }
        }
      },
      grid: {
        y: {
          show: true
        }
      },
       tooltip: {
         format: {
           value: function (value, ratio, id, index) {
              if(chart.decimalPlaces == null) {
                return value;
              } else {
                return parseFloat(Math.round(value * Math.pow(10,chart.decimalPlaces)) / Math.pow(10,chart.decimalPlaces)).toFixed(chart.decimalPlaces);
              }
            }
         }
       }
    });
  }

  function formattedMonthYear(date) {
    var monthNames = [
      "Jan", "Feb", "Mar",
      "Apr", "May", "Jun", "Jul",
      "Aug", "Sep", "Oct",
      "Nov", "Dec"];

    var monthIndex = date.getMonth();
    var year = date.getFullYear();

    return monthNames[monthIndex] + " " + year;
  }
}

function renderSvgAnnotations(bindTag, chart, chartHeight, chartWidth) {

  var svg = d3.select(bindTag + ' svg');

  var svgGroups = $(bindTag + ' svg > g').get();
  var headerGroup = svg.append('g');

  //var chartHeight = chartGroup.node().getBBox().height
  // annotate
  var title = headerGroup.append('text') // Title
    .style('font-size', '20px')
    .style('font-family', '"DaxlinePro", sans-serif')
    .style('fill', '#000000')
    .text(chart.title);

  var currentYOffset = 8 + applyLineWrap(title, chartWidth);

  if (chart.subtitle != '') {
    var subtitle = headerGroup.append('text') // Subtitle
      .attr("transform", "translate(0," + currentYOffset + ")")
      .style('font-size', '15px')
      .style('font-family', '"Open Sans", sans-serif')
      .style('fill', '#999999')
      .text(chart.subtitle);

    currentYOffset += 8 + applyLineWrap(subtitle, chartWidth);
  }

  currentYOffset += 2;

  // offset all the existing top level groups. This includes the chart and the legend
  var arrayLength = svgGroups.length;
  for (var i = 0; i < arrayLength; i++) { // ignore the last group as we just added it
    var group = svgGroups[i];

    var transform = $(group).attr("transform");
    var xOffset = 0;
    var yOffset = 0;

    if (typeof transform !== 'undefined') {
      var splitParts = transform.split(",");
      xOffset = ~~splitParts [0].split("(")[1];
      yOffset = ~~splitParts [1].split("(")[1];
    }

    $(group).attr("transform", "translate(" + (xOffset) + "," + (currentYOffset + yOffset) + ")");
  }

  currentYOffset += chartHeight;

  if (chart.source != '') {
    var source = d3.select(bindTag + ' svg').append('text') // Source
      .attr("transform", "translate(" + chartWidth + "," + currentYOffset + ")")
      .attr('text-anchor', 'end')
      .style('font-size', '12px')
      .style('font-family', '"Open Sans", sans-serif')
      .style('fill', '#999999')
      .text(chart.source);

    currentYOffset += 5 + applyLineWrap(source, chartWidth);
  }

  // reset the max height property of the container div.
  // C3 seems to set this and it becomes a stale value after rendering annotations.
  $(bindTag + ' svg').attr('height', currentYOffset);
  $(bindTag).css('max-height', currentYOffset +'px');

  return currentYOffset;

  // apply word wrap if required on text we have inserted
  function applyLineWrap(text, width) {

    var wrappedHeight = 0;

    text.each(function() {
      var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.2,
        y = text.attr("y"),
        tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", lineHeight + "em");
      while (word = words.pop()) {
        line.push(word);
        tspan.text(line.join(" "));
        if (tspan.node().getComputedTextLength() > width) {
          line.pop();
          tspan.text(line.join(" "));
          line = [word];
          tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("y", ((++lineNumber + 1) * lineHeight) + "em").text(word);
        }
      }

      wrappedHeight = tspan.node().getBBox().height;
    });

    return wrappedHeight;
  }
}<commit_msg>Second fix to chart renderer<commit_after>
function renderChartForUri(uri) {

  var chart;
  var selector = '.markdown-chart'; //'#' + uri.replace(/\//g, '\\/');
  var $graphic = $(selector);
  var pymChild = new pym.Child({});

  function drawGraphic() {
    var chartWidth = $graphic.width(); //- margin.left - margin.right;
    var chartHeight = chartWidth * chart.aspectRatio;
    renderChartObject('.markdown-chart', chart, chartHeight, chartWidth);
    pymChild.sendHeight();
  }

  var dataUri = uri + "/data";
  if (dataUri.indexOf('/') !== 0) {
    dataUri = '/' + dataUri;
  }

  $.ajax({
    url: dataUri,
    type: "GET",
    success: function (data) {
      chart = data;
      drawGraphic();
      window.onresize = drawGraphic;
    }
  });
}

// Do the rendering
function renderChartObject(bindTag, chart, chartHeight, chartWidth) {

  // Create our svg
  var svg = d3.select(bindTag + " svg")
    .attr("viewBox", "0 0 " + chartWidth + " " + chartHeight)
    .attr("preserveAspectRatio", "xMinYMin meet");

   //If we are talking time series skip
  if (chart.isTimeSeries && (chart.chartType == 'line')) {
    renderTimeseriesChartObject(bindTag, chart, chartWidth, chartHeight);
    setFontStyle();
    renderChartUnit();
    return;
  }

  // Calculate padding at top (and left) of SVG
  var types = chart.chartType === 'barline' ? chart.chartTypes : {};
  var groups = chart.chartType === 'barline' ? chart.groups : [];
  var type = checkType(chart);
  var rotate = chart.chartType === 'rotated';
  var yLabel = rotate === true ? chart.unit : '';

  // work out position for chart legend
  var seriesCount = chart.series.length;
  var yOffset = (chart.legend == 'bottom-left' || chart.legend == 'bottom-right') ? seriesCount * 20 + 10 : 5;

  var culledLabels = {};
  var labelRotate = 0;
  var labelInterval = chart.labelInterval;
  _.each(chart.data, function (data_point) {
    if( labelInterval == null ) {
      culledLabels[data_point[chart.headers[0]]] = data_point[chart.headers[0]];
    } else {
      if(labelRotate === 0) {
        culledLabels[data_point[chart.headers[0]]] = data_point[chart.headers[0]];
      } else {
        culledLabels[data_point[chart.headers[0]]] = "";
      }
      labelRotate = (labelRotate + 1) % labelInterval;
    }
  });

  // Generate the chart
  var c3Config = {
    bindto: bindTag,
    size: {
      height: chartHeight,
      width: chartWidth
    },
    data: {
      json: chart.data,
      keys: {
        value: chart.series
      },
      type: type,
      types: types,
      groups: groups,
      colors: getColours(chart.series)
    },
    legend: {
      hide: chart.hideLegend,
      position: 'inset',
      inset: {
        anchor: chart.legend,
        x: 10,
        y: yOffset
      },
      title: chart.title,
      subTitle: chart.subtitle
    },
    axis: {
      x: {
        label: chart.xaxis,
        type: 'category',
        categories: chart.categories,
        tick: {
          format: function (x) {
            var data_point = chart.data[x];
            if(data_point == null) {
              return "";
            } else if( labelInterval == null ) {
              return chart.data[x][chart.headers[0]];
            } else {
              if(x % labelInterval === 0) {
                return chart.data[x][chart.headers[0]];
              } else {
                return "";
              }
            }
          }
        }
      },
      y: {
        label: yLabel,
        position: 'outer-top'
      },
      rotated: rotate
    },
    grid: {
      y: {
        show: true
      }
    },
    tooltip: {
        format: {
          value: function (value, ratio, id, index) {
             if(chart.decimalPlaces == null) {
               return value;
             } else {
             // This line ensures rounding to certain decimal places
               return parseFloat(Math.round(value * Math.pow(10,chart.decimalPlaces)) / Math.pow(10,chart.decimalPlaces)).toFixed(chart.decimalPlaces);
             }
           },
           // This line ensures all data labels are displayed in tooltips when ticks are culled
           title: function (name, ratio, id, index) { return chart.data[name][chart.headers[0]]; }
        }
      }
  };

  c3.generate(c3Config);
  setFontStyle();
  renderChartUnit();

  function setFontStyle() {
    var chartText = d3.select(bindTag + ' svg');
    chartText.style('font-size', '12px')
      .style('font-family', '"Open Sans", sans-serif')
      .style('fill', '#000000');
  }

  function renderChartUnit() {

    var svg = d3.select(bindTag + ' svg');
    var headerGroup = svg.append('g');
    var chartGroup = d3.select('g');

    var transform = chartGroup.attr("transform");
    var chartXOffset = 0;

    if (typeof transform !== 'undefined') {
      var splitParts = transform.split(",");
      chartXOffset = ~~splitParts [0].split("(")[1];
    }
    if (chart.unit && !rotate) {
      headerGroup.append('text') // Unit (if non rotated)
        .attr("transform", "translate(" + (chartXOffset + 10) + "," + 15 + ")")
        .style('font-size', '12px')
        .style('font-family', '"Open Sans", sans-serif')
        .style('fill', '#000000')
        .text(chart.unit);
    }
  }

  function getColours(series) {

    var availableColours = ['#274796','#F5942F','#E73F40','#7BCAE2', '#979796', '#E9E117', '#74B630', '#674796', '#BD5B9E'];
    var colours = {};

    $.each(series, function(index, series) {
      colours[series] = availableColours[index];
    });

    return colours;
  }

  function checkType(chart) {
    if (chart.chartType === 'rotated') {
      type = 'bar';
      return type;
    } else if (chart.chartType === 'barline') {
      type = 'bar';
      return type;
    } else {
      return type = chart.chartType;
    }
  }

  function renderTimeseriesChartObject(bindTag, timechart, chartWidth, chartHeight) {
    var padding = 25;
    var chart = timechart; //timeSubchart(timechart, period);

    // Create a dictionary so we can reverse lookup a tooltip label
    var dates_to_label = {};
    _.each(chart.timeSeries, function (data_point) {
      data_point.date = new Date(data_point.date);
      dates_to_label[data_point.date] = data_point.label;
    });

    // make room for titles if necessary
    if (chart.subtitle != '') {
      padding += 16;
    }
    if (chart.unit != '') {
      padding += 24;
    }

    // should we show
    var showPoints = true;
    if (chart.data.length > 100) {
      showPoints = false;
    }

    // work out position for chart legend
    var seriesCount = chart.series.length;
    var yOffset = (chart.legend == 'bottom-left' || chart.legend == 'bottom-right') ? seriesCount * 20 + 10 : 5;

    // refers to the issue of time axes not being applicable to non continuous charts
    var axisType;
    var keys;

    if (chart.chartType == 'line') { // continuous line charts
      axisType = {
        label: chart.xaxis,
        type: 'timeseries',
      }

      var monthsOnTimeline = (chart.timeSeries[chart.timeSeries.length - 1].date - chart.timeSeries[0].date) / (1000 * 60 * 60 * 24 * 30);
      var tick = {
        format: function (x) {
          return x.getFullYear();
        }
      }
      if (monthsOnTimeline <= 24.5) {
        tick = {
          format: function (x) {
            return formattedMonthYear(x);
          }
        }
      }

      axisType.tick = tick;
      keys = {
        x: 'date',
        value: chart.series
      }
    } else { // bar charts and other
      axisType = {
        label: chart.xaxis,
        type: 'category',
        categories: chart.categories
      }
      keys = {
        x: 'label',
        value: chart.series
      }
    }

    c3.generate({
      bindto: bindTag,
      size: {
        height: chartHeight,
        width: chartWidth
      },
      padding: {
        right: 15
      },
      data: {
        json: chart.timeSeries,
        keys: keys,
        type: chart.chartType,
        xFormat: '%Y-%m-%d %H:%M:%S',
        colors: getColours(chart.series)
      },

      point: {
        show: showPoints
      },

      legend: {
        hide: chart.hideLegend,
        position: 'inset',
        inset: {
          anchor: chart.legend,
          x: 10,
          y: yOffset
        }
      },

      axis: {
        x: axisType
      },
      tooltip: {
        format: {
          title: function (x) {
            return dates_to_label[x];
          }
        }
      },
      grid: {
        y: {
          show: true
        }
      },
       tooltip: {
         format: {
           value: function (value, ratio, id, index) {
              if(chart.decimalPlaces == null) {
                return value;
              } else {
                return parseFloat(Math.round(value * Math.pow(10,chart.decimalPlaces)) / Math.pow(10,chart.decimalPlaces)).toFixed(chart.decimalPlaces);
              }
            }
         }
       }
    });
  }

  function formattedMonthYear(date) {
    var monthNames = [
      "Jan", "Feb", "Mar",
      "Apr", "May", "Jun", "Jul",
      "Aug", "Sep", "Oct",
      "Nov", "Dec"];

    var monthIndex = date.getMonth();
    var year = date.getFullYear();

    return monthNames[monthIndex] + " " + year;
  }
}

function renderSvgAnnotations(bindTag, chart, chartHeight, chartWidth) {

  var svg = d3.select(bindTag + ' svg');

  var svgGroups = $(bindTag + ' svg > g').get();
  var headerGroup = svg.append('g');

  //var chartHeight = chartGroup.node().getBBox().height
  // annotate
  var title = headerGroup.append('text') // Title
    .style('font-size', '20px')
    .style('font-family', '"DaxlinePro", sans-serif')
    .style('fill', '#000000')
    .text(chart.title);

  var currentYOffset = 8 + applyLineWrap(title, chartWidth);

  if (chart.subtitle != '') {
    var subtitle = headerGroup.append('text') // Subtitle
      .attr("transform", "translate(0," + currentYOffset + ")")
      .style('font-size', '15px')
      .style('font-family', '"Open Sans", sans-serif')
      .style('fill', '#999999')
      .text(chart.subtitle);

    currentYOffset += 8 + applyLineWrap(subtitle, chartWidth);
  }

  currentYOffset += 2;

  // offset all the existing top level groups. This includes the chart and the legend
  var arrayLength = svgGroups.length;
  for (var i = 0; i < arrayLength; i++) { // ignore the last group as we just added it
    var group = svgGroups[i];

    var transform = $(group).attr("transform");
    var xOffset = 0;
    var yOffset = 0;

    if (typeof transform !== 'undefined') {
      var splitParts = transform.split(",");
      xOffset = ~~splitParts [0].split("(")[1];
      yOffset = ~~splitParts [1].split("(")[1];
    }

    $(group).attr("transform", "translate(" + (xOffset) + "," + (currentYOffset + yOffset) + ")");
  }

  currentYOffset += chartHeight;

  if (chart.source != '') {
    var source = d3.select(bindTag + ' svg').append('text') // Source
      .attr("transform", "translate(" + chartWidth + "," + currentYOffset + ")")
      .attr('text-anchor', 'end')
      .style('font-size', '12px')
      .style('font-family', '"Open Sans", sans-serif')
      .style('fill', '#999999')
      .text(chart.source);

    currentYOffset += 5 + applyLineWrap(source, chartWidth);
  }

  // reset the max height property of the container div.
  // C3 seems to set this and it becomes a stale value after rendering annotations.
  $(bindTag + ' svg').attr('height', currentYOffset);
  $(bindTag).css('max-height', currentYOffset +'px');

  return currentYOffset;

  // apply word wrap if required on text we have inserted
  function applyLineWrap(text, width) {

    var wrappedHeight = 0;

    text.each(function() {
      var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.2,
        y = text.attr("y"),
        tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", lineHeight + "em");
      while (word = words.pop()) {
        line.push(word);
        tspan.text(line.join(" "));
        if (tspan.node().getComputedTextLength() > width) {
          line.pop();
          tspan.text(line.join(" "));
          line = [word];
          tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("y", ((++lineNumber + 1) * lineHeight) + "em").text(word);
        }
      }

      wrappedHeight = tspan.node().getBBox().height;
    });

    return wrappedHeight;
  }
}<|endoftext|>