decidim-term_customizer 0.16.6 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/term_customizer/admin/translations_admin_bulk.js.es6 +113 -0
  3. data/app/commands/decidim/term_customizer/admin/destroy_translations.rb +40 -0
  4. data/app/commands/decidim/term_customizer/admin/import_set_translations.rb +139 -0
  5. data/app/controllers/decidim/term_customizer/admin/translation_sets_controller.rb +11 -0
  6. data/app/controllers/decidim/term_customizer/admin/translations_controller.rb +23 -0
  7. data/app/controllers/decidim/term_customizer/admin/translations_destroys_controller.rb +54 -0
  8. data/app/forms/decidim/term_customizer/admin/translations_destroy_form.rb +46 -0
  9. data/app/forms/decidim/term_customizer/admin/translations_import_form.rb +48 -0
  10. data/app/helpers/decidim/term_customizer/admin/translations_helper.rb +21 -0
  11. data/app/jobs/decidim/term_customizer/admin/export_job.rb +19 -0
  12. data/app/permissions/decidim/term_customizer/admin/permissions.rb +11 -1
  13. data/app/views/decidim/term_customizer/admin/add_translations/index.html.erb +26 -6
  14. data/app/views/decidim/term_customizer/admin/translation_sets/_form.html.erb +25 -1
  15. data/app/views/decidim/term_customizer/admin/translation_sets/index.html.erb +39 -33
  16. data/app/views/decidim/term_customizer/admin/translations/_export_dropdown.html.erb +8 -0
  17. data/app/views/decidim/term_customizer/admin/translations/_form.html.erb +27 -1
  18. data/app/views/decidim/term_customizer/admin/translations/bulk_actions/_destroy.html.erb +13 -0
  19. data/app/views/decidim/term_customizer/admin/translations/bulk_actions/_dropdown.html.erb +26 -0
  20. data/app/views/decidim/term_customizer/admin/translations/index.html.erb +66 -38
  21. data/app/views/decidim/term_customizer/admin/translations/new_import.html.erb +40 -0
  22. data/app/views/decidim/term_customizer/admin/translations_destroys/new.html.erb +36 -0
  23. data/config/locales/ca.yml +72 -26
  24. data/config/locales/en.yml +109 -26
  25. data/config/locales/es.yml +72 -26
  26. data/config/locales/fi.yml +72 -26
  27. data/config/locales/fr.yml +72 -26
  28. data/config/locales/sv.yml +72 -26
  29. data/lib/decidim/term_customizer.rb +4 -0
  30. data/lib/decidim/term_customizer/admin_engine.rb +11 -1
  31. data/lib/decidim/term_customizer/import.rb +12 -0
  32. data/lib/decidim/term_customizer/import/importer.rb +69 -0
  33. data/lib/decidim/term_customizer/import/importer_factory.rb +17 -0
  34. data/lib/decidim/term_customizer/import/parser.rb +49 -0
  35. data/lib/decidim/term_customizer/import/readers.rb +39 -0
  36. data/lib/decidim/term_customizer/import/readers/base.rb +36 -0
  37. data/lib/decidim/term_customizer/import/readers/csv.rb +23 -0
  38. data/lib/decidim/term_customizer/import/readers/json.rb +25 -0
  39. data/lib/decidim/term_customizer/import/readers/xls.rb +25 -0
  40. data/lib/decidim/term_customizer/translation_import_collection.rb +71 -0
  41. data/lib/decidim/term_customizer/translation_parser.rb +13 -0
  42. data/lib/decidim/term_customizer/translation_serializer.rb +28 -0
  43. data/lib/decidim/term_customizer/version.rb +2 -2
  44. metadata +39 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0adf060c3c243ebc5ac49b06b470a5ca036d6fe04ecee991775e5da5d464a65
4
- data.tar.gz: eb46370b1bf5186def7e3aa10b7b93c50ac5704f858b665fe4dffb62de877261
3
+ metadata.gz: 16eec8f526df47ea82ef79a644e4afd290f64e7e1f7aa2fea64723ad7a30d6b9
4
+ data.tar.gz: 93c4464c8c3e2b3437df94b25e0f457e60ad1f18240c1fbc6609421600888805
5
5
  SHA512:
