decidim-surveys 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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