decidim-term_customizer 0.16.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +180 -0
  4. data/Rakefile +48 -0
  5. data/app/assets/config/translation_sets_admin_manifest.js +2 -0
  6. data/app/assets/javascripts/decidim/term_customizer/admin/constraint_fields.js.es6 +45 -0
  7. data/app/assets/javascripts/decidim/term_customizer/admin/multifield/component.js.es6 +103 -0
  8. data/app/assets/javascripts/decidim/term_customizer/admin/multifield.js.es6 +5 -0
  9. data/app/assets/javascripts/decidim/term_customizer/admin/translation_sets_admin.js.es6 +19 -0
  10. data/app/assets/javascripts/decidim/term_customizer/admin/translations_admin.js.es6 +72 -0
  11. data/app/commands/decidim/term_customizer/admin/create_translation.rb +53 -0
  12. data/app/commands/decidim/term_customizer/admin/create_translation_set.rb +68 -0
  13. data/app/commands/decidim/term_customizer/admin/import_translation_keys.rb +59 -0
  14. data/app/commands/decidim/term_customizer/admin/update_translation.rb +65 -0
  15. data/app/commands/decidim/term_customizer/admin/update_translation_set.rb +61 -0
  16. data/app/controllers/decidim/term_customizer/admin/add_translations_controller.rb +72 -0
  17. data/app/controllers/decidim/term_customizer/admin/application_controller.rb +15 -0
  18. data/app/controllers/decidim/term_customizer/admin/translation_sets_controller.rb +107 -0
  19. data/app/controllers/decidim/term_customizer/admin/translations_controller.rb +102 -0
  20. data/app/forms/decidim/term_customizer/admin/translation_form.rb +39 -0
  21. data/app/forms/decidim/term_customizer/admin/translation_key_import_form.rb +15 -0
  22. data/app/forms/decidim/term_customizer/admin/translation_set_constraint_form.rb +58 -0
  23. data/app/forms/decidim/term_customizer/admin/translation_set_form.rb +18 -0
  24. data/app/forms/decidim/term_customizer/admin/translation_set_subject_component_form.rb +12 -0
  25. data/app/forms/decidim/term_customizer/admin/translation_set_subject_form.rb +61 -0
  26. data/app/helpers/decidim/term_customizer/admin/application_helper.rb +13 -0
  27. data/app/models/decidim/term_customizer/application_record.rb +10 -0
  28. data/app/models/decidim/term_customizer/constraint.rb +36 -0
  29. data/app/models/decidim/term_customizer/translation.rb +23 -0
  30. data/app/models/decidim/term_customizer/translation_set.rb +19 -0
  31. data/app/permissions/decidim/term_customizer/admin/permissions.rb +66 -0
  32. data/app/queries/decidim/term_customizer/organization_translation_sets.rb +37 -0
  33. data/app/queries/decidim/term_customizer/set_translations.rb +21 -0
  34. data/app/views/decidim/term_customizer/admin/add_translations/index.html.erb +59 -0
  35. data/app/views/decidim/term_customizer/admin/translation_sets/_constraint_fields.html.erb +58 -0
  36. data/app/views/decidim/term_customizer/admin/translation_sets/_form.html.erb +29 -0
  37. data/app/views/decidim/term_customizer/admin/translation_sets/edit.html.erb +9 -0
  38. data/app/views/decidim/term_customizer/admin/translation_sets/index.html.erb +44 -0
  39. data/app/views/decidim/term_customizer/admin/translation_sets/new.html.erb +9 -0
  40. data/app/views/decidim/term_customizer/admin/translations/_form.html.erb +15 -0
  41. data/app/views/decidim/term_customizer/admin/translations/edit.html.erb +7 -0
  42. data/app/views/decidim/term_customizer/admin/translations/index.html.erb +52 -0
  43. data/app/views/decidim/term_customizer/admin/translations/new.html.erb +7 -0
  44. data/config/locales/en.yml +65 -0
  45. data/config/locales/fi.yml +65 -0
  46. data/config/locales/sv.yml +65 -0
  47. data/lib/decidim/term_customizer/admin.rb +10 -0
  48. data/lib/decidim/term_customizer/admin_engine.rb +46 -0
  49. data/lib/decidim/term_customizer/engine.rb +35 -0
  50. data/lib/decidim/term_customizer/i18n_backend.rb +82 -0
  51. data/lib/decidim/term_customizer/resolver.rb +108 -0
  52. data/lib/decidim/term_customizer/test/factories.rb +43 -0
  53. data/lib/decidim/term_customizer/translation_directory.rb +50 -0
  54. data/lib/decidim/term_customizer/translation_store.rb +67 -0
  55. data/lib/decidim/term_customizer/version.rb +8 -0
  56. data/lib/decidim/term_customizer.rb +21 -0
  57. metadata +170 -0