6
- metadata.gz: 42bff42d6fe263926813ed5f3e56822be28d4a483784d8097572a891e9a86152ff3ae3e432f4475fab443bb4a01f6ac65d6ae33d5474f155cbc3f095604c9ea6
7
- data.tar.gz: 8e8c1cee2e2350183ee9bbf5e6f3f185f56eb5a720742526c3acd367ad10bf7d7bcfe5aa437c3aadad9d32e70cbb4ced5712cefdf00eccba68327cf988002f66
6
+ metadata.gz: 4efdea5c3cca3ec8cd9f1e4d068b63f587b84f1ec9c175f9be2d1ff5832b688c467655596fcc85be7d2c2d6f22acffcf3c07913910c6a74151a4b5e4bb35aff6
7
+ data.tar.gz: 8864a676c3562377cc114556c058cbe50983d4b4691f9007130de45d3addbc8bb0ab4dd670c11e2a1412e53b748f06a06b78d23f652b379808ee499be24ce99c
@@ -0,0 +1,113 @@
1
+ // = require_self
2
+
3
+ /* eslint-disable no-invalid-this */
4
+ $(document).ready(function () {
5
+ let selectedTranslationsCount = function() {
6
+ return $(".table-list .js-check-all-translation:checked").length
7
+ }
8
+
9
+ window.selectedTranslationsCountUpdate = function() {
10
+ if (selectedTranslationsCount() === 0) {
11
+ $("#js-selected-translation-count").text("")
12
+ } else {
13
+ $("#js-selected-translation-count").text(selectedTranslationsCount());
14
+ }
15
+ }
16
+
17
+ let showBulkActionsButton = function() {
18
+ if (selectedTranslationsCount() > 0) {
19
+ $("#js-bulk-actions-button").removeClass("hide");
20
+ }
21
+ }
22
+
23
+ let hideBulkActionsButton = function(force = false) {
24
+ if (selectedTranslationsCount() === 0 || force === true) {
25
+ $("#js-bulk-actions-button").addClass("hide");
26
+ $("#js-bulk-actions-dropdown").removeClass("is-open");
27
+ }
28
+ }
29
+
30
+ window.showOtherActionsButtons = function() {
31
+ $("#js-other-actions-wrapper").removeClass("hide");
32
+ }
33
+
34
+ const hideOtherActionsButtons = function() {
35
+ $("#js-other-actions-wrapper").addClass("hide");
36
+ }
37
+
38
+ window.hideBulkActionForms = function() {
39
+ return $(".js-bulk-action-form").addClass("hide");
40
+ }
41
+
42
+ if ($(".js-bulk-action-form").length) {
43
+ window.hideBulkActionForms();
44
+ $("#js-bulk-actions-button").addClass("hide");
45
+
46
+ $("#js-bulk-actions-dropdown ul li button").click(function(ev) {
47
+ ev.preventDefault();
48
+ let action = $(ev.target).data("action");
49
+
50
+ if (action) {
51
+ $(`#js-form-${action}`).submit(function() {
52
+ $(".layout-content > .callout-wrapper").html("");
53
+ })
54
+
55
+ $(`#js-${action}-actions`).removeClass("hide");
56
+ hideBulkActionsButton(true);
57
+ hideOtherActionsButtons();
58
+ }
59
+ })
60
+
61
+ // select all checkboxes
62
+ $(".js-check-all").change(function() {
63
+ $(".js-check-all-translation").prop("checked", $(this).prop("checked"));
64
+
65
+ if ($(this).prop("checked")) {
66
+ $(".js-check-all-translation").closest("tr").addClass("selected");
67
+ showBulkActionsButton();
68
+ } else {
69
+ $(".js-check-all-translation").closest("tr").removeClass("selected");
70
+ hideBulkActionsButton();
71
+ }
72
+
73
+ window.selectedTranslationsCountUpdate();
74
+ });
75
+
76
+ // translation checkbox change
77
+ $(".table-list").on("change", ".js-check-all-translation", function () {
78
+ let translationId = $(this).val()
79
+ let checked = $(this).prop("checked")
80
+
81
+ // uncheck "select all", if one of the listed checkbox item is unchecked
82
+ if ($(this).prop("checked") === false) {
83
+ $(".js-check-all").prop("checked", false);
84
+ }
85
+ // check "select all" if all checkbox translations are checked
86
+ if ($(".js-check-all-translation:checked").length === $(".js-check-all-translation").length) {
87
+ $(".js-check-all").prop("checked", true);
88
+ showBulkActionsButton();
89
+ }
90
+
91
+ if ($(this).prop("checked")) {
92
+ showBulkActionsButton();
93
+ $(this).closest("tr").addClass("selected");
94
+ } else {
95
+ hideBulkActionsButton();
96
+ $(this).closest("tr").removeClass("selected");
97
+ }
98
+
99
+ if ($(".js-check-all-translation:checked").length === 0) {
100
+ hideBulkActionsButton();
101
+ }
102
+
103
+ $(".js-bulk-action-form").find(`.js-translation-id-${translationId}`).prop("checked", checked);
104
+ window.selectedTranslationCountUpdate();
105
+ });
106
+
107
+ $(".js-cancel-bulk-action").on("click", function () {
108
+ window.hideBulkActionForms()
109
+ showBulkActionsButton();
110
+ window.showOtherActionsButtons();
111
+ });
112
+ }
113
+ });
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module TermCustomizer
5
+ module Admin
6
+ # A command with all the business logic when an admin destroys
7
+ # translations from a translation set.
8
+ class DestroyTranslations < Rectify::Command
9
+ # Public: Initializes the command.
10
+ #
11
+ # form - A form object with the params.
12
+ def initialize(form)
13
+ @form = form
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid.
19
+ # - :invalid if the form wasn't valid and we couldn't proceed.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ return broadcast(:invalid) unless form.valid?
24
+
25
+ destroy_translations
26
+
27
+ broadcast(:ok)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :form
33
+
34
+ def destroy_translations
35
+ form.translations.destroy_all
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zip"
4
+
5
+ module Decidim
6
+ module TermCustomizer
7
+ module Admin
8
+ # A command with all the business logic when importing translations to a
9
+ # set from a file submitted through the form. The file may be one of the
10
+ # supported import formats or a ZIP file containing a supported import
11
+ # file.
12
+ class ImportSetTranslations < Rectify::Command
13
+ # Public: Initializes the command.
14
+ #
15
+ # form - A form object with the params.
16
+ # translation_set - The translation set to which the import is
17
+ # performed.
18
+ def initialize(form, translation_set)
19
+ @form = form
20
+ @translation_set = translation_set
21
+ end
22
+
23
+ # Executes the command. Broadcasts these events:
24
+ #
25
+ # - :ok when everything is valid.
26
+ # - :invalid if the form wasn't valid and we couldn't proceed.
27
+ #
28
+ # Returns nothing.
29
+ def call
30
+ return broadcast(:invalid) if form.invalid?
31
+
32
+ @translations = import_translations
33
+
34
+ if @translations.length.positive?
35
+ broadcast(:ok, @translations)
36
+ else
37
+ broadcast(:invalid)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :form, :translation_set
44
+
45
+ # Private: Handles the import of either a zip file or a regular import
46
+ # file, one of the supported reader formats based on the file provided
47
+ # by the import form.
48
+ #
49
+ # Returns Array or nil. The returned value is an array of the imported
50
+ # translations when the import is successful, otherwise nil.
51
+ def import_translations
52
+ return import_zip(form.file_path) if form.zip_file?
53
+
54
+ import_file(form.file_path, form.mime_type)
55
+ end
56
+
57
+ # Private: Handles the import of a regular import file, one of the
58
+ # supported reader formats. Will iterate over the whole imported data
59
+ # array and save all records to the database.
60
+ #
61
+ # filepath - A filepath with the data to be imported.
62
+ # mime_type - The mime type of the provided file.
63
+ #
64
+ # Returns Array or nil. The returned value is an array of the imported
65
+ # translations when the import is successful, otherwise nil.
66
+ def import_file(filepath, mime_type)
67
+ importer_for(filepath, mime_type).import do |records|
68
+ import = TranslationImportCollection.new(
69
+ translation_set,
70
+ records,
71
+ form.current_organization.available_locales
72
+ )
73
+
74
+ return translation_set.translations.create(import.import_attributes)
75
+ end
76
+
77
+ nil
78
+ end
79
+
80
+ # Private: Parses through the provided zip file and searches for the
81
+ # first file with one of the supported import formats. Once found,
82
+ # creates an extracted temp file of that file and passes that back to
83
+ # the import method for the final import to be executed on.
84
+ #
85
+ # If no supported import file is found at the root of the zip archive,
86
+ # nothing will be done and false will be returned.
87
+ #
88
+ # filepath - A filepath with the zip file containing the actual import
89
+ # file.
90
+ #
91
+ # Returns Array or nil. The returned value is an array of the imported
92
+ # translations when the import is successful, otherwise nil.
93
+ def import_zip(filepath)
94
+ Zip::File.open(filepath) do |zip_file|
95
+ zip_file.each do |entry|
96
+ next unless entry.file?
97
+
98
+ ext = File.extname(entry.name)[1..-1]
99
+ mime_type = TranslationsImportForm::ACCEPTED_MIME_TYPES[ext.to_sym]
100
+ next if mime_type.nil?
101
+
102
+ collection = nil
103
+
104
+ file = Tempfile.new("translations_import.#{ext}")
105
+ begin
106
+ content = entry.get_input_stream.read.force_encoding("UTF-8")
107
+ file.write(content)
108
+ file.close
109
+
110
+ collection = import_file(file.path, mime_type)
111
+ ensure
112
+ file.unlink
113
+ end
114
+
115
+ return collection
116
+ end
117
+ end
118
+
119
+ nil
120
+ end
121
+
122
+ # Private: Creates a new imported for the provided file with the given
123
+ # mime type.
124
+ #
125
+ # filepath - A filepath with the data to be imported.
126
+ # mime_type - The mime type of the provided file.
127
+ #
128
+ # Returns Decidim::TermCustomizer::Import::Importer.
129
+ def importer_for(filepath, mime_type)
130
+ Import::ImporterFactory.build(
131
+ filepath,
132
+ mime_type,
133
+ TranslationParser
134
+ )
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -75,6 +75,17 @@ module Decidim
75
75
  redirect_to translation_sets_path
76
76
  end
77
77
 
78
+ def export
79
+ enforce_permission_to :export, :translation_set, translation_set: set
80
+ name = "set-translations"
81
+
82
+ ExportJob.perform_later(current_user, set, name, params[:format] || "json")
83
+
84
+ flash[:notice] = I18n.t("exports.notice", scope: "decidim.admin")
85
+
86
+ redirect_to translation_set_translations_path(set)
87
+ end
88
+
78
89
  private
79
90
 
80
91
  def sets
@@ -79,6 +79,29 @@ module Decidim
79
79
  redirect_to translation_set_translations_path(set)
80
80
  end
81
81
 
82
+ def new_import
83
+ enforce_permission_to :import, :translation_set, translation_set: set
84
+
85
+ @import = form(Admin::TranslationsImportForm).instance
86
+ end
87
+
88
+ def import
89
+ enforce_permission_to :import, :translation_set, translation_set: set
90
+
91
+ @import = form(Admin::TranslationsImportForm).from_params(params)
92
+ ImportSetTranslations.call(@import, set) do
93
+ on(:ok) do
94
+ flash[:notice] = I18n.t("translations.import.success", scope: "decidim.term_customizer.admin")
95
+ redirect_to translation_set_translations_path(set)
96
+ end
97
+
98
+ on(:invalid) do
99
+ flash.now[:alert] = I18n.t("translations.import.error", scope: "decidim.term_customizer.admin")
100
+ render action: "new_import"
101
+ end
102
+ end
103
+ end
104
+
82
105
  private
