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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5d69c74665668b67e559573ca50ba753023f7730
4
+ data.tar.gz: 197812b81fe85469d40b6a4eb9e26904dc8321b8
5
+ SHA512:
6
+ metadata.gz: 105ca8bc419db8b649bb4c1afcd3cf3fdd2b960a5e92f40073a17f05d1cff5bebcfb5a70c66ccebf69d025dc4ed56ee2be476c0cf174a06b0973478e9be8d2b9
7
+ data.tar.gz: 6689cceacf3b2814e764894a022afb799e93af0073f8bbbf498c8de65187a8b8264960bc6a126581d76cb5ef518e373eac7a15f775af0cdd79d58ca27fbb45ef
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Decidim::Surveys
2
+
3
+ The Surveys module adds one of the main features of Decidim: allows admins to contribute to a participatory process by creating surveys. Users can participate in the surveys answering their questions from the public page.
4
+
5
+ ## Usage
6
+ Surveys will be available as a Feature for a Participatory Process.
7
+
8
+ ## Installation
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'decidim-surveys'
13
+ ```
14
+
15
+ And then execute:
16
+ ```bash
17
+ $ bundle
18
+ ```
19
+
20
+ ## Contributing
21
+ See [Decidim](https://github.com/decidim/decidim).
22
+
23
+ ## License
24
+ See [Decidim](https://github.com/decidim/decidim).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/dev/common_rake"
@@ -0,0 +1 @@
1
+ //= link decidim/surveys/admin/surveys.js
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36.02 36.02"><path d="M373,545.54a18,18,0,1,1,18-18A18,18,0,0,1,373,545.54Zm0-34a16,16,0,1,0,16,16A16,16,0,0,0,373,511.53Z" transform="translate(-354.99 -509.53)"/><path d="M377.73,536.41h-9.46a1,1,0,1,1,0-2h9.46A1,1,0,0,1,377.73,536.41Z" transform="translate(-354.99 -509.53)"/><path d="M375,525.54h-6.73a1,1,0,1,1,0-2H375A1,1,0,1,1,375,525.54Z" transform="translate(-354.99 -509.53)"/><path d="M375,535.41a1,1,0,0,1-1-1v-9.87a1,1,0,1,1,2,0v9.87A1,1,0,0,1,375,535.41Z" transform="translate(-354.99 -509.53)"/><path d="M373,523a3,3,0,1,1,3-3A3,3,0,0,1,373,523Zm0-4a1,1,0,1,0,1,1A1,1,0,0,0,373,519Z" transform="translate(-354.99 -509.53)"/><path d="M371,535.41a1,1,0,0,1-1-1v-9.87a1,1,0,1,1,2,0v9.87A1,1,0,0,1,371,535.41Z" transform="translate(-354.99 -509.53)"/></svg>
@@ -0,0 +1,33 @@
1
+ ((exports) => {
2
+ class AutoLabelByPositionComponent {
3
+ constructor(options = {}) {
4
+ this.listSelector = options.listSelector;
5
+ this.labelSelector = options.labelSelector;
6
+ this.onPositionComputed = options.onPositionComputed;
7
+
8
+ this.run();
9
+ }
10
+
11
+ run() {
12
+ const $list = $(this.listSelector);
13
+
14
+ $list.each((idx, el) => {
15
+ const $label = $(el).find(this.labelSelector);
16
+ const labelContent = $label.html();
17
+
18
+ if (labelContent.match(/#(\d+)/)) {
19
+ $label.html(labelContent.replace(/#(\d+)/, `#${idx + 1}`));
20
+ } else {
21
+ $label.html(`${labelContent} #${idx + 1}`);
22
+ }
23
+
24
+ if (this.onPositionComputed) {
25
+ this.onPositionComputed(el, idx);
26
+ }
27
+ });
28
+ }
29
+ }
30
+
31
+ exports.DecidimAdmin = exports.DecidimAdmin || {};
32
+ exports.DecidimAdmin.AutoLabelByPositionComponent = AutoLabelByPositionComponent;
33
+ })(window);
@@ -0,0 +1,95 @@
1
+ ((exports) => {
2
+ class DynamicFieldsComponent {
3
+ constructor(options = {}) {
4
+ this.templateId = options.templateId;
5
+ this.wrapperSelector = options.wrapperSelector;
6
+ this.containerSelector = options.containerSelector;
7
+ this.fieldSelector = options.fieldSelector;
8
+ this.addFieldButtonSelector = options.addFieldButtonSelector;
9
+ this.removeFieldButtonSelector = options.removeFieldButtonSelector;
10
+ this.onAddField = options.onAddField;
11
+ this.onRemoveField = options.onRemoveField;
12
+ this.tabsPrefix = options.tabsPrefix;
13
+ this._compileTemplate();
14
+ this._bindEvents();
15
+ }
16
+
17
+ _compileTemplate() {
18
+ $.template(this.templateId, $(`#${this.templateId}`).html());
19
+ }
20
+
21
+ _bindEvents() {
22
+ $(this.wrapperSelector).on('click', this.addFieldButtonSelector, (event) =>
23
+ this._bindSafeEvent(event, () => this._addField())
24
+ );
25
+
26
+ $(this.wrapperSelector).on('click', this.removeFieldButtonSelector, (event) =>
27
+ this._bindSafeEvent(event, (target) => this._removeField(target))
28
+ );
29
+ }
30
+
31
+ _bindSafeEvent(event, cb) {
32
+ event.preventDefault();
33
+ event.stopPropagation();
34
+
35
+ try {
36
+ return cb(event.target);
37
+ } catch (error) {
38
+ console.error(error); // eslint-disable-line no-console
39
+ return error;
40
+ }
41
+ }
42
+
43
+ _addField() {
44
+ const $container = $(this.wrapperSelector).find(this.containerSelector);
45
+ const uid = this._getUID();
46
+ const tabsId = `${this.tabsPrefix}-${uid}`;
47
+
48
+ const $newField = $.tmpl(this.templateId, { tabsId });
49
+
50
+ $newField.attr('id', `${tabsId}-field`);
51
+ $newField.find('[disabled]').attr('disabled', false);
52
+ $newField.find('ul.tabs').attr('data-tabs', true);
53
+
54
+ $newField.appendTo($container);
55
+ $newField.foundation();
56
+
57
+ if (this.onAddField) {
58
+ this.onAddField($newField);
59
+ }
60
+ }
61
+
62
+ _removeField(target) {
63
+ const $target = $(target);
64
+ const $removedField = $target.parents(this.fieldSelector);
65
+ const idInput = $removedField.find('input').filter((idx, input) => input.name.match(/id/));
66
+
67
+ if (idInput.length > 0) {
68
+ const deletedInput = $removedField.find('input').filter((idx, input) => input.name.match(/delete/));
69
+
70
+ if (deletedInput.length > 0) {
71
+ $(deletedInput[0]).val(true);
72
+ }
73
+
74
+ $removedField.addClass('hidden');
75
+ $removedField.hide();
76
+ } else {
77
+ $removedField.remove();
78
+ }
79
+
80
+ if (this.onRemoveField) {
81
+ this.onRemoveField($removedField);
82
+ }
83
+ }
84
+
85
+ _getUID() {
86
+ return `${new Date().getTime()}-${Math.floor(Math.random() * 1000000)}`;
87
+ }
88
+ }
89
+
90
+ exports.DecidimAdmin = exports.DecidimAdmin || {};
91
+ exports.DecidimAdmin.DynamicFieldsComponent = DynamicFieldsComponent;
92
+ exports.DecidimAdmin.createDynamicFields = (options) => {
93
+ return new DynamicFieldsComponent(options);
94
+ };
95
+ })(window);
@@ -0,0 +1,85 @@
1
+ // = require jquery-tmpl
2
+ // = require ./auto_label_by_position.component
3
+ // = require ./dynamic_fields.component
4
+
5
+ ((exports) => {
6
+ const { AutoLabelByPositionComponent, createDynamicFields, createSortList } = exports.DecidimAdmin;
7
+
8
+ const wrapperSelector = '.survey-questions';
9
+ const fieldSelector = '.survey-question';
10
+ const questionTypeSelector = '[name="survey[questions][][question_type]"]';
11
+ const answerOptionsWrapperSelector = '.survey-question-answer-options';
12
+
13
+ const autoLabelByPosition = new AutoLabelByPositionComponent({
14
+ listSelector: '.survey-question:not(.hidden)',
15
+ labelSelector: '.card-title span:first',
16
+ onPositionComputed: (el, idx) => {
17
+ $(el).find('input[name="survey[questions][][position]"]').val(idx);
18
+ }
19
+ });
20
+
21
+ const createSortableList = () => {
22
+ createSortList('.survey-questions-list:not(.published)', {
23
+ handle: '.question-divider',
24
+ placeholder: '<div style="border-style: dashed; border-color: #000"></div>',
25
+ forcePlaceholderSize: true,
26
+ onSortUpdate: () => { autoLabelByPosition.run() }
27
+ });
28
+ };
29
+
30
+ const createDynamicFieldsForAnswerOptions = (fieldId) => {
31
+ createDynamicFields({
32
+ templateId: `survey-question-answer-option-tmpl`,
33
+ tabsPrefix: `survey-question-answer-option`,
34
+ wrapperSelector: `#${fieldId} ${answerOptionsWrapperSelector}`,
35
+ containerSelector: `.survey-question-answer-options-list`,
36
+ fieldSelector: `.survey-question-answer-option`,
37
+ addFieldButtonSelector: `.add-answer-option`,
38
+ removeFieldButtonSelector: `.remove-answer-option`
39
+ });
40
+ };
41
+
42
+ const setAnswerOptionsWrapperVisibility = ($target) => {
43
+ const $answerOptionsWrapper = $target.parents(fieldSelector).find(answerOptionsWrapperSelector);
44
+ const value = $target.val();
45
+
46
+ $answerOptionsWrapper.hide();
47
+
48
+ if (value === 'single_option' || value === 'multiple_option') {
49
+ $answerOptionsWrapper.show();
50
+ }
51
+ };
52
+
53
+ createDynamicFields({
54
+ templateId: 'survey-question-tmpl',
55
+ tabsPrefix: 'survey-question',
56
+ wrapperSelector: wrapperSelector,
57
+ containerSelector: '.survey-questions-list',
58
+ fieldSelector: fieldSelector,
59
+ addFieldButtonSelector: '.add-question',
60
+ removeFieldButtonSelector: '.remove-question',
61
+ onAddField: ($field) => {
62
+ const fieldId = $field.attr('id');
63
+
64
+ createSortableList();
65
+ autoLabelByPosition.run();
66
+ createDynamicFieldsForAnswerOptions(fieldId);
67
+ setAnswerOptionsWrapperVisibility($field.find(questionTypeSelector));
68
+ },
69
+ onRemoveField: () => {
70
+ autoLabelByPosition.run();
71
+ }
72
+ });
73
+
74
+ createSortableList();
75
+
76
+ $(fieldSelector).each((idx, el) => {
77
+ createDynamicFieldsForAnswerOptions($(el).attr('id'));
78
+ setAnswerOptionsWrapperVisibility($(el).find(questionTypeSelector));
79
+ });
80
+
81
+ $(wrapperSelector).on('change', questionTypeSelector, (ev) => {
82
+ const $target = $(ev.target);
83
+ setAnswerOptionsWrapperVisibility($target);
84
+ });
85
+ })(window);
@@ -0,0 +1,9 @@
1
+ form.answer-survey {
2
+ .radio-button-collection, .check-box-collection {
3
+ margin: 0 0 1rem;
4
+ }
5
+
6
+ .tos-agreement-help-text {
7
+ margin: 0.2rem 0 0;
8
+ }
9
+ }
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Admin
6
+ # This command is executed when the user changes a Survey from the admin
7
+ # panel.
8
+ class UpdateSurvey < Rectify::Command
9
+ # Initializes a UpdateSurvey Command.
10
+ #
11
+ # form - The form from which to get the data.
12
+ # survey - The current instance of the survey to be updated.
13
+ def initialize(form, survey)
14
+ @form = form
15
+ @survey = survey
16
+ end
17
+
18
+ # Updates the survey if valid.
19
+ #
20
+ # Broadcasts :ok if successful, :invalid otherwise.
21
+ def call
22
+ return broadcast(:invalid) if @form.invalid?
23
+
24
+ Survey.transaction do
25
+ update_survey_questions if @survey.questions_editable?
26
+ update_survey
27
+ end
28
+
29
+ broadcast(:ok)
30
+ end
31
+
32
+ private
33
+
34
+ def update_survey_questions
35
+ @form.questions.each do |form_question|
36
+ question_attributes = {
37
+ body: form_question.body,
38
+ position: form_question.position,
39
+ mandatory: form_question.mandatory,
40
+ question_type: form_question.question_type,
41
+ answer_options: form_question.answer_options.map { |answer| { "body" => answer.body } }
42
+ }
43
+
44
+ if form_question.id.present?
45
+ question = @survey.questions.where(id: form_question.id).first
46
+ if form_question.deleted?
47
+ question.destroy!
48
+ else
49
+ question.update_attributes!(question_attributes)
50
+ end
51
+ else
52
+ @survey.questions.create!(question_attributes)
53
+ end
54
+ end
55
+ end
56
+
57
+ def update_survey
58
+ @survey.update_attributes!(title: @form.title,
59
+ description: @form.description,
60
+ tos: @form.tos)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # This command is executed when the user answers a Survey.
6
+ class AnswerSurvey < Rectify::Command
7
+ # Initializes a AnswerSurvey Command.
8
+ #
9
+ # form - The form from which to get the data.
10
+ # survey - The current instance of the survey to be answered.
11
+ def initialize(form, current_user, survey)
12
+ @form = form
13
+ @current_user = current_user
14
+ @survey = survey
15
+ end
16
+
17
+ # Answers a survey if it is valid
18
+ #
19
+ # Broadcasts :ok if successful, :invalid otherwise.
20
+ def call
21
+ return broadcast(:invalid) if @form.invalid?
22
+
23
+ answer_survey
24
+ broadcast(:ok)
25
+ end
26
+
27
+ private
28
+
29
+ def answer_survey
30
+ SurveyAnswer.transaction do
31
+ @form.answers.each do |form_answer|
32
+ SurveyAnswer.create!(
33
+ user: @current_user,
34
+ survey: @survey,
35
+ question: form_answer.question,
36
+ body: form_answer.body
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ # Command that gets called whenever a feature's survey has to be created. It
6
+ # usually happens as a callback when the feature itself is created.
7
+ class CreateSurvey < Rectify::Command
8
+ def initialize(feature)
9
+ @feature = feature
10
+ end
11
+
12
+ def call
13
+ @survey = Survey.new(feature: @feature)
14
+
15
+ @survey.save ? broadcast(:ok) : broadcast(:invalid)
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 controller is the abstract class from which all other controllers of
7
+ # this engine inherit.
8
+ #
9
+ # Note that it inherits from `Decidim::Admin::Features::BaseController`, which
10
+ # override its layout and provide all kinds of useful methods.
11
+ class ApplicationController < Decidim::Admin::Features::BaseController
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Surveys
5
+ module Admin
6
+ # This controller allows the user to update a Page.
7
+ class SurveysController < Admin::ApplicationController
8
+ helper_method :survey, :blank_question, :blank_answer_option, :question_types
9
+
10
+ def edit
11
+ authorize! :edit, Survey
12
+ @form = form(Admin::SurveyForm).from_model(survey)
13
+ end
14
+
15
+ def update
16
+ authorize! :update, Survey
17
+ params["published_at"] = Time.current if params.has_key? "save_and_publish"
18
+ @form = form(Admin::SurveyForm).from_params(params)
19
+
20
+ Admin::UpdateSurvey.call(@form, survey) do
21
+ on(:ok) do
22
+ flash[:notice] = I18n.t("surveys.update.success", scope: "decidim.surveys.admin")
23
+ redirect_to parent_path
24
+ end
25
+
26
+ on(:invalid) do
27
+ flash.now[:alert] = I18n.t("surveys.update.invalid", scope: "decidim.surveys.admin")
28
+ render action: "edit"
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def survey
36
+ @survey ||= Survey.find_by(feature: current_feature)
37
+ end
38
+
39
+ def blank_question
40
+ @blank_question ||= survey.questions.build(body: {}, answer_options: [])
41
+ end
42
+
43
+ def blank_answer_option
44
+ @blank_answer_option ||= OpenStruct.new(body: {})
45
+ end
46
+
47
+ def question_types
48
+ @question_types ||= SurveyQuestion::TYPES.map do |question_type|
49
+ [question_type, I18n.t("decidim.surveys.question_types.#{question_type}")]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end