diff --git a/Gemfile b/Gemfile index dc5e1a0e7..6e8cf5c24 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,7 @@ gem "savon", "~> 2.12.0" gem "sitemap_generator", "~> 6.0.1" gem "social-share-button", "~> 1.1" gem "sprockets", "~> 3.7.2" +gem "translator-text", "~> 0.1.0" gem "turbolinks", "~> 2.5.3" gem "turnout", "~> 2.4.0" gem "uglifier", "~> 4.1.2" diff --git a/Gemfile.lock b/Gemfile.lock index e90a4c4a0..582f12578 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,6 +168,31 @@ GEM devise (>= 4.0) diff-lcs (1.3) docile (1.3.1) + dry-configurable (0.7.0) + concurrent-ruby (~> 1.0) + dry-container (0.6.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.1, >= 0.1.3) + dry-core (0.4.7) + concurrent-ruby (~> 1.0) + dry-equalizer (0.2.1) + dry-inflector (0.1.2) + dry-logic (0.4.2) + dry-container (~> 0.2, >= 0.2.6) + dry-core (~> 0.2) + dry-equalizer (~> 0.2) + dry-struct (0.5.1) + dry-core (~> 0.4, >= 0.4.3) + dry-equalizer (~> 0.2) + dry-types (~> 0.13) + ice_nine (~> 0.11) + dry-types (0.13.3) + concurrent-ruby (~> 1.0) + dry-container (~> 0.3) + dry-core (~> 0.4, >= 0.4.4) + dry-equalizer (~> 0.2) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 0.4, >= 0.4.2) email_spec (2.1.1) htmlentities (~> 4.3.3) launchy (~> 2.1) @@ -223,6 +248,9 @@ GEM highline (2.0.0) html_tokenizer (0.0.7) htmlentities (4.3.4) + httparty (0.17.0) + mime-types (~> 3.0) + multi_xml (>= 0.5.2) httpi (2.4.4) rack socksify @@ -237,6 +265,7 @@ GEM parser (>= 2.2.3.0) rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) + ice_nine (0.11.2) initialjs-rails (0.2.0.5) railties (>= 3.1, < 6.0) invisible_captcha (0.10.0) @@ -493,6 +522,9 @@ GEM tilt (2.0.8) tins (1.16.3) tomlrb (1.2.7) + translator-text (0.1.0) + dry-struct (~> 0.5.0) + httparty (~> 0.15) turbolinks (2.5.4) coffee-rails turnout (2.4.1) @@ -614,6 +646,7 @@ DEPENDENCIES spring (~> 2.0.1) spring-commands-rspec (~> 1.0.4) sprockets (~> 3.7.2) + translator-text (~> 0.1.0) turbolinks (~> 2.5.3) turnout (~> 2.4.0) uglifier (~> 4.1.2) diff --git a/app/assets/javascripts/forms.js.coffee b/app/assets/javascripts/forms.js.coffee index 34ea71916..5cf403e59 100644 --- a/app/assets/javascripts/forms.js.coffee +++ b/app/assets/javascripts/forms.js.coffee @@ -40,14 +40,15 @@ App.Forms = hideOrShowFieldsAfterSelection: -> $("[name='progress_bar[kind]']").on change: -> - title_field = $("[name^='progress_bar'][name$='[title]']").parent() + locale = App.Globalize.selected_language() + title_field = $(".translatable-fields[data-locale=#{locale}]") if this.value == "primary" title_field.hide() - $("#globalize_locales").hide() + $(".globalize-languages").hide() else title_field.show() - $("#globalize_locales").show() + $(".globalize-languages").show() $("[name='progress_bar[kind]']").change() diff --git a/app/assets/javascripts/globalize.js.coffee b/app/assets/javascripts/globalize.js.coffee index 1238402a2..af320e2e1 100644 --- a/app/assets/javascripts/globalize.js.coffee +++ b/app/assets/javascripts/globalize.js.coffee @@ -1,37 +1,47 @@ App.Globalize = + selected_language: -> + $("#select_language").val() + display_locale: (locale) -> App.Globalize.enable_locale(locale) - $(".js-globalize-locale-link").each -> - if $(this).data("locale") == locale - $(this).show() - App.Globalize.highlight_locale($(this)) - $(".js-globalize-locale option:selected").removeAttr("selected") - return + App.Globalize.add_language(locale) + $(".js-add-language option:selected").removeAttr("selected") display_translations: (locale) -> + $(".js-select-language option[value=#{locale}]").prop("selected", true) $(".js-globalize-attribute").each -> if $(this).data("locale") == locale $(this).show() else $(this).hide() $(".js-delete-language").hide() - $("#js_delete_#{locale}").show() + $(".js-delete-" + locale).show() - highlight_locale: (element) -> - $(".js-globalize-locale-link").removeClass("is-active") - element.addClass("is-active") + add_language: (locale) -> + language_option = $(".js-add-language [value=#{locale}]") + if $(".js-select-language option[value=#{locale}]").length == 0 + option = new Option(language_option.text(), language_option.val()) + $(".js-select-language").append(option) + $(".js-select-language option[value=#{locale}]").prop("selected", true) remove_language: (locale) -> $(".js-globalize-attribute[data-locale=#{locale}]").each -> $(this).val("").hide() - if CKEDITOR.instances[$(this).attr("id")] - CKEDITOR.instances[$(this).attr("id")].setData("") - $(".js-globalize-locale-link[data-locale=#{locale}]").hide() - next = $(".js-globalize-locale-link:visible").first() - App.Globalize.highlight_locale(next) - App.Globalize.display_translations(next.data("locale")) + App.Globalize.resetEditor(this) + + $(".js-select-language option[value=#{locale}]").remove() + next = $(".js-select-language option:not([value=''])").first() + App.Globalize.display_translations(next.val()) App.Globalize.disable_locale(locale) + App.Globalize.update_description() + + if $(".js-select-language option").length == 1 + $(".js-select-language option").prop("selected", true) + + resetEditor: (element) -> + if CKEDITOR.instances[$(element).attr("id")] + CKEDITOR.instances[$(element).attr("id")].setData("") enable_locale: (locale) -> App.Globalize.destroy_locale_field(locale).val(false) @@ -43,8 +53,8 @@ App.Globalize = enabled_locales: -> $.map( - $(".js-globalize-locale-link:visible"), - (element) -> $(element).data("locale") + $(".js-select-language:first option:not([value=''])"), + (element) -> $(element).val() ) destroy_locale_field: (locale) -> @@ -54,20 +64,34 @@ App.Globalize = $("#enabled_translations_#{locale}") refresh_visible_translations: -> - locale = $(".js-globalize-locale-link.is-active").data("locale") + locale = $(".js-select-language").val() App.Globalize.display_translations(locale) + update_description: -> + count = App.Globalize.enabled_locales().length + description = App.Globalize.language_description(count) + $(".js-languages-description").html(description) + $(".js-languages-count").text(count) + + language_description: (count) -> + switch count + when 0 then $(".globalize-languages").data("zero-languages-description") + when 1 then $(".globalize-languages").data("one-languages-description") + else $(".globalize-languages").data("other-languages-description") + initialize: -> - $(".js-globalize-locale").on "change", -> - App.Globalize.display_translations($(this).val()) - App.Globalize.display_locale($(this).val()) - - $(".js-globalize-locale-link").on "click", -> - locale = $(this).data("locale") + $(".js-add-language").on "change", -> + locale = $(this).val() App.Globalize.display_translations(locale) - App.Globalize.highlight_locale($(this)) + App.Globalize.display_locale(locale) + App.Globalize.update_description() - $(".js-delete-language").on "click", -> + $(".js-select-language").on "change", -> + locale = $(this).val() + App.Globalize.display_translations(locale) + + $(".js-delete-language").on "click", (e) -> + e.preventDefault() locale = $(this).data("locale") $(this).hide() App.Globalize.remove_language(locale) diff --git a/app/assets/javascripts/suggest.js.coffee b/app/assets/javascripts/suggest.js.coffee index 854f7ccf1..c8e242cf6 100644 --- a/app/assets/javascripts/suggest.js.coffee +++ b/app/assets/javascripts/suggest.js.coffee @@ -9,11 +9,15 @@ App.Suggest = callback = -> $.ajax url: $this.data("js-url") - data: { search: $this.val() }, - type: "GET", + data: + search: $this.val() + type: "GET" dataType: "html" success: (stHtml) -> js_suggest_selector = $this.data("js-suggest") + if js_suggest_selector.startsWith(".") + locale = $this.closest(".translatable-fields").data("locale") + js_suggest_selector += "[data-locale=#{locale}]" $(js_suggest_selector).html(stHtml) timer = null diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 8e7334935..c65fbe03c 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -197,6 +197,10 @@ a { padding-top: $line-height; } +.padding-top { + padding-top: $line-height; +} + .light { background: $light; } @@ -474,6 +478,26 @@ header { } } + .remote-translations-button { + + &.callout { + margin: 0; + padding: rem-calc(6); + + [type="submit"] { + background: none; + border: 0; + cursor: pointer; + font-weight: bold; + color: $brand; + + &:hover { + text-decoration: underline; + } + } + } + } + .external-links { float: none; padding: rem-calc(6) 0; @@ -1060,6 +1084,19 @@ form { } } +.translatable-fields { + + &.highlight { + display: inline-block; + padding-top: $line-height; + width: 100%; + } +} + +.translation-locale { + padding-top: $line-height; +} + // 07. Callout // ----------- diff --git a/app/controllers/admin/budget_investments_controller.rb b/app/controllers/admin/budget_investments_controller.rb index c1bfbcf35..a5d2ee21a 100644 --- a/app/controllers/admin/budget_investments_controller.rb +++ b/app/controllers/admin/budget_investments_controller.rb @@ -3,6 +3,7 @@ class Admin::BudgetInvestmentsController < Admin::BaseController include CommentableActions include DownloadSettingsHelper include ChangeLogHelper + include Translatable feature_flag :budgets @@ -81,17 +82,16 @@ class Admin::BudgetInvestmentsController < Admin::BaseController end def load_investments - @investments = Budget::Investment.scoped_filter(params, @current_filter) - .order_filter(params) - + @investments = Budget::Investment.scoped_filter(params, @current_filter).order_filter(params) + @investments = Kaminari.paginate_array(@investments) if @investments.kind_of?(Array) @investments = @investments.page(params[:page]) unless request.format.csv? end def budget_investment_params - params.require(:budget_investment) - .permit(:title, :description, :external_url, :heading_id, :administrator_id, :tag_list, + attributes = [:external_url, :heading_id, :administrator_id, :tag_list, :valuation_tag_list, :incompatible, :visible_to_valuators, :selected, - :milestone_tag_list, tracker_ids: [], valuator_ids: [], valuator_group_ids: []) + :milestone_tag_list, tracker_ids: [], valuator_ids: [], valuator_group_ids: []] + params.require(:budget_investment).permit(attributes, translation_params(Budget::Investment)) end def load_budget diff --git a/app/controllers/admin/hidden_budget_investments_controller.rb b/app/controllers/admin/hidden_budget_investments_controller.rb index 439f3c722..f34818361 100644 --- a/app/controllers/admin/hidden_budget_investments_controller.rb +++ b/app/controllers/admin/hidden_budget_investments_controller.rb @@ -19,7 +19,7 @@ class Admin::HiddenBudgetInvestmentsController < Admin::BaseController end def restore - @investment.restore + @investment.restore(recursive: true) @investment.ignore_flag Activity.log(current_user, :restore, @investment) redirect_to request.query_parameters.merge(action: :index) diff --git a/app/controllers/admin/hidden_comments_controller.rb b/app/controllers/admin/hidden_comments_controller.rb index c3e3bdea9..561fe3aef 100644 --- a/app/controllers/admin/hidden_comments_controller.rb +++ b/app/controllers/admin/hidden_comments_controller.rb @@ -14,7 +14,7 @@ class Admin::HiddenCommentsController < Admin::BaseController end def restore - @comment.restore + @comment.restore(recursive: true) @comment.ignore_flag Activity.log(current_user, :restore, @comment) redirect_to request.query_parameters.merge(action: :index) diff --git a/app/controllers/admin/hidden_debates_controller.rb b/app/controllers/admin/hidden_debates_controller.rb index 77d30b488..3e93db0a6 100644 --- a/app/controllers/admin/hidden_debates_controller.rb +++ b/app/controllers/admin/hidden_debates_controller.rb @@ -17,7 +17,7 @@ class Admin::HiddenDebatesController < Admin::BaseController end def restore - @debate.restore + @debate.restore!(recursive: true) @debate.ignore_flag Activity.log(current_user, :restore, @debate) redirect_to request.query_parameters.merge(action: :index) diff --git a/app/controllers/admin/hidden_proposals_controller.rb b/app/controllers/admin/hidden_proposals_controller.rb index 48c910c0c..7442c0626 100644 --- a/app/controllers/admin/hidden_proposals_controller.rb +++ b/app/controllers/admin/hidden_proposals_controller.rb @@ -18,7 +18,7 @@ class Admin::HiddenProposalsController < Admin::BaseController end def restore - @proposal.restore + @proposal.restore(recursive: true) @proposal.ignore_flag Activity.log(current_user, :restore, @proposal) redirect_to request.query_parameters.merge(action: :index) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 794dcd26b..5451451ff 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ require "application_responder" class ApplicationController < ActionController::Base + include GlobalizeFallbacks include HasFilters include HasOrders include AccessDeniedHandler @@ -14,7 +15,6 @@ class ApplicationController < ActionController::Base before_action :track_email_campaign before_action :set_return_url before_action :set_current_user - before_action :set_fallbacks_to_all_available_locales check_authorization unless: :devise_controller? self.responder = ApplicationResponder @@ -124,8 +124,4 @@ class ApplicationController < ActionController::Base def set_current_user User.current_user = current_user end - - def set_fallbacks_to_all_available_locales - Globalize.set_fallbacks_to_all_available_locales - end end diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index 1aea05085..5a57dd1f4 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -6,6 +6,7 @@ module Budgets include FlagActions include RandomSeed include ImageAttributes + include Translatable PER_PAGE = 10 @@ -48,6 +49,7 @@ module Budgets load_investment_votes(@investments) @tag_cloud = tag_cloud + @remote_translations = detect_remote_translations(@investments) end def new @@ -60,6 +62,7 @@ module Budgets set_comment_flags(@comment_tree.comments) load_investment_votes(@investment) @investment_ids = [@investment.id] + @remote_translations = detect_remote_translations([@investment], @comment_tree.comments) end def create @@ -122,12 +125,12 @@ module Budgets end def investment_params - params.require(:budget_investment) - .permit(:title, :description, :heading_id, :tag_list, + attributes = [:heading_id, :tag_list, :organization_name, :location, :terms_of_service, :skip_map, image_attributes: image_attributes, documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], - map_location_attributes: [:latitude, :longitude, :zoom]) + map_location_attributes: [:latitude, :longitude, :zoom]] + params.require(:budget_investment).permit(attributes, translation_params(Budget::Investment)) end def load_ballot diff --git a/app/controllers/concerns/commentable_actions.rb b/app/controllers/concerns/commentable_actions.rb index 66e2456de..2c6d1da81 100644 --- a/app/controllers/concerns/commentable_actions.rb +++ b/app/controllers/concerns/commentable_actions.rb @@ -3,6 +3,7 @@ module CommentableActions include Polymorphic include Search include DownloadSettingsHelper + include RemotelyTranslatable def index @resources = resource_model.all @@ -23,6 +24,8 @@ module CommentableActions set_resource_votes(@resources) set_resources_instance + @remote_translations = detect_remote_translations(@resources, featured_proposals) + respond_to do |format| format.html format.csv {send_data to_csv(resources_csv, resource_model), @@ -38,6 +41,7 @@ module CommentableActions @comment_tree = CommentTree.new(@commentable, params[:page], @current_order) set_comment_flags(@comment_tree.comments) set_resource_instance + @remote_translations = detect_remote_translations([@resource], @comment_tree.comments) end def new @@ -132,4 +136,7 @@ module CommentableActions end end + def featured_proposals + @featured_proposals ||= [] + end end diff --git a/app/controllers/concerns/globalize_fallbacks.rb b/app/controllers/concerns/globalize_fallbacks.rb new file mode 100644 index 000000000..386bb3ae1 --- /dev/null +++ b/app/controllers/concerns/globalize_fallbacks.rb @@ -0,0 +1,13 @@ +module GlobalizeFallbacks + extend ActiveSupport::Concern + + included do + before_action :initialize_globalize_fallbacks + end + + private + + def initialize_globalize_fallbacks + Globalize.set_fallbacks_to_all_available_locales + end +end diff --git a/app/controllers/concerns/remotely_translatable.rb b/app/controllers/concerns/remotely_translatable.rb new file mode 100644 index 000000000..4f030ff6c --- /dev/null +++ b/app/controllers/concerns/remotely_translatable.rb @@ -0,0 +1,29 @@ +module RemotelyTranslatable + + private + + def detect_remote_translations(*args) + return [] unless Setting["feature.remote_translations"].present? + + resources_groups(*args).flatten.select { |resource| translation_empty?(resource) }.map do |resource| + remote_translation_for(resource) + end + end + + def remote_translation_for(resource) + { "remote_translatable_id" => resource.id.to_s, + "remote_translatable_type" => resource.class.to_s, + "locale" => I18n.locale } + end + + def translation_empty?(resource) + resource.translations.where(locale: I18n.locale).empty? + end + + def resources_groups(*args) + feeds = args.detect { |arg| arg&.first.class == Widget::Feed } || [] + + args.compact - [feeds] + feeds.map(&:items) + end + +end diff --git a/app/controllers/concerns/translatable.rb b/app/controllers/concerns/translatable.rb index 94cdcaaab..533b8db22 100644 --- a/app/controllers/concerns/translatable.rb +++ b/app/controllers/concerns/translatable.rb @@ -3,10 +3,13 @@ module Translatable private - def translation_params(resource_model) - { - translations_attributes: [:id, :_destroy, :locale] + - resource_model.translated_attribute_names - } + def translation_params(resource_model, options = {}) + attributes = [:id, :locale, :_destroy] + if options[:only] + attributes += [*options[:only]] + else + attributes += resource_model.translated_attribute_names + end + { translations_attributes: attributes - [*options[:except]] } end end diff --git a/app/controllers/debates_controller.rb b/app/controllers/debates_controller.rb index b10e28b88..c0def9803 100644 --- a/app/controllers/debates_controller.rb +++ b/app/controllers/debates_controller.rb @@ -2,6 +2,7 @@ class DebatesController < ApplicationController include FeatureFlags include CommentableActions include FlagActions + include Translatable before_action :parse_tag_filter, only: :index before_action :authenticate_user!, except: [:index, :show, :map] @@ -55,7 +56,8 @@ class DebatesController < ApplicationController private def debate_params - params.require(:debate).permit(:title, :description, :tag_list, :terms_of_service) + attributes = [:tag_list, :terms_of_service] + params.require(:debate).permit(attributes, translation_params(Debate)) end def resource_model diff --git a/app/controllers/management/base_controller.rb b/app/controllers/management/base_controller.rb index 4314b2aa7..9f45fab39 100644 --- a/app/controllers/management/base_controller.rb +++ b/app/controllers/management/base_controller.rb @@ -1,4 +1,5 @@ class Management::BaseController < ActionController::Base + include GlobalizeFallbacks layout "management" before_action :verify_manager diff --git a/app/controllers/management/budgets/investments_controller.rb b/app/controllers/management/budgets/investments_controller.rb index 79a65cf15..da7bb117e 100644 --- a/app/controllers/management/budgets/investments_controller.rb +++ b/app/controllers/management/budgets/investments_controller.rb @@ -1,4 +1,5 @@ class Management::Budgets::InvestmentsController < Management::BaseController + include Translatable before_action :load_budget load_resource :budget @@ -53,8 +54,8 @@ class Management::Budgets::InvestmentsController < Management::BaseController end def investment_params - params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, - :tag_list, :organization_name, :location, :skip_map) + attributes = [:external_url, :heading_id, :tag_list, :organization_name, :location, :skip_map] + params.require(:budget_investment).permit(attributes, translation_params(Budget::Investment)) end def only_verified_users diff --git a/app/controllers/management/proposals_controller.rb b/app/controllers/management/proposals_controller.rb index f24e94303..93e20f690 100644 --- a/app/controllers/management/proposals_controller.rb +++ b/app/controllers/management/proposals_controller.rb @@ -1,6 +1,7 @@ class Management::ProposalsController < Management::BaseController include HasOrders include CommentableActions + include Translatable before_action :only_verified_users, except: :print before_action :set_proposal, only: [:vote, :show] @@ -52,10 +53,10 @@ class Management::ProposalsController < Management::BaseController end def proposal_params - params.require(:proposal).permit(:title, :summary, :description, :video_url, - :responsible_name, :tag_list, :terms_of_service, :geozone_id, - :skip_map, - map_location_attributes: [:latitude, :longitude, :zoom]) + attributes = [:video_url, :responsible_name, :tag_list, + :terms_of_service, :geozone_id, + :skip_map, map_location_attributes: [:latitude, :longitude, :zoom]] + params.require(:proposal).permit(attributes, translation_params(Proposal)) end def resource_model diff --git a/app/controllers/management/sessions_controller.rb b/app/controllers/management/sessions_controller.rb index a88c1de9f..83e364e00 100644 --- a/app/controllers/management/sessions_controller.rb +++ b/app/controllers/management/sessions_controller.rb @@ -1,6 +1,7 @@ require "manager_authenticator" class Management::SessionsController < ActionController::Base + include GlobalizeFallbacks include AccessDeniedHandler def create diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index fc596c5f0..6f35d44be 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -3,6 +3,7 @@ class ProposalsController < ApplicationController include CommentableActions include FlagActions include ImageAttributes + include Translatable before_action :parse_tag_filter, only: :index before_action :load_categories, only: [:index, :new, :create, :edit, :map, :summary] @@ -37,7 +38,6 @@ class ProposalsController < ApplicationController def create @proposal = Proposal.new(proposal_params.merge(author: current_user)) - if @proposal.save redirect_to created_proposal_path(@proposal), notice: I18n.t("flash.actions.create.proposal") else @@ -62,7 +62,7 @@ class ProposalsController < ApplicationController end def retire - if valid_retired_params? && @proposal.update(retired_params.merge(retired_at: Time.current)) + if @proposal.update(retired_params.merge(retired_at: Time.current)) redirect_to proposal_path(@proposal), notice: t("proposals.notice.retired") else render action: :retire_form @@ -98,22 +98,20 @@ class ProposalsController < ApplicationController private def proposal_params - params.require(:proposal).permit(:title, :summary, :description, :video_url, - :responsible_name, :tag_list, :terms_of_service, - :geozone_id, :skip_map, image_attributes: image_attributes, - documents_attributes: [:id, :title, :attachment, - :cached_attachment, :user_id, :_destroy], - map_location_attributes: [:latitude, :longitude, :zoom]) + attributes = [:video_url,:responsible_name, :tag_list, + :terms_of_service, :geozone_id, :skip_map, + image_attributes: image_attributes, + documents_attributes: [:id, :title, :attachment, :cached_attachment, + :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]] + translations_attributes = translation_params(Proposal, except: :retired_explanation) + params.require(:proposal).permit(attributes, translations_attributes) end def retired_params - params.require(:proposal).permit(:retired_reason, :retired_explanation) - end - - def valid_retired_params? - @proposal.errors.add(:retired_reason, I18n.t("errors.messages.blank")) if params[:proposal][:retired_reason].blank? - @proposal.errors.add(:retired_explanation, I18n.t("errors.messages.blank")) if params[:proposal][:retired_explanation].blank? - @proposal.errors.empty? + attributes = [:retired_reason] + translations_attributes = translation_params(Proposal, only: :retired_explanation) + params.require(:proposal).permit(attributes, translations_attributes) end def resource_model diff --git a/app/controllers/remote_translations_controller.rb b/app/controllers/remote_translations_controller.rb new file mode 100644 index 000000000..d811d2f5c --- /dev/null +++ b/app/controllers/remote_translations_controller.rb @@ -0,0 +1,34 @@ +class RemoteTranslationsController < ApplicationController + skip_authorization_check + respond_to :html, :js + + before_action :set_remote_translations, only: :create + + def create + @remote_translations.each do |remote_translation| + RemoteTranslation.create(remote_translation) unless translations_enqueued?(remote_translation) + end + redirect_to request.referer, notice: t("remote_translations.create.enqueue_remote_translation") + end + + private + + def remote_translations_params + params.permit(:remote_translations) + end + + def set_remote_translations + remote_translations = remote_translations_params["remote_translations"] + decoded_remote_translations = ActiveSupport::JSON.decode(remote_translations) + @remote_translations = decoded_remote_translations.map{ |remote_translation| + remote_translation.slice("remote_translatable_id", + "remote_translatable_type", + "locale") + } + end + + def translations_enqueued?(remote_translation) + RemoteTranslation.remote_translation_enqueued?(remote_translation) + end + +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index eb009746a..b6bb138c4 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -1,4 +1,6 @@ class WelcomeController < ApplicationController + include RemotelyTranslatable + skip_authorization_check before_action :set_user_recommendations, only: :index, if: :current_user before_action :authenticate_user!, only: :welcome @@ -10,6 +12,9 @@ class WelcomeController < ApplicationController @feeds = Widget::Feed.active @cards = Widget::Card.body @banners = Banner.in_section("homepage").with_active + @remote_translations = detect_remote_translations(@feeds, + @recommended_debates, + @recommended_proposals) end def welcome diff --git a/app/helpers/globalize_helper.rb b/app/helpers/globalize_helper.rb index 92d02b63e..65021b350 100644 --- a/app/helpers/globalize_helper.rb +++ b/app/helpers/globalize_helper.rb @@ -1,46 +1,134 @@ module GlobalizeHelper - def options_for_locale_select - options_for_select(locale_options, nil) + def options_for_select_language(resource) + options_for_select(available_locales(resource), selected_locale(resource)) end - def locale_options - I18n.available_locales.map do |locale| - [name_for_locale(locale), locale] + def available_locales(resource) + I18n.available_locales.select{ |locale| enabled_locale?(resource, locale) }.map do |locale| + [name_for_locale(locale), locale , { data: { locale: locale } }] end end - def display_translation?(resource, locale) - if !resource || resource.translations.blank? || - resource.locales_not_marked_for_destruction.include?(I18n.locale) - locale == I18n.locale + def enabled_locale?(resource, locale) + return site_customization_enable_translation?(locale) if resource.blank? + + if resource.locales_not_marked_for_destruction.any? + resource.locales_not_marked_for_destruction.include?(locale) + elsif resource.locales_persisted_and_marked_for_destruction.any? + locale == first_marked_for_destruction_translation(resource) else - locale == resource.translations.first.locale + locale == I18n.locale end end + def selected_locale(resource) + return first_i18n_content_translation_locale if resource.blank? + + if resource.locales_not_marked_for_destruction.any? + first_translation(resource) + elsif resource.locales_persisted_and_marked_for_destruction.any? + first_marked_for_destruction_translation(resource) + else + I18n.locale + end + end + + def first_i18n_content_translation_locale + if I18nContentTranslation.existing_languages.count == 0 || + I18nContentTranslation.existing_languages.include?(I18n.locale) + I18n.locale + else + I18nContentTranslation.existing_languages.first + end + end + + def first_translation(resource) + if resource.locales_not_marked_for_destruction.include? I18n.locale + I18n.locale + else + resource.locales_not_marked_for_destruction.first + end + end + + def first_marked_for_destruction_translation(resource) + if resource.locales_persisted_and_marked_for_destruction.include? I18n.locale + I18n.locale + else + resource.locales_persisted_and_marked_for_destruction.first + end + end + + def translations_for_locale?(resource) + resource.locales_not_marked_for_destruction.any? + end + + def selected_languages_description(resource) + t("shared.translations.languages_in_use_html", count: active_languages_count(resource)) + end + + def select_language_error(resource) + return if resource.blank? + + current_translation = resource.translation_for(selected_locale(resource)) + if current_translation.errors.added? :base, :translations_too_short + content_tag :div, class: "small error" do + current_translation.errors[:base].join(", ") + end + end + end + + def active_languages_count(resource) + if resource.blank? + no_resource_languages_count + elsif resource.locales_not_marked_for_destruction.size > 0 + resource.locales_not_marked_for_destruction.size + else + 1 + end + end + + def no_resource_languages_count + count = I18nContentTranslation.existing_languages.count + count > 0 ? count : 1 + end + def display_translation_style(resource, locale) "display: none;" unless display_translation?(resource, locale) end - def translation_enabled_tag(locale, enabled) - hidden_field_tag("enabled_translations[#{locale}]", (enabled ? 1 : 0)) - end + def display_translation?(resource, locale) + return locale == I18n.locale if resource.blank? - def enable_translation_style(resource, locale) - "display: none;" unless enable_locale?(resource, locale) - end - - def enable_locale?(resource, locale) - if resource.translations.any? - resource.locales_not_marked_for_destruction.include?(locale) + if resource.locales_not_marked_for_destruction.any? + locale == first_translation(resource) + elsif resource.locales_persisted_and_marked_for_destruction.any? + locale == first_marked_for_destruction_translation(resource) else locale == I18n.locale end end - def highlight_class(resource, locale) - "is-active" if display_translation?(resource, locale) + def display_destroy_locale_style(resource, locale) + "display: none;" unless display_destroy_locale_link?(resource, locale) + end + + def display_destroy_locale_link?(resource, locale) + selected_locale(resource) == locale + end + + def options_for_add_language + options_for_select(all_language_options, nil) + end + + def all_language_options + I18n.available_locales.map do |locale| + [name_for_locale(locale), locale] + end + end + + def translation_enabled_tag(locale, enabled) + hidden_field_tag("enabled_translations[#{locale}]", (enabled ? 1 : 0)) end def globalize(locale, &block) @@ -48,9 +136,4 @@ module GlobalizeHelper yield end end - - def same_locale?(locale1, locale2) - locale1 == locale2 - end - end diff --git a/app/helpers/proposals_helper.rb b/app/helpers/proposals_helper.rb index ecd8a1490..3879d4679 100644 --- a/app/helpers/proposals_helper.rb +++ b/app/helpers/proposals_helper.rb @@ -64,6 +64,10 @@ module ProposalsHelper proposals_current_view == "default" ? "minimal" : "default" end + def summary_help_text_id(translations_form) + "summary-help-text-#{translations_form.locale}" + end + def link_to_toggle_proposal_selection(proposal) if proposal.selected? button_text = t("admin.proposals.index.selected") diff --git a/app/helpers/remote_translations_helper.rb b/app/helpers/remote_translations_helper.rb new file mode 100644 index 000000000..d9435f833 --- /dev/null +++ b/app/helpers/remote_translations_helper.rb @@ -0,0 +1,12 @@ +module RemoteTranslationsHelper + + def display_remote_translation_info?(remote_translations, locale) + remote_translations.present? && RemoteTranslations::Microsoft::AvailableLocales.include_locale?(locale) + end + + def display_remote_translation_button?(remote_translations) + remote_translations.none? do |remote_translation| + RemoteTranslation.remote_translation_enqueued?(remote_translation) + end + end +end diff --git a/app/helpers/text_with_links_helper.rb b/app/helpers/text_with_links_helper.rb index 9144bdd6b..b59c6ae7f 100644 --- a/app/helpers/text_with_links_helper.rb +++ b/app/helpers/text_with_links_helper.rb @@ -8,6 +8,7 @@ module TextWithLinksHelper def safe_html_with_links(html) return if html.nil? + html = ActiveSupport::SafeBuffer.new(html) if html.is_a?(String) return html.html_safe unless html.html_safe? Rinku.auto_link(html, :all, 'target="_blank" rel="nofollow"').html_safe end diff --git a/app/helpers/translatable_form_helper.rb b/app/helpers/translatable_form_helper.rb index fdee3bf34..458c7a929 100644 --- a/app/helpers/translatable_form_helper.rb +++ b/app/helpers/translatable_form_helper.rb @@ -6,15 +6,27 @@ module TranslatableFormHelper end end + def translations_interface_enabled? + Setting["feature.translation_interface"].present? || backend_translations_enabled? + end + + def backend_translations_enabled? + (controller.class.parents & [Admin, Management, Valuation, Tracking]).any? + end + + def highlight_translation_html_class + "highlight" if translations_interface_enabled? + end + class TranslatableFormBuilder < FoundationRailsHelper::FormBuilder attr_accessor :translations def translatable_fields(&block) @translations = {} - @object.globalize_locales.map do |locale| + visible_locales.map do |locale| @translations[locale] = translation_for(locale) end - @object.globalize_locales.map do |locale| + visible_locales.map do |locale| Globalize.with_locale(locale) { fields_for_locale(locale, &block) } end.join.html_safe end @@ -26,6 +38,7 @@ module TranslatableFormHelper @template.content_tag :div, translations_options(translations_form.object, locale) do @template.concat translations_form.hidden_field( :_destroy, + value: !@template.enabled_locale?(translations_form.object.globalized_model, locale), data: { locale: locale } ) @@ -52,15 +65,17 @@ module TranslatableFormHelper def new_translation_for(locale) @object.translations.new(locale: locale).tap do |translation| - unless locale == I18n.locale && no_other_translations?(translation) - translation.mark_for_destruction - end + translation.mark_for_destruction end end + def highlight_translation_html_class + @template.highlight_translation_html_class + end + def translations_options(resource, locale) { - class: "translatable-fields js-globalize-attribute", + class: "translatable-fields js-globalize-attribute #{highlight_translation_html_class}", style: @template.display_translation_style(resource.globalized_model, locale), data: { locale: locale } } @@ -69,6 +84,14 @@ module TranslatableFormHelper def no_other_translations?(translation) (@object.translations - [translation]).reject(&:_destroy).empty? end + + def visible_locales + if @template.translations_interface_enabled? + @object.globalize_locales + else + [I18n.locale] + end + end end class TranslationsFieldsBuilder < FoundationRailsHelper::FormBuilder diff --git a/app/models/budget.rb b/app/models/budget.rb index 133956b07..4904f3923 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -22,8 +22,6 @@ class Budget < ApplicationRecord CURRENCY_SYMBOLS = %w(€ $ £ ¥).freeze - before_validation :assign_model_to_translations - validates_translation :name, presence: true validates :phase, inclusion: { in: Budget::Phase::PHASE_KINDS } validates :currency_symbol, presence: true @@ -231,4 +229,16 @@ class Budget < ApplicationRecord slug.nil? || drafting? end + class Translation < Globalize::ActiveRecord::Translation + validate :name_uniqueness_by_budget + + def name_uniqueness_by_budget + if Budget.joins(:translations) + .where(name: name) + .where.not("budget_translations.budget_id": budget_id).any? + errors.add(:name, I18n.t("errors.messages.taken")) + end + end + end + end diff --git a/app/models/budget/group.rb b/app/models/budget/group.rb index 43d28a0ae..741040c78 100644 --- a/app/models/budget/group.rb +++ b/app/models/budget/group.rb @@ -22,8 +22,6 @@ class Budget has_many :headings, dependent: :destroy - before_validation :assign_model_to_translations - validates_translation :name, presence: true validates :budget_id, presence: true validates :slug, presence: true, format: /\A[a-z0-9\-_]+\z/ @@ -42,5 +40,18 @@ class Budget slug.nil? || budget.drafting? end + class Translation < Globalize::ActiveRecord::Translation + delegate :budget, to: :globalized_model + + validate :name_uniqueness_by_budget + + def name_uniqueness_by_budget + if budget.groups.joins(:translations) + .where(name: name) + .where.not("budget_group_translations.budget_group_id": budget_group_id).any? + errors.add(:name, I18n.t("errors.messages.taken")) + end + end + end end end diff --git a/app/models/budget/heading.rb b/app/models/budget/heading.rb index 7b33c4e54..0221061d7 100644 --- a/app/models/budget/heading.rb +++ b/app/models/budget/heading.rb @@ -26,8 +26,6 @@ class Budget has_many :investments has_many :content_blocks - before_validation :assign_model_to_translations - validates_translation :name, presence: true validates :group_id, presence: true validates :price, presence: true @@ -62,5 +60,19 @@ class Budget slug.nil? || budget.drafting? end + class Translation < Globalize::ActiveRecord::Translation + delegate :budget, to: :globalized_model + + validate :name_uniqueness_by_budget + + def name_uniqueness_by_budget + if budget.headings + .joins(:translations) + .where(name: name) + .where.not("budget_heading_translations.budget_heading_id": budget_heading_id).any? + errors.add(:name, I18n.t("errors.messages.taken")) + end + end + end end end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 9da713c84..aa83a499a 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -1,7 +1,7 @@ class Budget require "csv" class Investment < ApplicationRecord - SORTING_OPTIONS = {id: "id", title: "title", supports: "cached_votes_up"}.freeze + SORTING_OPTIONS = { id: "id", supports: "cached_votes_up" }.freeze include ActiveModel::Dirty include Rails.application.routes.url_helpers @@ -28,6 +28,10 @@ class Budget extend DownloadSettings::BudgetInvestmentCsv + translates :title, touch: true + translates :description, touch: true + include Globalizable + belongs_to :author, -> { with_hidden }, class_name: "User", foreign_key: "author_id" belongs_to :heading belongs_to :group @@ -48,15 +52,13 @@ class Budget delegate :name, :email, to: :author, prefix: true - validates :title, presence: true + validates_translation :title, presence: true, length: { in: 4..Budget::Investment.title_max_length } + validates_translation :description, presence: true, length: { maximum: Budget::Investment.description_max_length } + validates :author, presence: true - validates :description, presence: true validates :heading_id, presence: true validates :unfeasibility_explanation, presence: { if: :unfeasibility_explanation_required? } validates :price, presence: { if: :price_required? } - - validates :title, length: { in: 4..Budget::Investment.title_max_length } - validates :description, length: { maximum: Budget::Investment.description_max_length } validates :terms_of_service, acceptance: { allow_nil: false }, on: :create scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) } @@ -64,7 +66,6 @@ class Budget scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) } scope :sort_by_id, -> { order("id DESC") } - scope :sort_by_title, -> { order("title ASC") } scope :sort_by_supports, -> { order("cached_votes_up DESC") } scope :valuation_open, -> { where(valuation_finished: false) } @@ -117,6 +118,10 @@ class Budget budget_investment_path(budget, self) end + def self.sort_by_title + with_translation.sort_by(&:title) + end + def self.filter_params(params) params.permit(%i[heading_id group_id administrator_id tag_name valuator_id]) end @@ -162,10 +167,12 @@ class Budget def self.order_filter(params) sorting_key = params[:sort_by]&.downcase&.to_sym allowed_sort_option = SORTING_OPTIONS[sorting_key] + direction = params[:direction] == "desc" ? "desc" : "asc" if allowed_sort_option.present? - direction = params[:direction] == "desc" ? "desc" : "asc" order("#{allowed_sort_option} #{direction}") + elsif sorting_key == :title + direction == "asc" ? sort_by_title : sort_by_title.reverse else order(cached_votes_up: :desc).order(id: :desc) end @@ -184,20 +191,17 @@ class Budget end def self.search_by_title_or_id(title_or_id, results) - if title_or_id =~ /^[0-9]+$/ - results.where(id: title_or_id) - else - results.where("title ILIKE ?", "%#{title_or_id}%") - end + return results.where(id: title_or_id) if title_or_id =~ /^[0-9]+$/ + + results.with_translations(Globalize.fallbacks(I18n.locale)). + where("budget_investment_translations.title ILIKE ?", "%#{title_or_id}%") end def searchable_values - { title => "A", - author.username => "B", - heading.try(:name) => "B", - tag_list.join(" ") => "B", - description => "C" - } + { author.username => "B", + heading.name => "B", + tag_list.join(" ") => "B" + }.merge(searchable_globalized_values) end def self.search(terms) @@ -413,5 +417,10 @@ class Budget end end end + + def searchable_translations_definitions + { title => "A", + description => "D" } + end end end diff --git a/app/models/budget/phase.rb b/app/models/budget/phase.rb index 125368ccd..5d28e65ab 100644 --- a/app/models/budget/phase.rb +++ b/app/models/budget/phase.rb @@ -100,5 +100,14 @@ class Budget PHASE_KINDS.index(kind) >= PHASE_KINDS.index(phase) end + class Translation < Globalize::ActiveRecord::Translation + before_validation :sanitize_description + + private + + def sanitize_description + self.description = WYSIWYGSanitizer.new.sanitize(description) + end + end end end diff --git a/app/models/comment.rb b/app/models/comment.rb index e5b83e818..e80a5e048 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -18,7 +18,10 @@ class Comment < ApplicationRecord attr_accessor :as_moderator, :as_administrator - validates :body, presence: true + translates :body, touch: true + include Globalizable + + validates_translation :body, presence: true validates :user, presence: true validates :commentable_type, inclusion: { in: COMMENTABLE_TYPES } diff --git a/app/models/concerns/globalizable.rb b/app/models/concerns/globalizable.rb index f963787c7..9999e3502 100644 --- a/app/models/concerns/globalizable.rb +++ b/app/models/concerns/globalizable.rb @@ -1,27 +1,101 @@ module Globalizable + MIN_TRANSLATIONS = 1 extend ActiveSupport::Concern included do globalize_accessors accepts_nested_attributes_for :translations, allow_destroy: true - def locales_not_marked_for_destruction - translations.reject(&:_destroy).map(&:locale) - end - - def assign_model_to_translations - translations.each { |translation| translation.globalized_model = self } - end + validate :check_translations_number, on: :update, if: :translations_required? + after_validation :copy_error_to_current_translation, on: :update def description self.read_attribute(:description).try :html_safe end + + def locales_not_marked_for_destruction + translations.reject(&:marked_for_destruction?).map(&:locale) + end + + def locales_marked_for_destruction + I18n.available_locales - locales_not_marked_for_destruction + end + + def locales_persisted_and_marked_for_destruction + translations.select{|t| t.persisted? && t.marked_for_destruction? }.map(&:locale) + end + + def translations_required? + translated_attribute_names.any?{|attr| required_attribute?(attr)} + end + + if self.paranoid? && translation_class.attribute_names.include?("hidden_at") + translation_class.send :acts_as_paranoid, column: :hidden_at + end + + scope :with_translation, -> { joins("LEFT OUTER JOIN #{translations_table_name} ON #{table_name}.id = #{translations_table_name}.#{reflections["translations"].foreign_key} AND #{translations_table_name}.locale='#{I18n.locale }'") } + + private + + def required_attribute?(attribute) + presence_validators = [ActiveModel::Validations::PresenceValidator, + ActiveRecord::Validations::PresenceValidator] + + attribute_validators(attribute).any?{|validator| presence_validators.include? validator } + end + + def attribute_validators(attribute) + self.class.validators_on(attribute).map(&:class) + end + + def check_translations_number + errors.add(:base, :translations_too_short) unless traslations_count_valid? + end + + def traslations_count_valid? + translations.reject(&:marked_for_destruction?).count >= MIN_TRANSLATIONS + end + + def copy_error_to_current_translation + return unless errors.added?(:base, :translations_too_short) + + if locales_persisted_and_marked_for_destruction.include?(I18n.locale) + locale = I18n.locale + else + locale = locales_persisted_and_marked_for_destruction.first + end + + translation = translation_for(locale) + translation.errors.add(:base, :translations_too_short) + end + + def searchable_globalized_values + values = {} + translations.each do |translation| + Globalize.with_locale(translation.locale) do + values.merge! searchable_translations_definitions + end + end + values + end end class_methods do def validates_translation(method, options = {}) validates(method, options.merge(if: lambda { |resource| resource.translations.blank? })) - translation_class.instance_eval { validates method, options } + if options.include?(:length) + lenght_validate = { length: options[:length] } + translation_class.instance_eval do + validates method, lenght_validate.merge(if: lambda { |translation| translation.locale == I18n.default_locale }) + end + if options.count > 1 + translation_class.instance_eval do + validates method, options.reject { |key| key == :length } + end + end + else + translation_class.instance_eval { validates method, options } + end end def translation_class_delegate(method) diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb index b10c57f70..aa7d4d3bc 100644 --- a/app/models/concerns/notifiable.rb +++ b/app/models/concerns/notifiable.rb @@ -12,6 +12,10 @@ module Notifiable end end + def notifiable_body + body if attribute_names.include?("body") + end + def notifiable_available? case self.class.name when "ProposalNotification" diff --git a/app/models/debate.rb b/app/models/debate.rb index 3f1ca7d76..b3a23200a 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -20,6 +20,10 @@ class Debate < ApplicationRecord acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + translates :title, touch: true + translates :description, touch: true + include Globalizable + belongs_to :author, -> { with_hidden }, class_name: "User", foreign_key: "author_id" belongs_to :geozone has_many :comments, as: :commentable @@ -27,13 +31,10 @@ class Debate < ApplicationRecord extend DownloadSettings::DebateCsv delegate :name, :email, to: :author, prefix: true - validates :title, presence: true - validates :description, presence: true + validates_translation :title, presence: true, length: { in: 4..Debate.title_max_length } + validates_translation :description, presence: true, length: { in: 10..Debate.description_max_length } validates :author, presence: true - validates :title, length: { in: 4..Debate.title_max_length } - validates :description, length: { in: 10..Debate.description_max_length } - validates :terms_of_service, acceptance: { allow_nil: false }, on: :create before_save :calculate_hot_score, :calculate_confidence_score @@ -64,13 +65,17 @@ class Debate < ApplicationRecord .where("author_id != ?", user.id) end + def searchable_translations_definitions + { title => "A", + description => "D" } + end + def searchable_values - { title => "A", + { author.username => "B", tag_list.join(" ") => "B", geozone.try(:name) => "B", - description => "D" - } + }.merge!(searchable_globalized_values) end def self.search(terms) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 52f8ccb29..d4219c004 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -11,8 +11,6 @@ class Milestone < ApplicationRecord validates :milestoneable, presence: true validates :publication_date, presence: true - - before_validation :assign_model_to_translations validates_translation :description, presence: true, unless: -> { status_id.present? } scope :order_by_publication_date, -> { order(publication_date: :asc, created_at: :asc) } diff --git a/app/models/notification.rb b/app/models/notification.rb index 738e5ab5f..e5a8e2b95 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -11,8 +11,9 @@ class Notification < ApplicationRecord scope :recent, -> { order(id: :desc) } scope :for_render, -> { includes(:notifiable) } - delegate :notifiable_title, :notifiable_available?, :check_availability, - :linkable_resource, to: :notifiable, allow_nil: true + delegate :notifiable_title, :notifiable_body, :notifiable_available?, + :check_availability, :linkable_resource, + to: :notifiable, allow_nil: true def mark_as_read update(read_at: Time.current) diff --git a/app/models/progress_bar.rb b/app/models/progress_bar.rb index c0bacc7ba..e9994f011 100644 --- a/app/models/progress_bar.rb +++ b/app/models/progress_bar.rb @@ -18,7 +18,6 @@ class ProgressBar < ApplicationRecord } validates :percentage, presence: true, inclusion: RANGE, numericality: { only_integer: true } - before_validation :assign_model_to_translations validates_translation :title, presence: true, unless: :primary? end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index e0f707918..497d8181e 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -28,6 +28,13 @@ class Proposal < ApplicationRecord RETIRE_OPTIONS = %w[duplicated started unfeasible done other] + translates :title, touch: true + translates :description, touch: true + translates :summary, touch: true + translates :retired_explanation, touch: true + include Globalizable + translation_class_delegate :retired_at + belongs_to :author, -> { with_hidden }, class_name: "User", foreign_key: "author_id" belongs_to :geozone has_many :comments, as: :commentable, dependent: :destroy @@ -39,18 +46,16 @@ class Proposal < ApplicationRecord extend DownloadSettings::ProposalCsv delegate :name, :email, to: :author, prefix: true - extend DownloadSettings::ProposalCsv - delegate :name, :email, to: :author, prefix: true + validates_translation :title, presence: true, length: { in: 4..Proposal.title_max_length } + validates_translation :description, length: { maximum: Proposal.description_max_length } + validates_translation :summary, presence: true + validates_translation :retired_explanation, presence: true, unless: -> { retired_at.blank? } - validates :title, presence: true - validates :summary, presence: true validates :author, presence: true validates :responsible_name, presence: true, unless: :skip_user_verification? - validates :title, length: { in: 4..Proposal.title_max_length } - validates :description, length: { maximum: Proposal.description_max_length } validates :responsible_name, length: { in: 6..Proposal.responsible_name_max_length }, unless: :skip_user_verification? - validates :retired_reason, inclusion: { in: RETIRE_OPTIONS, allow_nil: true } + validates :retired_reason, presence: true, inclusion: { in: RETIRE_OPTIONS }, unless: -> { retired_at.blank? } validates :terms_of_service, acceptance: { allow_nil: false }, on: :create @@ -120,14 +125,18 @@ class Proposal < ApplicationRecord "#{id}-#{title}".parameterize end + def searchable_translations_definitions + { title => "A", + summary => "C", + description => "D" } + end + def searchable_values - { title => "A", - author.username => "B", - tag_list.join(" ") => "B", - geozone.try(:name) => "B", - summary => "C", - description => "D" - } + { + author.username => "B", + tag_list.join(" ") => "B", + geozone.try(:name) => "B" + }.merge!(searchable_globalized_values) end def self.search(terms) @@ -264,5 +273,4 @@ class Proposal < ApplicationRecord self.responsible_name = author.document_number end end - end diff --git a/app/models/remote_translation.rb b/app/models/remote_translation.rb new file mode 100644 index 000000000..d669f9083 --- /dev/null +++ b/app/models/remote_translation.rb @@ -0,0 +1,21 @@ +class RemoteTranslation < ApplicationRecord + + belongs_to :remote_translatable, polymorphic: true + + validates :remote_translatable_id, presence: true + validates :remote_translatable_type, presence: true + validates :locale, presence: true + + after_create :enqueue_remote_translation + + def enqueue_remote_translation + RemoteTranslations::Caller.new(self).delay.call + end + + def self.remote_translation_enqueued?(remote_translation) + where(remote_translatable_id: remote_translation["remote_translatable_id"], + remote_translatable_type: remote_translation["remote_translatable_type"], + locale: remote_translation["locale"], + error_message: nil).any? + end +end diff --git a/app/models/setting.rb b/app/models/setting.rb index 6e87816dc..a59e38651 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -93,6 +93,8 @@ class Setting < ApplicationRecord "feature.allow_attached_documents": true, "feature.allow_images": true, "feature.help_page": true, + "feature.remote_translations": nil, + "feature.translation_interface": nil, "feature.valuation_comment_notification": true, "homepage.widgets.feeds.debates": true, "homepage.widgets.feeds.processes": true, diff --git a/app/models/widget/feed.rb b/app/models/widget/feed.rb index 2ddd00d62..553d35db6 100644 --- a/app/models/widget/feed.rb +++ b/app/models/widget/feed.rb @@ -34,4 +34,4 @@ class Widget::Feed < ApplicationRecord Legislation::Process.open.published.order("created_at DESC").limit(limit) end -end \ No newline at end of file +end diff --git a/app/views/admin/admin_notifications/_form.html.erb b/app/views/admin/admin_notifications/_form.html.erb index 0048c93a5..d671c2517 100644 --- a/app/views/admin/admin_notifications/_form.html.erb +++ b/app/views/admin/admin_notifications/_form.html.erb @@ -1,19 +1,33 @@ -<%= render "admin/shared/globalize_locales", resource: @admin_notification %> +<%= render "shared/globalize_locales", resource: @admin_notification %> <%= translatable_form_for [:admin, @admin_notification] do |f| %> <%= render "shared/errors", resource: @admin_notification %> - <%= f.select :segment_recipient, options_for_select(user_segments_options, - @admin_notification[:segment_recipient]) %> - <%= f.text_field :link %> +
- <%= t("admin.budget_headings.form.population_info") %> -
- <%= f.text_field :population, - label: false, - maxlength: 8, - placeholder: t("admin.budget_headings.form.population"), - data: {toggle_focus: "population-info"}, - aria: {describedby: "budgets-population-help-text"} %> + <%= f.label :population, t("admin.budget_headings.form.population") %> ++ <%= t("admin.budget_headings.form.population_info") %> +
+ <%= f.text_field :population, + label: false, + maxlength: 8, + placeholder: t("admin.budget_headings.form.population"), + data: {toggle_focus: "population-info"}, + aria: {describedby: "budgets-population-help-text"} %> - <%= f.text_field :latitude, - label: t("admin.budget_headings.form.latitude"), - maxlength: 22, - placeholder: "latitude" %> + <%= f.text_field :latitude, + label: t("admin.budget_headings.form.latitude"), + maxlength: 22, + placeholder: "latitude" %> - <%= f.text_field :longitude, - label: t("admin.budget_headings.form.longitude"), - maxlength: 22, - placeholder: "longitude" %> -- <%= t("admin.budget_headings.form.coordinates_info") %> -
+ <%= f.text_field :longitude, + label: t("admin.budget_headings.form.longitude"), + maxlength: 22, + placeholder: "longitude" %> ++ <%= t("admin.budget_headings.form.coordinates_info") %> +
- <%= f.check_box :allow_custom_content, label: t("admin.budget_headings.form.allow_content_block") %> -- <%= t("admin.budget_headings.form.content_blocks_info") %> -
+ <%= f.check_box :allow_custom_content, label: t("admin.budget_headings.form.allow_content_block") %> ++ <%= t("admin.budget_headings.form.content_blocks_info") %> +
- <%= f.submit t("admin.budget_headings.form.#{action}"), class: "button success" %> - <% end %> -