decidim-term_customizer 0.16.0

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