decidim-surveys 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +24 -0
  3. data/Rakefile +3 -0
  4. data/app/assets/config/admin/decidim_surveys_manifest.js +1 -0
  5. data/app/assets/images/decidim/surveys/icon.svg +1 -0
  6. data/app/assets/javascripts/decidim/surveys/admin/auto_label_by_position.component.js.es6 +33 -0
  7. data/app/assets/javascripts/decidim/surveys/admin/dynamic_fields.component.js.es6 +95 -0
  8. data/app/assets/javascripts/decidim/surveys/admin/surveys.js.es6 +85 -0
  9. data/app/assets/stylesheets/decidim/surveys/surveys.scss +9 -0
  10. data/app/commands/decidim/surveys/admin/update_survey.rb +65 -0
  11. data/app/commands/decidim/surveys/answer_survey.rb +43 -0
  12. data/app/commands/decidim/surveys/create_survey.rb +19 -0
  13. data/app/controllers/decidim/surveys/admin/application_controller.rb +15 -0
  14. data/app/controllers/decidim/surveys/admin/surveys_controller.rb +55 -0
  15. data/app/controllers/decidim/surveys/application_controller.rb +13 -0
  16. data/app/controllers/decidim/surveys/surveys_controller.rb +39 -0
  17. data/app/forms/decidim/surveys/admin/survey_form.rb +19 -0
  18. data/app/forms/decidim/surveys/admin/survey_question_answer_option_form.rb +15 -0
  19. data/app/forms/decidim/surveys/admin/survey_question_form.rb +24 -0
  20. data/app/forms/decidim/surveys/survey_answer_form.rb +36 -0
  21. data/app/forms/decidim/surveys/survey_form.rb +22 -0
  22. data/app/helpers/decidim/surveys/admin/application_helper.rb +39 -0
  23. data/app/models/decidim/surveys/abilities/admin_user.rb +34 -0
  24. data/app/models/decidim/surveys/abilities/current_user.rb +47 -0
  25. data/app/models/decidim/surveys/abilities/process_admin_user.rb +44 -0
  26. data/app/models/decidim/surveys/application_record.rb +10 -0
  27. data/app/models/decidim/surveys/survey.rb +25 -0
  28. data/app/models/decidim/surveys/survey_answer.rb +27 -0
  29. data/app/models/decidim/surveys/survey_question.rb +18 -0
  30. data/app/queries/decidim/surveys/survey_user_answers.rb +28 -0
  31. data/app/views/decidim/surveys/admin/surveys/_answer_option.html.erb +23 -0
  32. data/app/views/decidim/surveys/admin/surveys/_form.html.erb +47 -0
  33. data/app/views/decidim/surveys/admin/surveys/_question.html.erb +57 -0
  34. data/app/views/decidim/surveys/admin/surveys/edit.html.erb +7 -0
  35. data/app/views/decidim/surveys/surveys/show.html.erb +102 -0
  36. data/config/i18n-tasks.yml +11 -0
  37. data/config/locales/ca.yml +69 -0
  38. data/config/locales/en.yml +73 -0
  39. data/config/locales/es.yml +69 -0
  40. data/config/locales/eu.yml +5 -0
  41. data/config/locales/fi.yml +5 -0
  42. data/config/locales/fr.yml +5 -0
  43. data/config/locales/it.yml +5 -0
  44. data/config/locales/nl.yml +5 -0
  45. data/db/migrate/20170511092231_create_decidim_surveys.rb +15 -0
  46. data/db/migrate/20170515090916_create_decidim_survey_questions.rb +12 -0
  47. data/db/migrate/20170515144119_create_decidim_survey_answers.rb +14 -0
  48. data/db/migrate/20170518085302_add_position_to_surveys_questions.rb +7 -0
  49. data/db/migrate/20170522075938_add_mandatory_to_surveys_questions.rb +7 -0
  50. data/db/migrate/20170524122229_add_question_type_to_surveys_questions.rb +7 -0
  51. data/db/migrate/20170525132233_add_answer_options_to_surveys_questions.rb +7 -0
  52. data/lib/decidim/surveys.rb +14 -0
  53. data/lib/decidim/surveys/admin.rb +10 -0
  54. data/lib/decidim/surveys/admin_engine.rb +34 -0
  55. data/lib/decidim/surveys/engine.rb +25 -0
  56. data/lib/decidim/surveys/feature.rb +85 -0
  57. data/lib/decidim/surveys/survey_user_answers_serializer.rb +23 -0
  58. data/lib/decidim/surveys/test/factories.rb +39 -0
  59. metadata +147 -0
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # This controller is the abstract class from which all other controllers of
6
+ # this engine inherit.
7
+ #
8
+ # Note that it inherits from `Decidim::Features::BaseController`, which
9
+ # override its layout and provide all kinds of useful methods.
10
+ class ApplicationController < Decidim::Features::BaseController
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # Exposes the survey resource so users can view and answer them.
6
+ class SurveysController < Decidim::Surveys::ApplicationController
7
+ include FormFactory
8
+
9
+ helper_method :survey
10
+
11
+ def show
12
+ @form = form(SurveyForm).from_model(survey)
13
+ end
14
+
15
+ def answer
16
+ authorize! :answer, Survey
17
+ @form = form(SurveyForm).from_params(params)
18
+
19
+ AnswerSurvey.call(@form, current_user, survey) do
20
+ on(:ok) do
21
+ flash[:notice] = I18n.t("surveys.answer.success", scope: "decidim.surveys")
22
+ redirect_to survey_path(survey)
23
+ end
24
+
25
+ on(:invalid) do
26
+ flash.now[:alert] = I18n.t("surveys.answer.invalid", scope: "decidim.surveys")
27
+ render action: "show"
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def survey
35
+ @survey ||= Survey.find_by(feature: current_feature)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Admin
6
+ # This class holds a Form to update surveys from Decidim's admin panel.
7
+ class SurveyForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ translatable_attribute :title, String
11
+ translatable_attribute :description, String
12
+ translatable_attribute :tos, String
13
+ attribute :published_at, DateTime
14
+
15
+ attribute :questions, Array[SurveyQuestionForm]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Admin
6
+ # This class holds a Form to update survey question answer options
7
+ class SurveyQuestionAnswerOptionForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ attribute :body, String
11
+ translatable_attribute :body, String
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Admin
6
+ # This class holds a Form to update survey questions from Decidim's admin panel.
7
+ class SurveyQuestionForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ attribute :id, String
11
+ attribute :position, Integer
12
+ attribute :mandatory, Boolean, default: false
13
+ attribute :question_type, String
14
+ attribute :answer_options, Array[SurveyQuestionAnswerOptionForm]
15
+ attribute :deleted, Boolean, default: false
16
+
17
+ translatable_attribute :body, String
18
+
19
+ validates :position, numericality: { greater_than_or_equal_to: 0 }
20
+ validates :question_type, inclusion: { in: SurveyQuestion::TYPES }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # This class holds a Form to update survey unswers from Decidim's public page
6
+ class SurveyAnswerForm < Decidim::Form
7
+ attribute :question_id, String
8
+ attribute :body, String
9
+
10
+ validates :body, presence: true, if: -> { question.mandatory? }
11
+ validate :body_not_blank, if: -> { question.mandatory? }
12
+
13
+ def question
14
+ @question ||= survey.questions.find(question_id)
15
+ end
16
+
17
+ # Public: Map the correct fields.
18
+ #
19
+ # Returns nothing.
20
+ def map_model(model)
21
+ self.question_id = model.id
22
+ end
23
+
24
+ private
25
+
26
+ def survey
27
+ @survey ||= Survey.where(feature: current_feature).first
28
+ end
29
+
30
+ def body_not_blank
31
+ return if body.nil?
32
+ errors.add("body", :blank) if body.all?(&:blank?)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # This class holds a Form to answer a surveys from Decidim's public page.
6
+ class SurveyForm < Decidim::Form
7
+ attribute :answers, Array[SurveyAnswerForm]
8
+
9
+ attribute :tos_agreement, Boolean
10
+ validates :tos_agreement, allow_nil: false, acceptance: true
11
+
12
+ # Private: Create the answers from the survey questions
13
+ #
14
+ # Returns nothing.
15
+ def map_model(model)
16
+ self.answers = model.questions.map do |question|
17
+ SurveyAnswerForm.from_params(question_id: question.id)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Admin
6
+ # Custom helpers, scoped to the surveys engine.
7
+ #
8
+ module ApplicationHelper
9
+ def tabs_id_for_question(question)
10
+ return "survey-question-#{question.id}" if question.persisted?
11
+ "${tabsId}"
12
+ end
13
+
14
+ def tabs_id_for_question_answer_option(question, idx)
15
+ return "survey-question-answer-option-#{question.id}-#{idx}" if question.present?
16
+ "${tabsId}"
17
+ end
18
+
19
+ def label_for_question(survey, _question)
20
+ survey.questions_editable? ? "#{icon("move")} #{t(".question")}".html_safe : t(".question")
21
+ end
22
+
23
+ def mandatory_id_for_question(question)
24
+ return "survey_questions_#{question.id}_mandatory" if question.persisted?
25
+ "${tabsId}_mandatory"
26
+ end
27
+
28
+ def question_type_id_for_question(question)
29
+ return "survey_questions_#{question.id}_question_type" if question.persisted?
30
+ "${tabsId}_question_type"
31
+ end
32
+
33
+ def disabled_for_question(survey, question)
34
+ !question.persisted? || !survey.questions_editable?
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Abilities
6
+ # Defines the abilities related to surveys for a logged in admin user.
7
+ # Intended to be used with `cancancan`.
8
+ class AdminUser
9
+ include CanCan::Ability
10
+
11
+ attr_reader :user, :context
12
+
13
+ def initialize(user, context)
14
+ return unless user && user.role?(:admin)
15
+
16
+ @user = user
17
+ @context = context
18
+
19
+ can :manage, Survey
20
+ end
21
+
22
+ private
23
+
24
+ def current_settings
25
+ context.fetch(:current_settings, nil)
26
+ end
27
+
28
+ def feature_settings
29
+ context.fetch(:feature_settings, nil)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Abilities
6
+ # Defines the abilities related to surveys for a logged in user.
7
+ # Intended to be used with `cancancan`.
8
+ class CurrentUser
9
+ include CanCan::Ability
10
+
11
+ attr_reader :user, :context
12
+
13
+ def initialize(user, context)
14
+ return unless user
15
+
16
+ @user = user
17
+ @context = context
18
+
19
+ can :answer, Survey if authorized?(:answer)
20
+ end
21
+
22
+ private
23
+
24
+ def authorized?(action)
25
+ return unless feature
26
+
27
+ ActionAuthorizer.new(user, feature, action).authorize.ok?
28
+ end
29
+
30
+ def current_settings
31
+ context.fetch(:current_settings, nil)
32
+ end
33
+
34
+ def feature_settings
35
+ context.fetch(:feature_settings, nil)
36
+ end
37
+
38
+ def feature
39
+ feature = context.fetch(:current_feature, nil)
40
+ return nil unless feature && feature.manifest.name == :surveys
41
+
42
+ feature
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Abilities
6
+ # Defines the abilities related to surveys for a logged in process admin user.
7
+ # Intended to be used with `cancancan`.
8
+ class ProcessAdminUser
9
+ include CanCan::Ability
10
+
11
+ attr_reader :user, :context
12
+
13
+ def initialize(user, context)
14
+ return unless user && !user.role?(:admin)
15
+
16
+ @user = user
17
+ @context = context
18
+
19
+ can :manage, Survey do |survey|
20
+ participatory_processes.include?(survey.feature.participatory_process)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def current_settings
27
+ context.fetch(:current_settings, nil)
28
+ end
29
+
30
+ def feature_settings
31
+ context.fetch(:feature_settings, nil)
32
+ end
33
+
34
+ def current_feature
35
+ context.fetch(:current_feature, nil)
36
+ end
37
+
38
+ def participatory_processes
39
+ @participatory_processes ||= Decidim::Admin::ManageableParticipatoryProcessesForUser.for(@user)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # Abstract class from which all models in this engine inherit.
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # The data store for a Survey in the Decidim::Surveys component.
6
+ class Survey < Surveys::ApplicationRecord
7
+ include Decidim::HasFeature
8
+
9
+ feature_manifest_name "surveys"
10
+
11
+ has_many :questions, -> { order(:position) }, class_name: "SurveyQuestion", foreign_key: "decidim_survey_id"
12
+ has_many :answers, class_name: "SurveyAnswer", foreign_key: "decidim_survey_id"
13
+
14
+ # Public: returns whether the survey questions can be modified or not.
15
+ def questions_editable?
16
+ answers.empty?
17
+ end
18
+
19
+ # Public: returns whether the survey is answered by the user or not.
20
+ def answered_by?(user)
21
+ answers.where(user: user).count == questions.length
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # The data store for a SurveyAnswer in the Decidim::Surveys component.
6
+ class SurveyAnswer < Surveys::ApplicationRecord
7
+ belongs_to :user, class_name: "Decidim::User", foreign_key: "decidim_user_id"
8
+ belongs_to :survey, class_name: "Survey", foreign_key: "decidim_survey_id"
9
+ belongs_to :question, class_name: "SurveyQuestion", foreign_key: "decidim_survey_question_id"
10
+
11
+ validates :body, presence: true, if: -> { question.mandatory? }
12
+ validate :user_survey_same_organization
13
+ validate :question_belongs_to_survey
14
+
15
+ private
16
+
17
+ def user_survey_same_organization
18
+ return if user&.organization == survey&.organization
19
+ errors.add(:user, :invalid)
20
+ end
21
+
22
+ def question_belongs_to_survey
23
+ errors.add(:survey, :invalid) if question&.survey != survey
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # The data store for a SurveyQuestion in the Decidim::Surveys component.
6
+ class SurveyQuestion < Surveys::ApplicationRecord
7
+ TYPES = %w(short_answer long_answer single_option multiple_option).freeze
8
+
9
+ belongs_to :survey, class_name: "Survey", foreign_key: "decidim_survey_id"
10
+
11
+ # Rectify can't handle a hash when using the from_model method so
12
+ # the answer options must be converted to struct.
13
+ def answer_options
14
+ self[:answer_options].map { |option| OpenStruct.new(option) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # A class used to collect user answers for a survey
6
+ class SurveyUserAnswers < Rectify::Query
7
+ # Syntactic sugar to initialize the class and return the queried objects.
8
+ #
9
+ # survey - a Survey object
10
+ def self.for(survey)
11
+ new(survey).query
12
+ end
13
+
14
+ # Initializes the class.
15
+ #
16
+ # survey = a Survey object
17
+ def initialize(survey)
18
+ @survey = survey
19
+ end
20
+
21
+ # Finds and group answers by user for each survey's question.
22
+ def query
23
+ answers = SurveyAnswer.where(survey: @survey)
24
+ answers.sort_by { |answer| answer.question.position }.group_by(&:user).values
25
+ end
26
+ end
27
+ end
28
+ end