FEATURE: ability to set 'required' on individual survey fields

FEATURE: ability to set ‘required’ on individual survey fields

diff --git a/app/serializers/survey_field_serializer.rb b/app/serializers/survey_field_serializer.rb
index 015213b..02d88d5 100644
--- a/app/serializers/survey_field_serializer.rb
+++ b/app/serializers/survey_field_serializer.rb
@@ -3,6 +3,7 @@
 class SurveyFieldSerializer < ApplicationSerializer
   attributes :question,
              :response_type,
+             :response_required,
              :digest,
              :options,
              :has_options,
diff --git a/assets/javascripts/lib/discourse-markdown/survey.js.es6 b/assets/javascripts/lib/discourse-markdown/survey.js.es6
index 0865be0..3f38e12 100644
--- a/assets/javascripts/lib/discourse-markdown/survey.js.es6
+++ b/assets/javascripts/lib/discourse-markdown/survey.js.es6
@@ -9,6 +9,7 @@ const WHITELISTED_ATTRIBUTES = [
   "name",
   "public",
   "question",
+  "required",
   "status",
   "type"
 ];
diff --git a/assets/javascripts/widgets/discourse-survey.js.es6 b/assets/javascripts/widgets/discourse-survey.js.es6
index 02ff5c2..1c67f74 100644
--- a/assets/javascripts/widgets/discourse-survey.js.es6
+++ b/assets/javascripts/widgets/discourse-survey.js.es6
@@ -360,8 +360,7 @@ export default createWidget("discourse-survey", {
     let cssClasses = "survey";
     return {
       class: cssClasses,
-      "data-survey-name": attrs.survey.get("name"),
-      "data-survey-type": attrs.survey.get("type")
+      "data-survey-name": attrs.survey.get("name")
     };
   },
 
@@ -425,10 +424,15 @@ export default createWidget("discourse-survey", {
       return false;
     }
 
-    const respondedFieldCount = Object.keys(attrs.response).length;
-    const totalFieldCount = attrs.survey.fields.length;
+    const requiredFields = [];
+    attrs.survey.fields.map(field => {
+      if (field.response_required) {
+        requiredFields.push(field.digest);
+      }
+    })
 
-    return totalFieldCount === respondedFieldCount;
+    const respondedFields = Object.keys(attrs.response)
+    return requiredFields.every(i => respondedFields.includes(i));
   },
 
   showLogin() {
@@ -451,6 +455,10 @@ export default createWidget("discourse-survey", {
         } else {
           response[optionInfo.fieldId].push(optionInfo.option.digest);
         }
+        // delete empty array
+        if (response[optionInfo.fieldId].length === 0) {
+          delete response[optionInfo.fieldId];
+        }
       } else {
         response[optionInfo.fieldId] = [optionInfo.option.digest];
       }
@@ -467,7 +475,12 @@ export default createWidget("discourse-survey", {
   toggleValue(fieldInfo) {
     if (!this.currentUser) return this.showLogin();
     const { response } = this.attrs;
-    response[fieldInfo.fieldId] = fieldInfo.value;
+    // delete empty string
+    if (fieldInfo.value === "") {
+      delete response[fieldInfo.fieldId];
+    } else {
+      response[fieldInfo.fieldId] = fieldInfo.value;
+    }
   },
 
   submitResponse() {
diff --git a/db/migrate/20200901155134_add_response_required_to_survey_fields.rb b/db/migrate/20200901155134_add_response_required_to_survey_fields.rb
new file mode 100644
index 0000000..63c9c05
--- /dev/null
+++ b/db/migrate/20200901155134_add_response_required_to_survey_fields.rb
@@ -0,0 +1,5 @@
+class AddResponseRequiredToSurveyFields < ActiveRecord::Migration[6.0]
+  def change
+    add_column :survey_fields, :response_required, :boolean, null: false, default: true
+  end
+end
diff --git a/lib/discourse-surveys/helper.rb b/lib/discourse-surveys/helper.rb
index fb442c0..7e6800f 100644
--- a/lib/discourse-surveys/helper.rb
+++ b/lib/discourse-surveys/helper.rb
@@ -23,7 +23,8 @@ module DiscourseSurveys
               digest:  field["field-id"].presence,
               question: field["question"],
               position: position,
-              response_type: SurveyField.response_type[field["type"].to_sym] || SurveyField.response_type[:radio]
+              response_type: SurveyField.response_type[field["type"].to_sym] || SurveyField.response_type[:radio],
+              response_required: field["required"].presence || true
             )
 
             if field["options"].present?
@@ -39,44 +40,6 @@ module DiscourseSurveys
         end
       end
 
-      def extract(raw, topic_id, user_id = nil)
-        cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id)
-
-        Nokogiri::HTML5(cooked).css("div.survey").map do |s|
-          survey = { "name" => DiscourseSurveys::DEFAULT_SURVEY_NAME, "fields" => Set.new }
-
-          s.attributes.values.each do |attribute|
-            if attribute.name.start_with?(DATA_PREFIX)
-              survey[attribute.name[DATA_PREFIX.length..-1]] = CGI.escapeHTML(attribute.value || "")
-            end
-          end
-
-          type_attribute = "#{DATA_PREFIX}type"
-          s.css("div[#{type_attribute}]").each.with_index do |field, position|
-            attribute = field.attributes[type_attribute].value.to_s
-
-            case attribute
-            when 'radio'
-              survey['fields'] << extract_radio(field, position)
-            when 'checkbox'
-              survey['fields'] << extract_checkbox(field, position)
-            when 'dropdown'
-              survey['fields'] << extract_dropdown(field, position)
-            when 'textarea'
-              survey['fields'] << extract_textarea(field, position)
-            when 'number'
-              survey['fields'] << extract_number(field, position)
-            when 'star'
-              survey['fields'] << extract_star(field, position)
-            when 'thumbs'
-              survey['fields'] << extract_thumbs(field, position)
-            end
-          end
-
-          survey
-        end
-      end
-
       def submit_response(post_id, survey_name, response, user)
         Survey.transaction do
           post = Post.find_by(id: post_id)
@@ -130,6 +93,44 @@ module DiscourseSurveys
         end
       end
 
+      def extract(raw, topic_id, user_id = nil)
+        cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id)
+
+        Nokogiri::HTML5(cooked).css("div.survey").map do |s|
+          survey = { "name" => DiscourseSurveys::DEFAULT_SURVEY_NAME, "fields" => Set.new }
+
+          s.attributes.values.each do |attribute|
+            if attribute.name.start_with?(DATA_PREFIX)
+              survey[attribute.name[DATA_PREFIX.length..-1]] = CGI.escapeHTML(attribute.value || "")
+            end
+          end
+
+          type_attribute = "#{DATA_PREFIX}type"
+          s.css("div[#{type_attribute}]").each.with_index do |field, position|
+            attribute = field.attributes[type_attribute].value.to_s
+
+            case attribute
+            when 'radio'
+              survey['fields'] << extract_radio(field, position)
+            when 'checkbox'
+              survey['fields'] << extract_checkbox(field, position)
+            when 'dropdown'
+              survey['fields'] << extract_dropdown(field, position)
+            when 'textarea'
+              survey['fields'] << extract_textarea(field, position)
+            when 'number'
+              survey['fields'] << extract_number(field, position)
+            when 'star'
+              survey['fields'] << extract_star(field, position)
+            when 'thumbs'
+              survey['fields'] << extract_thumbs(field, position)
+            end
+          end
+
+          survey
+        end
+      end
+
       private
 
       def extract_checkbox(checkbox, position)
diff --git a/lib/discourse-surveys/survey_updater.rb b/lib/discourse-surveys/survey_updater.rb
index 0316de5..21f72a8 100644
--- a/lib/discourse-surveys/survey_updater.rb
+++ b/lib/discourse-surveys/survey_updater.rb
@@ -83,8 +83,9 @@ module DiscourseSurveys
           new_field = new_field.first
           new_field_options = new_field["options"]
 
-          # update field position
+          # update field position and required attribute
           old_field["position"] = new_field["position"]

[... diff too long, it was truncated ...]

GitHub sha: 502254c1

There should be a test for that :wink: