roqua/quby_engine

View on GitHub
app/assets/javascripts/quby/answers/validation.js

Summary

Maintainability
F
4 days
Test Coverage
(function() {
  window.skipValidations = false;
  var validationI = 0;
  var fail_vals = new Array();

  window.validatePanel = function(panel) {
    if(skipValidations){
        return true;
    }
    var failed = false;
    validationI = 0;
    panel.find(".error").addClass("hidden");
    panel.find(".errors").removeClass("errors");
    if (panel_validations[panel.attr("id")]) {
      var validations = panel_validations[panel.attr("id")];

      for (var question_key in validations) {
        if(validations[question_key].length == 0){
            continue;
        }

        var question_item = $("#answer_" + question_key + "_input").closest('.item');

        var depends_on = eval(question_item.attr("data-depends-on"));
        if(depends_on){
          var dep_inputs = $($.map(depends_on, function(key){
              // select options always count as hidden in chrome
              // so they would be excluded here without the extra add
              return $("#answer_"+key).not(":disabled, :hidden").add("option.select#answer_"+key).not(":disabled");
          }));
          if(!is_answered(dep_inputs) || dep_inputs.length == 0){
              continue;
          }
        }

        if(question_item.length == 0){
            question_item = panel.find("[data-for=" + question_key + "]");
        }

        var inputs = question_item.find("input, textarea, select");
        if (question_item.is('.slider')) {
            if (question_item.is('.hide'))
                continue;
            inputs = inputs.not(":disabled");
        } else {
            inputs = inputs.not(":disabled, :hidden")
        }


        var values = $.map(inputs, function(e) {
          return $(e).val()
        });

        fail_vals = new Array();

        for (var i in validations[question_key]) {
          var validation = validations[question_key][i];
          switch(validation.type) {
              case "requires_answer":
                  if (!is_answered(inputs)) {
                    pushFailVal(validation.type);
                  }
                  break;
              case "minimum":
                  if ($(inputs[0]).attr('class') === 'date') {
                    try {
                      if(allDateFieldsFilledIn(inputs)) {
                        var enteredDate = parsePartialDate(inputs);
                        var minimumDate = new Date(validation.value);
                        if(enteredDate < minimumDate) {
                          pushFailVal(validation.type);
                        }
                      }
                    } catch(e) {} // errors in date parsing are handled by valid_date
                  } else if (inputs.length == 1) {
                      var value = values[0];
                      if(value === undefined || value == ""){
                          continue;
                      }
                      if(parseFloat(value) < validation.value){
                          pushFailVal(validation.type);
                      }
                  }
                  break;
              case "maximum":
                  if ($(inputs[0]).attr('class') === 'date') {
                    try {
                      if(allDateFieldsFilledIn(inputs)) {
                        var enteredDate = parsePartialDate(inputs);
                        var maximumDate = new Date(validation.value)
                        if(enteredDate > maximumDate) {
                          pushFailVal(validation.type);
                        }
                      }
                    } catch(e) {}
                  } else if (inputs.length == 1) {
                      var value = values[0];
                      if(value === undefined || value == ""){
                          continue;
                      }
                      if(parseFloat(value) > validation.value){
                          pushFailVal(validation.type);
                      }
                  }
                  break;
              case "regexp":
                  //super dirty regex replace /A to ^ and /Z to $
                  var jsregex = validation.matcher.replace("\\A", "^").replace("\\Z", "$")
                  var regex = new RegExp(jsregex);
                  var value = undefined;
                  if (inputs.length == 3 && (values[0] != "" || values[1] != "" || values[2] != "")) {
                      value = values.join("-");
                  } else if (inputs.length == 1){
                      value = values[0];
                  }
                  if(value == undefined || value == ""){
                      continue;
                  }
                  var result = regex.exec(value);
                  if(result === null || result[0] != value){
                      pushFailVal(validation.type);
                  }
                  break;
              case "valid_integer":
                  var value = values[0];
                  if(value === undefined || value == ""){
                      continue;
                  }
                  var rgx = /(\s*-?[1-9]+[0-9]+\s*|\s*-?[0-9]\s*)/;
                  var result = rgx.exec(value);
                  if(result == null || result[0] != value){
                      pushFailVal(validation.type);
                  }
                  break;
              case "valid_float":
                  var value = values[0];
                  if(value === undefined || value == ""){
                      continue;
                  }
                  var isNumber = !isNaN(parseFloat(value)) && isFinite(value);
                  if(!isNumber){
                    pushFailVal(validation.type);
                  }
                  break;
              case "valid_date":
                  if (allFieldsEmpty(inputs)) {
                    break;
                  }
                  try {
                    fieldsEmpty = numberOfEmptyRequiredDateFields(inputs);
                    if(fieldsEmpty > 0 && fieldsEmpty < inputs.length) {
                      throw "invalidDate";
                    }

                    var date = parsePartialDate(inputs);
                  }
                  catch(e) {
                    pushFailVal(validation.type);
                  }
                  break;
              case "answer_group_minimum":
                  var count = calculateAnswerGroup(validation.group, panel);
                  if(count.visible > 0 && count.answered < validation.value){
                      pushFailVal(validation.type);
                  }
                  break;
              case "answer_group_maximum":
                  var count = calculateAnswerGroup(validation.group, panel);
                  if(count.visible > 0 && count.answered > validation.value){
                      pushFailVal(validation.type);
                  }
                  break;
              case "maximum_checked_allowed":
                  var checkboxes = question_item.find('input[type=checkbox]:checked');
                  if (checkboxes.length > validation.maximum_checked_value) {
                      pushFailVal(validation.type);
                  }
                  break;
              case "minimum_checked_required":
                  var checkboxes = question_item.find('input[type=checkbox]:checked');
                  if (checkboxes.length < validation.minimum_checked_value) {
                    pushFailVal(validation.type);
                  }
                  break;
              //These validations would only come into play if the javascript that makes it impossible
              //to check an invalid combination of checkboxes fails.
              case "too_many_checked":
                  break;
              case "not_all_checked":
                  break;
              }
        }
        if (fail_vals.length != 0) {
            var item = question_item.addClass('errors');
            $(fail_vals).each(function(index, ele){
                item.find(".error." + ele).first().removeClass("hidden");
            });
            failed = true;
        }
      }
    }

    // Scroll the first element that has validation errors into view
    if(failed){
        var first_error = $('.error').not('.hidden')[0];
        if( first_error != undefined) {
          // Prevent horizontal scrolling (on mobile devices)
          $('html, body').animate({scrollTop: first_error.offsetTop}, 0);
        }
    }
    return !failed;
  };

  function parsePartialDate(inputs) {
    var values = dateValuesWithDefaults(inputs)
    if(!dateValuesValid(values)) {
      throw "invalidDate";
    }
    // NB: month is already substracted by 1 by dateValuesWithDefaults to account for js date months starting on 0
    return new Date(Date.UTC(values.year, values.month, values.day, values.hour, values.minute));
  }

  function dateValuesValid(values) {
    return values['year']   >= 1900 && values['year']   <= 2100 &&
           values['month']  >= 0    && values['month']  <= 11   &&
           values['day']    >= 1    && values['day']    <= 31   &&
           values['hour']   >= 0    && values['hour']   <= 23   &&
           values['minute'] >= 0    && values['minute'] <= 59;
  }

  function dateValuesWithDefaults(inputs) {
    var valueWithDefault = function(default_date_key, default_value) {
      var val = $.trim(inputs.filter("[data-default-date-key='" + default_date_key + "']").first().val());
      if (val === undefined || val == "") {
        return default_value;
      }

      if (!/^\d+$/.test(val)) {
        throw "invalidDate";
      }

      var intVal = parseInt(val, 10);
      if (!intVal && intVal !== 0) {
        throw "invalidDate";
      }

      return intVal;
    };

    return {
      year:   valueWithDefault('yyyy', 2000),
      month:  valueWithDefault('mm', 1) - 1, // JS months range from 0-11 instead of 1-12
      day:    valueWithDefault('dd', 1),
      hour:   valueWithDefault('hh', 0),
      minute: valueWithDefault('ii', 0)
    }
  }

  function numberOfEmptyRequiredDateFields(inputs) {
    return inputs.toArray().reduce(function(fieldsEmpty, field) {
      if($(field).val() == '' && $(field).data('required') == true) {
        return ++fieldsEmpty;
      }
      return fieldsEmpty;
    }, 0);
  }

  function allFieldsEmpty(inputs) {
    return inputs.toArray().every(function(field) {
      return $(field).val() == ''
    })
  }

  function allDateFieldsFilledIn(inputs) {
    return numberOfEmptyRequiredDateFields(inputs) == 0;
  }

  function pushFailVal(val){
    fail_vals[validationI] = val;
    validationI++;
  }

  function is_answered(inputs){
    for (var j = 0; j < inputs.length; j++){
      var input = $(inputs[j]);
      if(input.is("[type=text], [type=range], textarea")){ // test for slider, since ie8- can't update type
        if (/\S/.test($(input).val())) {
          return true;
        }
      }
      if(input.is("[type=radio]:checked") || input.is("[type=checkbox]:checked")){
        return true;
      }
      if(input.is("select")){
        return input.find("option:selected:not(.placeholder)").length > 0;
      }
      if(input.is("option:selected:not(.placeholder)")) {
        return true;
      }
    }
    return inputs.length == 0;
  }

  function calculateAnswerGroup(groupkey, panel){
    var groupItems = panel.find(".item." + groupkey + ", .option." + groupkey);

    var answered = 0;
    var visible = 0;
    var hidden = 0;

    for(var i = 0; i < groupItems.length; i++){
      var inputs = $(groupItems[i]).find("input, textarea, select").not(":disabled, :hidden")
              .add($(groupItems[i]).find("input.made-slider:hidden")); // slider inputs

      if (inputs.length == 0) {
        hidden++;
      } else {
        visible++;

        if (is_answered(inputs)) {
          answered++;
        }
      }
    }
    return {total: groupItems.length, visible: visible, hidden: hidden, answered: answered};
  }

})();