data/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # Decidim::TermCustomizer
2
+
3
+ [![Build Status](https://travis-ci.com/mainio/decidim-module-term_customizer.svg?branch=master)](https://travis-ci.com/mainio/decidim-module-term_customizer)
4
+ [![codecov](https://codecov.io/gh/mainio/decidim-module-term_customizer/branch/master/graph/badge.svg)](https://codecov.io/gh/mainio/decidim-module-term_customizer)
5
+
6
+ The gem has been developed by [Mainio Tech](https://www.mainiotech.fi/).
7
+
8
+ A [Decidim](https://github.com/decidim/decidim) module to customize the
9
+ localized terms in the system. The module allows administrators to add
10
+ "translation sets" through the admin panel which contain customized terms for
11
+ any module in the system. These sets can be applied against different scopes
12
+ within the system, e.g. the whole system, participatory space scope (e.g. all
13
+ participatory processes or a specific participatory process) or a specific
14
+ component within a participatory space.
15
+
16
+ The term customizations will be only applied to the scope which the admin user
17
+ has defined for the set. Multiple scopes can be defined against a single
18
+ translation set.
19
+
20
+ Example use cases for this module:
21
+
22
+ - Admin wants to change the term "Proposal" to "Idea" in all participatory
23
+ processes within the system.
24
+ - Admin wants to change the terms "accepted" and "rejected" in the proposals
25
+ component to "possible" and "not possible" for a specific participatory
26
+ process.
27
+ - Admin wants to change the term "debate" to "discussion" for all assemblies
28
+ within the system.
29
+
30
+ Currently using this gem requires a bit of technical knowledge in defining the
31
+ terms to be customized. The admin needs to know or find out the translation keys
32
+ for the terms to be customized.
33
+
34
+ ## Installation
35
+
36
+ Add this line to your application's Gemfile:
37
+
38
+ ```ruby
39
+ gem "decidim-term_customizer"
40
+ ```
41
+
42
+ And then execute:
43
+
44
+ ```bash
45
+ $ bundle
46
+ $ bundle exec rails decidim_term_customizer:install:migrations
47
+ $ bundle exec rails db:migrate
48
+ ```
49
+
50
+ To keep the gem up to date, you can use the commands above to also update it.
51
+
52
+ ## Usage
53
+
54
+ - Install the gem.
55
+ - Login to the system as an administrator.
56
+ - Go to the admin panel > Term customizer.
57
+ - Add a new translation set and give it a name (e.g. "All processes"). The
58
+ name of the set is only relevant for the admin to identify what that set is
59
+ used for. It is suggested to give the sets a name that identifies the scope
60
+ where it is used.
61
+ - Apply the set to the scope or scopes where you want these customizations to be
62
+ active.
63
+ - Save the set.
64
+ - Add the translations to the set which you want to customize within the defined
65
+ scope.
66
+
67
+ ## Contributing
68
+
69
+ For instructions how to setup your development environment for Decidim, see [Decidim](https://github.com/decidim/decidim). Also follow Decidim's general
70
+ instructions for development for this project as well.
71
+
72
+ ### Developing
73
+
74
+ To start contributing to this project, first:
75
+
76
+ - Install the basic dependencies (such as Ruby and PostgreSQL)
77
+ - Clone this repository
78
+
79
+ Decidim's main repository also provides a Docker configuration file if you
80
+ prefer to use Docker instead of installing the dependencies locally on your
81
+ machine.
82
+
83
+ You can create the development app by running the following commands after
84
+ cloning this project:
85
+
86
+ ```bash
87
+ $ bundle
88
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rake development_app
89
+ $ npm i
90
+ ```
91
+
92
+ Note that the database user has to have rights to create and drop a database in
93
+ order to create the dummy test app database.
94
+
95
+ Then to test how the module works in Decidim, start the development server:
96
+
97
+ ```bash
98
+ $ cd development_app
99
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rails s
100
+ ```
101
+
102
+ In case you are using [rbenv](https://github.com/rbenv/rbenv) and have the
103
+ [rbenv-vars](https://github.com/rbenv/rbenv-vars) plugin installed for it, you
104
+ can add the environment variables to the root directory of the project in a file
105
+ named `.rbenv-vars`. If these are defined for the environment, you can omit
106
+ defining these in the commands shown above.
107
+
108
+ #### Code Styling
109
+
110
+ Please follow the code styling defined by the different linters that ensure we
111
+ are all talking with the same language collaborating on the same project. This
112
+ project is set to follow the same rules that Decidim itself follows.
113
+
114
+ The following linters are used:
115
+
116
+ - Ruby, [Rubocop](https://rubocop.readthedocs.io/)
117
+ - JS/ES, [eslint](https://eslint.org/)
118
+
119
+ You can run the code styling checks by running the following commands from the
120
+ console:
121
+
122
+ ```
123
+ $ bundle exec rubocop
124
+ $ npm run lint
125
+ ```
126
+
127
+ To ease up following the style guide, you should install the plugins to your
128
+ favorite editor, such as:
129
+
130
+ - Atom
131
+ * [linter-rubocop](https://atom.io/packages/linter-rubocop)
132
+ * [linter-eslint](https://atom.io/packages/linter-eslint)
133
+ - Sublime Text
134
+ * [Sublime RuboCop](https://github.com/pderichs/sublime_rubocop)
135
+ * [SublimeLinter-eslint](https://github.com/SublimeLinter/SublimeLinter-eslint)
136
+ - Visual Studio Code
137
+ * [Rubocop for Visual Studio Code](https://github.com/misogi/vscode-ruby-rubocop)
138
+ * [VS Code ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
139
+
140
+ ### Testing
141
+
142
+ To run the tests run the following in the gem development path:
143
+
144
+ ```bash
145
+ $ bundle
146
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rake test_app
147
+ $ DATABASE_USERNAME=<username> DATABASE_PASSWORD=<password> bundle exec rspec
148
+ ```
149
+
150
+ Note that the database user has to have rights to create and drop a database in
151
+ order to create the dummy test app database.
152
+
153
+ In case you are using [rbenv](https://github.com/rbenv/rbenv) and have the
154
+ [rbenv-vars](https://github.com/rbenv/rbenv-vars) plugin installed for it, you
155
+ can add these environment variables to the root directory of the project in a
156
+ file named `.rbenv-vars`. In this case, you can omit defining these in the
157
+ commands shown above.
158
+
159
+ #### Test code coverage
160
+
161
+ If you want to generate the code coverage report for the tests, you can use
162
+ the `SIMPLECOV=1` environment variable in the rspec command as follows:
163
+
164
+ ```bash
165
+ $ SIMPLECOV=1 bundle exec rspec
166
+ ```
167
+
168
+ This will generate a folder named `coverage` in the project root which contains
169
+ the code coverage report.
170
+
171
+ ### Localization
172
+
173
+ If you would like to see this module in your own language, you can help with its
174
+ translation at Crowdin:
175
+
176
+ https://crowdin.com/project/decidim-term-customizer
177
+
178
+ ## License
179
+
180
+ See [LICENSE-AGPLv3.txt](LICENSE-AGPLv3.txt).
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/dev/common_rake"
4
+
5
+ def install_module(path)
6
+ Dir.chdir(path) do
7
+ system("bundle exec rake decidim_term_customizer:install:migrations")
8
+ system("bundle exec rake db:migrate")
9
+ end
10
+ end
11
+
12
+ def seed_db(path)
13
+ Dir.chdir(path) do
14
+ system("bundle exec rake db:seed")
15
+ end
16
+ end
17
+
18
+ desc "Generates a dummy app for testing"
19
+ task test_app: "decidim:generate_external_test_app" do
20
+ ENV["RAILS_ENV"] = "test"
21
+ install_module("spec/decidim_dummy_app")
22
+ end
23
+
24
+ desc "Generates a development app"
25
+ task :development_app do
26
+ Bundler.with_original_env do
27
+ generate_decidim_app(
28
+ "development_app",
29
+ "--app_name",
30
+ "#{base_app_name}_development_app",
31
+ "--path",
32
+ "..",
33
+ "--recreate_db",
34
+ "--demo"
35
+ )
36
+ end
37
+
38
+ install_module("development_app")
39
+ seed_db("development_app")
40
+ end
41
+
42
+ # Run all tests, include all
43
+ RSpec::Core::RakeTask.new(:spec) do |t|
44
+ t.verbose = false
45
+ end
46
+
47
+ # Run both by default
48
+ task default: [:spec]
@@ -0,0 +1,2 @@
1
+ //= link decidim/term_customizer/admin/translation_sets_admin.js
2
+ //= link decidim/term_customizer/admin/translations_admin.js
@@ -0,0 +1,45 @@
1
+ // = require_self
2
+
3
+ (() => {
4
+ const initConstraintFields = ($section) => {
5
+ const $select = $("select.constraint-subject-selector", $section);
6
+ const $modelSelect = $("select.constraint-subject-model-selector", $section);
7
+
8
+ $select.on(
9
+ "change init",
10
+
11
+ /* @this HTMLElement */
12
+ function() {
13
+ const val = $(this).val();
14
+ $("[data-manifest]", $section).hide();
15
+ $(`[data-manifest="${val}"]`, $section).show();
16
+ }
17
+ ).trigger("init");
18
+
19
+ $modelSelect.on(
20
+ "change init",
21
+
22
+ /* @this HTMLElement */
23
+ function() {
24
+ const $container = $(this).parents(".manifest-container");
25
+ const val = $(this).val();
26
+ $("[data-components]", $container).hide();
27
+ $(`[data-components="${val}"]`, $container).show();
28
+ }
29
+ ).trigger("init");
30
+ };
31
+
32
+ $.fn.constraintSection = function() {
33
+ $(this).each(
34
+
35
+ /**
36
+ * @this HTMLElement
37
+ * @return {void}
38
+ */
39
+ function() {
40
+ const $section = $(this);
41
+ initConstraintFields($section);
42
+ }
43
+ )
44
+ }
45
+ })();
@@ -0,0 +1,103 @@
1
+ ((exports) => {
2
+ const { AutoLabelByPositionComponent, AutoButtonsByPositionComponent, createDynamicFields, createSortList } = exports.DecidimAdmin;
3
+
4
+ const initMultifield = ($wrapper) => {
5
+ const wrapperSelector = `#${$wrapper.attr("id")}`;
6
+ const placeholderId = $wrapper.data("placeholder-id");
7
+
8
+ const fieldSelector = ".multifield-field";
9
+
10
+ const autoLabelByPosition = new AutoLabelByPositionComponent({
11
+ listSelector: `${wrapperSelector} .multifield-field:not(.hidden)`,
12
+ labelSelector: ".card-title span:first",
13
+ onPositionComputed: (el, idx) => {
14
+ $(el).find("input.position-input").val(idx);
15
+ }
16
+ });
17
+
18
+ const autoButtonsByPosition = new AutoButtonsByPositionComponent({
19
+ listSelector: `${wrapperSelector} .multifield-field:not(.hidden)`,
20
+ hideOnFirstSelector: ".move-up-field",
21
+ hideOnLastSelector: ".move-down-field"
22
+ });
23
+
24
+ const createSortableList = () => {
25
+ createSortList(`${wrapperSelector} .fields-list:not(.published)`, {
26
+ handle: ".multifield-field-divider",
27
+ placeholder: '<div style="border-style: dashed; border-color: #000"></div>',
28
+ forcePlaceholderSize: true,
29
+ onSortUpdate: () => { autoLabelByPosition.run() }
30
+ });
31
+ };
32
+
33
+ const hideDeletedSection = ($target) => {
34
+ const inputDeleted = $target.find("input[name$=\\[deleted\\]]").val();
35
+
36
+ if (inputDeleted === "true") {
37
+ $target.addClass("hidden");
38
+ $target.hide();
39
+ }
40
+ }
41
+
42
+ createDynamicFields({
43
+ placeholderId: placeholderId,
44
+ wrapperSelector: wrapperSelector,
45
+ containerSelector: ".multifield-fields-list",
46
+ fieldSelector: fieldSelector,
47
+ addFieldButtonSelector: ".add-field",
48
+ removeFieldButtonSelector: ".remove-field",
49
+ moveUpFieldButtonSelector: ".move-up-field",
50
+ moveDownFieldButtonSelector: ".move-down-field",
51
+ onAddField: ($newField) => {
52
+ createSortableList();
53
+
54
+ autoLabelByPosition.run();
55
+ autoButtonsByPosition.run();
56
+
57
+ $wrapper.trigger("add-field-section", $newField);
58
+ },
59
+ onRemoveField: () => {
60
+ autoLabelByPosition.run();
61
+ autoButtonsByPosition.run();
62
+ },
63
+ onMoveUpField: () => {
64
+ autoLabelByPosition.run();
65
+ autoButtonsByPosition.run();
66
+ },
67
+ onMoveDownField: () => {
68
+ autoLabelByPosition.run();
69
+ autoButtonsByPosition.run();
70
+ }
71
+ });
72
+
73
+ createSortableList();
74
+
75
+ $(fieldSelector).each((idx, el) => {
76
+ const $target = $(el);
77
+
78
+ hideDeletedSection($target);
79
+ });
80
+
81
+ autoLabelByPosition.run();
82
+ autoButtonsByPosition.run();
83
+ }
84
+
85
+ $.fn.multifield = function() {
86
+ $(this).each(
87
+
88
+ /**
89
+ * @this HTMLElement
90
+ * @return {void}
91
+ */
92
+ function() {
93
+ const $elem = $(this);
94
+ let id = $elem.attr("id");
95
+ if (!id || id.length < 1) {
96
+ id = `multifield-${Math.random().toString(16).slice(2)}`;
97
+ $elem.attr("id", id);
98
+ }
99
+ initMultifield($elem);
100
+ }
101
+ )
102
+ }
103
+ })(window);
@@ -0,0 +1,5 @@
1
+ // = require decidim/admin/auto_buttons_by_position.component
2
+ // = require decidim/admin/auto_label_by_position.component
3
+ // = require decidim/admin/dynamic_fields.component
4
+ // = require decidim/admin/sort_list.component
5
+ // = require decidim/term_customizer/admin/multifield/component
@@ -0,0 +1,19 @@
1
+ // = require decidim/term_customizer/admin/multifield
2
+ // = require decidim/term_customizer/admin/constraint_fields
3
+ // = require_self
4
+
5
+ $(() => {
6
+ const $fields = $("form.translation-sets-form .multifield-fields");
7
+
8
+ $fields.multifield();
9
+ $fields.on(
10
+ "add-field-section",
11
+
12
+ /* @this HTMLElement */
13
+ function(ev, newField) {
14
+ $(newField).constraintSection();
15
+ }
16
+ );
17
+
18
+ $(".constraints-list", $fields).constraintSection();
19
+ });
@@ -0,0 +1,72 @@
1
+ // = require_self
2
+
3
+ $(() => {
4
+ const $search = $("#data_picker-autocomplete");
5
+ const $results = $("#add-translations-results");
6
+ const $template = $("template", $results);
7
+ const $form = $search.parents("form");
8
+ let xhr = null;
9
+ let currentSearch = "";
10
+
11
+ $search.on("keyup", function() {
12
+ currentSearch = $search.val();
13
+ });
14
+
15
+ $search.autoComplete({
16
+ minChars: 2,
17
+ cache: 0,
18
+ source: function(term, response) {
19
+ try {
20
+ xhr.abort();
21
+ } catch (exception) { xhr = null; }
22
+
23
+ const url = $form.attr("action");
24
+ xhr = $.getJSON(
25
+ url,
26
+ $form.serializeArray(),
27
+ function(data) { response(data); }
28
+ );
29
+ },
30
+ renderItem: function (item, search) {
31
+ const sanitizedSearch = search.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
32
+ const re = new RegExp(`(${sanitizedSearch.split(" ").join("|")})`, "gi");
33
+ const modelId = item[0];
34
+ const title = item[1];
35
+ const val = `${title} - ${modelId}`;
36
+ return `<div class="autocomplete-suggestion" data-model-id="${modelId}" data-val="${title}">${val.replace(re, "<b>$1</b>")}</div>`;
37
+ },
38
+ onSelect: function(event, term, item) {
39
+ const $suggestions = $search.data("sc");
40
+ const modelId = item.data("modelId");
41
+ const title = item.data("val");
42
+
43
+ let template = $template.html();
44
+ template = template.replace(new RegExp("{{translation_key}}", "g"), modelId);
45
+ template = template.replace(new RegExp("{{translation_term}}", "g"), title);
46
+ const $newRow = $(template);
47
+ $("table tbody", $results).append($newRow);
48
+ $results.removeClass("hide");
49
+
50
+ // Add it to the autocomplete form
51
+ const $field = $(`<input type="hidden" name="keys[]" value="${modelId}">`);
52
+ $form.append($field);
53
+
54
+ // Listen to the click event on the remove button
55
+ $(".remove-translation-key", $newRow).on("click", function(ev) {
56
+ ev.preventDefault();
57
+ $newRow.remove();
58
+ $field.remove();
59
+
60
+ if ($("table tbody tr", $results).length < 1) {
61
+ $results.addClass("hide");
62
+ }
63
+ });
64
+
65
+ $search.val(currentSearch);
66
+ setTimeout(function() {
67
+ $(`[data-model-id="${modelId}"]`, $suggestions).remove();
68
+ $suggestions.show();
69
+ }, 20);
70
+ }
71
+ });
72
+ });