83
106
 
84
107
  def translation_set
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module TermCustomizer
5
+ module Admin
6
+ class TranslationsDestroysController < Admin::ApplicationController
7
+ helper_method :set
8
+
9
+ before_action :set_form
10
+
11
+ def new
12
+ enforce_permission_to :destroy, :translations, translation_set: set
13
+ end
14
+
15
+ def destroy
16
+ enforce_permission_to :destroy, :translations, translation_set: set
17
+
18
+ Admin::DestroyTranslations.call(@form) do
19
+ on(:ok) do
20
+ flash[:notice] = I18n.t("translations_destroys.destroy.success", scope: "decidim.term_customizer.admin")
21
+ redirect_to translation_set_translations_path(set)
22
+ end
23
+
24
+ on(:invalid) do
25
+ if @form.translations.count < 1
26
+ flash[:alert] = I18n.t("translations_destroys.destroy.error", scope: "decidim.term_customizer.admin")
27
+ redirect_to translation_set_translations_path(set)
28
+ else
29
+ flash.now[:alert] = I18n.t("translations_destroys.destroy.error", scope: "decidim.term_customizer.admin")
30
+ render action: "new"
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def set_form
39
+ @form = form(Admin::TranslationsDestroyForm).from_params(
40
+ params
41
+ ).with_context(translation_set: set)
42
+ end
43
+
44
+ def translation_set
45
+ @translation_set ||= OrganizationTranslationSets.new(
46
+ current_organization
47
+ ).query.find(params[:translation_set_id])
48
+ end
49
+
50
+ alias set translation_set
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module TermCustomizer
5
+ module Admin
6
+ # A form object to be used when admin users wants to destroy multiple
7
+ # translations at once.
8
+ class TranslationsDestroyForm < Decidim::Form
9
+ mimic :translations_destroy
10
+
11
+ delegate :translation_set, to: :context, prefix: false, allow_nil: true
12
+
13
+ attribute :translation_ids, Array
14
+ validates :translation_set, :translations, presence: true
15
+
16
+ # Translations for all locales corresponding the translations passed
17
+ # to the form.
18
+ def translations
19
+ return [] unless translation_set
20
+
21
+ @translations ||= translation_set.translations.where(
22
+ key: translation_keys
23
+ )
24
+ end
25
+
26
+ # Only the translations passed with the IDs (current locale).
27
+ def translations_current
28
+ return [] unless translation_set
29
+
30
+ @translations_current ||= translation_set.translations.where(
31
+ id: translation_ids
32
+ ).uniq
33
+ end
34
+
35
+ private
36
+
37
+ # Because we want to delete all locales for the translations to be
38
+ # deleted, find the corresponding keys for the translation IDs passed
39
+ # from the UI (current locale).
40
+ def translation_keys
41
+ @translation_keys ||= translations_current.map(&:key).uniq
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end