From 4ff314046ab132552cff1f79ba083c8662ea0deb Mon Sep 17 00:00:00 2001 From: f Date: Mon, 19 Feb 2018 16:33:28 -0300 Subject: [PATCH] WIP editor de i18n funcionando --- app/controllers/i18n_controller.rb | 15 ++++- app/models/jekyll_i18n.rb | 88 ++++++++++++++++++++++++++++++ app/models/site.rb | 37 +++++++++---- app/views/i18n/_text_field.haml | 33 ++++++++--- config/deploy/production.rb | 22 ++++++++ config/environments/production.rb | 3 - config/initializers/locale.rb | 4 ++ config/locales/en.yml | 1 + config/locales/es.yml | 1 + 9 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 app/models/jekyll_i18n.rb create mode 100644 config/deploy/production.rb create mode 100644 config/initializers/locale.rb diff --git a/app/controllers/i18n_controller.rb b/app/controllers/i18n_controller.rb index 1fa65d1..226e780 100644 --- a/app/controllers/i18n_controller.rb +++ b/app/controllers/i18n_controller.rb @@ -11,11 +11,22 @@ class I18nController < ApplicationController @site = find_site @lang_from = I18n.locale.to_s @lang_to = @site.config['i18n'].reject { |i| i == @lang_from }.sample - @lang_to = 'es' + @lang_to = 'ar' end def update @site = find_site - binding.pry + # No usamos params porque nos obliga a hacer una lista blanca de + # todos los parámetros que queremos, pero no tenemos forma aun de + # pasarse a permit un array de todas las keys y sus tipos en base al + # idioma que ya existe + p = request.parameters[:i18n][:ar] + i = JekyllI18n.new(site: @site, lang: :ar, attributes: p) + + if i.save + redirect_to site_path(@site) + else + render 'i18n/edit' + end end end diff --git a/app/models/jekyll_i18n.rb b/app/models/jekyll_i18n.rb new file mode 100644 index 0000000..8267825 --- /dev/null +++ b/app/models/jekyll_i18n.rb @@ -0,0 +1,88 @@ +# Esta clase se encarga de guardan los archivos YAML de idiomas +# +# TODO se podría convertir en una clase genérica de editor de YAML +class JekyllI18n + attr_reader :site, :lang, :attributes + + def initialize(site:, lang:, attributes:) + unless site.is_a? Site + raise ArgumentError, I18n.t('errors.argument_error', argument: :site, class: Site) + end + + unless I18n.available_locales.include? lang.to_sym + raise ArgumentError, I18n.t('errors.unknown_locale', locale: lang) + end + + @site = site + @lang = lang.to_sym + @attributes = attributes.to_hash # porque enviamos parametros + end + + # Vuelva los datos a YAML y los guarda en el archivo correspondiente + # + # TODO es necesario evitar que se agreguen llaves nuevas? No veo nada + # inseguro... + # + # https://codeclimate.com/blog/rails-remote-code-execution-vulnerability-explained/ + # + # Pero parece que ya se resolvió hace rato. En general es mala + # práctica aceptar cualquier input de las usuarias, pero en este caso + # parece que no nos tenemos que preocupar? + # + # En cualquier caso habría que hacer un deep_merge, descartando las + # llaves que no están en el original y/o en el destino (pero queremos + # que el destino se parezca al original) y chequeando que no haya + # deserialización de objetos + # + # Jekyll usa SafeYAML para cargar los datos, con lo que estaríamos + # bien en cuanto a inyección de código. + def save + # Reemplaza los datos en el sitio + replace_lang_in_site + # Escribe los cambios en disco + write + end + + # Obtiene la ruta a partir del sitio y el idioma + def path + File.join(@site.path, '_data', "#{@lang.to_s}.yml") + end + + def exist? + File.exist? path + end + + private + + def replace_lang_in_site + @site.data[@lang.to_s] = @attributes + end + + # Escribe los cambios en disco + # + # TODO unificar con Post.write + def write + r = File.open(path, File::RDWR | File::CREAT, 0o640) do |f| + # Bloquear el archivo para que no sea accedido por otro + # proceso u otra editora + f.flock(File::LOCK_EX) + + # Empezar por el principio + f.rewind + + # Escribir + f.write(content) + + # Eliminar el resto + f.flush + f.truncate(f.pos) + end + + return true if r.zero? + false + end + + def content + @attributes.to_yaml + end +end diff --git a/app/models/site.rb b/app/models/site.rb index 3eace1b..2a7202f 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -22,9 +22,21 @@ class Site end alias :to_s :id + def read + @jekyll.read + end + + # Fuerza relectura del sitio, eliminando el sitio actual en favor de + # un sitio nuevo, leído desde cero + def read! + @jekyll = Site.load_jekyll(@jekyll.path) + + @jekyll.read + end + def data if @jekyll.data.empty? - @jekyll.read + read Rails.logger.info 'Leyendo data' end @@ -33,7 +45,7 @@ class Site def config if @jekyll.config.empty? - @jekyll.read + read Rails.logger.info 'Leyendo config' end @@ -95,6 +107,17 @@ class Site File.directory?(dir) && File.exist?(File.join(dir, '_config.yml')) end + def self.load_jekyll(path) + config = ::Jekyll.configuration('source' => path) + + # No necesitamos cargar plugins en este momento + %w[plugins gems].each do |unneeded| + config[unneeded] = [] if config.key? unneeded + end + + Jekyll::Site.new(config) + end + # Obtener todos los directorios de sitios asociados a esta usuaria def self.all_for(usuaria) @sites ||= Pathname.new(Site.site_path_for(usuaria)) @@ -103,14 +126,8 @@ class Site next unless Site.jekyll? j Dir.chdir(j) do - config = ::Jekyll.configuration('source' => Dir.pwd) - - # No necesitamos cargar plugins en este momento - %w[plugins gems].each do |unneeded| - config[unneeded] = [] if config.key? unneeded - end - - Site.new(jekyll: ::Jekyll::Site.new(config), path: j) + jekyll = Site.load_jekyll(Dir.pwd) + Site.new(jekyll: jekyll, path: j) end end.compact end diff --git a/app/views/i18n/_text_field.haml b/app/views/i18n/_text_field.haml index 4494077..4869a39 100644 --- a/app/views/i18n/_text_field.haml +++ b/app/views/i18n/_text_field.haml @@ -1,15 +1,34 @@ +- dir = 'ltr' +- if @lang_to == 'ar' + - dir = 'rtl' .form-group - key = keys.pop - - form_keys = keys.map { |k| "[#{k.to_s}]" }.join('') + -# si la key es numerica, queremos un array de hashes, no un hash de + -# hashes con keys numericas + - form_keys = keys.map do |k| + - if k.is_a? Integer + - '[]' + - else + - "[#{k.to_s}]" + - form_keys = form_keys.join('') - form_help = (keys.size > 0) ? [keys,key].flatten.join('.') : key - - value_to = @site.data[@lang_to] + - value_to = @site.data[@lang_to] + -# recorrer el hash hasta obtener el valor original - [keys,key].flatten.each do |k| + - if value_to.nil? + - value_to = '' + - break - value_to = value_to[k] - = label_tag "i18n[#{@lang_to}]#{form_keys}", value + -# no especificar el id en una key numerica para que no se genere un + -# hash en lugar de un array de valores + - key = '' if key.is_a? Integer + = label_tag "i18n[#{@lang_to}]#{form_keys}[#{key}]", value + -# creamos un campo a mano porque los helpers de niveles mas altos + -# quieren hacer magia con los ids y fallan - if value.length > 140 - = text_area "i18n[#{@lang_to}]#{form_keys}", key, value: value_to, - class: 'form-control' + = text_area_tag "i18n[#{@lang_to}]#{form_keys}[#{key}]", value_to, + class: "form-control #{dir}" - else - = text_field "i18n[#{@lang_to}]#{form_keys}", key, value: value_to, - class: 'form-control' + = text_field_tag "i18n[#{@lang_to}]#{form_keys}[#{key}]", value_to, + class: "form-control #{dir}" %small.text-muted.form-text= form_help diff --git a/config/deploy/production.rb b/config/deploy/production.rb new file mode 100644 index 0000000..94e514e --- /dev/null +++ b/config/deploy/production.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +set :deploy_user, 'app' +set :deploy_to, '/srv/http/sutty.kefir.red' +set :branch, 'rails' +set :rack_env, 'production' + +set :tmp_dir, "#{fetch :deploy_to}/tmp" +set :bundle_path, '/srv/http/gems.kefir.red' + +set :rbenv_type, :user +set :rbenv_ruby, '2.3.6' + +set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec" +set :rbenv_map_bins, %w(rake gem bundle ruby rails) +set :rbenv_roles, :all # default value + +# Evitar compilar nokogiri +set :bundle_env_variables, nokogiri_use_system_libraries: 1 + +server 'miso', + user: fetch(:deploy_user), + roles: %w(app web db) diff --git a/config/environments/production.rb b/config/environments/production.rb index 3edf2f5..5d3ad7d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -66,9 +66,6 @@ Rails.application.configure do # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true - config.i18n.available_locales = [:es, :en] - config.i18n.default_locale = :es - # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb new file mode 100644 index 0000000..71e5a74 --- /dev/null +++ b/config/initializers/locale.rb @@ -0,0 +1,4 @@ +Rails.application.configure do + config.i18n.available_locales = [:es, :en, :ar] + config.i18n.default_locale = :es +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 26c115c..ccd7b37 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1,7 @@ en: errors: argument_error: 'Argument `%{argument}` must be an instance of %{class}' + unknown_locale: 'Unknown %{locale} locale' login: email: 'E-mail' password: 'Password' diff --git a/config/locales/es.yml b/config/locales/es.yml index fc10257..0f97c52 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,6 +1,7 @@ es: errors: argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}' + unknown_locale: 'El idioma %{locale} es desconocido' login: email: 'Dirección de correo' password: 'Contraseña'