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.
- checksums.yaml +7 -0
- data/README.md +24 -0
- data/Rakefile +3 -0
- data/app/assets/config/admin/decidim_surveys_manifest.js +1 -0
- data/app/assets/images/decidim/surveys/icon.svg +1 -0
- data/app/assets/javascripts/decidim/surveys/admin/auto_label_by_position.component.js.es6 +33 -0
- data/app/assets/javascripts/decidim/surveys/admin/dynamic_fields.component.js.es6 +95 -0
- data/app/assets/javascripts/decidim/surveys/admin/surveys.js.es6 +85 -0
- data/app/assets/stylesheets/decidim/surveys/surveys.scss +9 -0
- data/app/commands/decidim/surveys/admin/update_survey.rb +65 -0
- data/app/commands/decidim/surveys/answer_survey.rb +43 -0
- data/app/commands/decidim/surveys/create_survey.rb +19 -0
- data/app/controllers/decidim/surveys/admin/application_controller.rb +15 -0
- data/app/controllers/decidim/surveys/admin/surveys_controller.rb +55 -0
- data/app/controllers/decidim/surveys/application_controller.rb +13 -0
- data/app/controllers/decidim/surveys/surveys_controller.rb +39 -0
- data/app/forms/decidim/surveys/admin/survey_form.rb +19 -0
- data/app/forms/decidim/surveys/admin/survey_question_answer_option_form.rb +15 -0
- data/app/forms/decidim/surveys/admin/survey_question_form.rb +24 -0
- data/app/forms/decidim/surveys/survey_answer_form.rb +36 -0
- data/app/forms/decidim/surveys/survey_form.rb +22 -0
- data/app/helpers/decidim/surveys/admin/application_helper.rb +39 -0
- data/app/models/decidim/surveys/abilities/admin_user.rb +34 -0
- data/app/models/decidim/surveys/abilities/current_user.rb +47 -0
- data/app/models/decidim/surveys/abilities/process_admin_user.rb +44 -0
- data/app/models/decidim/surveys/application_record.rb +10 -0
- data/app/models/decidim/surveys/survey.rb +25 -0
- data/app/models/decidim/surveys/survey_answer.rb +27 -0
- data/app/models/decidim/surveys/survey_question.rb +18 -0
- data/app/queries/decidim/surveys/survey_user_answers.rb +28 -0
- data/app/views/decidim/surveys/admin/surveys/_answer_option.html.erb +23 -0
- data/app/views/decidim/surveys/admin/surveys/_form.html.erb +47 -0
- data/app/views/decidim/surveys/admin/surveys/_question.html.erb +57 -0
- data/app/views/decidim/surveys/admin/surveys/edit.html.erb +7 -0
- data/app/views/decidim/surveys/surveys/show.html.erb +102 -0
- data/config/i18n-tasks.yml +11 -0
- data/config/locales/ca.yml +69 -0
- data/config/locales/en.yml +73 -0
- data/config/locales/es.yml +69 -0
- data/config/locales/eu.yml +5 -0
- data/config/locales/fi.yml +5 -0
- data/config/locales/fr.yml +5 -0
- data/config/locales/it.yml +5 -0
- data/config/locales/nl.yml +5 -0
- data/db/migrate/20170511092231_create_decidim_surveys.rb +15 -0
- data/db/migrate/20170515090916_create_decidim_survey_questions.rb +12 -0
- data/db/migrate/20170515144119_create_decidim_survey_answers.rb +14 -0
- data/db/migrate/20170518085302_add_position_to_surveys_questions.rb +7 -0
- data/db/migrate/20170522075938_add_mandatory_to_surveys_questions.rb +7 -0
- data/db/migrate/20170524122229_add_question_type_to_surveys_questions.rb +7 -0
- data/db/migrate/20170525132233_add_answer_options_to_surveys_questions.rb +7 -0
- data/lib/decidim/surveys.rb +14 -0
- data/lib/decidim/surveys/admin.rb +10 -0
- data/lib/decidim/surveys/admin_engine.rb +34 -0
- data/lib/decidim/surveys/engine.rb +25 -0
- data/lib/decidim/surveys/feature.rb +85 -0
- data/lib/decidim/surveys/survey_user_answers_serializer.rb +23 -0
- data/lib/decidim/surveys/test/factories.rb +39 -0
- 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,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
|