diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fe1a05e..8a28934 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -13,12 +13,18 @@ module ApplicationHelper root = names.shift names.each do |n| - root += "[#{n}]" + root += '[' + n.to_s + ']' end [root, name] end + def plain_field_name_for(*names) + root, name = field_name_for(*names) + + root + '[' + name.to_s + ']' + end + def distance_of_time_in_words_if_more_than_a_minute(seconds) if seconds > 60 distance_of_time_in_words seconds @@ -62,13 +68,15 @@ module ApplicationHelper # Opciones por defecto para el campo de un formulario def field_options(attribute, metadata, **extra) + required = metadata.required || extra[:required] + { class: "form-control #{invalid(metadata.post, attribute)} #{extra[:class]}", - required: metadata.required, + required: required, autofocus: (metadata.post.attributes.first == attribute), aria: { describedby: id_for_help(attribute), - required: metadata.required + required: required } } end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9887a2a..f3e8bc0 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -30,7 +30,58 @@ import "prosemirror-menu/style/menu.css" import "prosemirror-view/style/prosemirror.css" import "prosemirror-example-setup/style/style.css" +// Lista de equivalencias entre Date#getTimezoneOffset de JS y +// MetadataEvent +const timeZoneOffsets = { + '720': '-12:00', + '660': '-11:00', + '600': '-10:00', + '570': '-09:30', + '540': '-09:00', + '480': '-08:00', + '420': '-07:00', + '360': '-06:00', + '300': '-05:00', + '240': '-04:00', + '210': '-03:30', + '180': '-03:00', + '120': '-02:00', + '60': '-01:00', + '0': '00:00', + '-60': '+01:00', + '-120': '+02:00', + '-180': '+03:00', + '-210': '+03:30', + '-240': '+04:00', + '-270': '+04:30', + '-300': '+05:00', + '-330': '+05:30', + '-345': '+05:45', + '-360': '+06:00', + '-390': '+06:30', + '-420': '+07:00', + '-480': '+08:00', + '-525': '+08:45', + '-540': '+09:00', + '-570': '+09:30', + '-600': '+10:00', + '-630': '+10:30', + '-660': '+11:00', + '-720': '+12:00', + '-765': '+12:45', + '-780': '+13:00', + '-840': '+14:00' +}; + +// Obtiene el huso horario local +const timeZoneOffset = timeZoneOffsets[(new Date).getTimezoneOffset().toString()]; + document.addEventListener('turbolinks:load', () => { + + // Aplicar el huso horario descubierto en los campos de evento solo + // cuando estamos creando un artículo. + document.querySelectorAll('.new .event .zone select').forEach(zone => zone.value = timeZoneOffset); + document.querySelectorAll('.markdown-content').forEach(mdc => { let textArea = mdc.querySelector(".content"), editor = mdc.querySelector(".editor"); diff --git a/app/models/metadata_event.rb b/app/models/metadata_event.rb new file mode 100644 index 0000000..a458926 --- /dev/null +++ b/app/models/metadata_event.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# Gestiona eventos compatibles con jekyll-ical +class MetadataEvent < MetadataTemplate + # Preferimos los husos horarios en números porque los husos con + # nombres dejan afuera territorios que comparten el mismo huso que + # otras ciudades más hegemónicas. + # + # @see {https://en.wikipedia.org/wiki/List_of_UTC_time_offsets} + TIMEZONES = %w[-12:00 -11:00 -10:00 -09:30 -09:00 -08:00 -07:00 -06:00 + -05:00 -04:00 -03:30 -03:00 -02:00 -01:00 00:00 +01:00 +02:00 +03:00 + +03:30 +04:00 +04:30 +05:00 +05:30 +05:45 +06:00 +06:30 +07:00 +08:00 + +08:45 +09:00 +09:30 +10:00 +10:30 +11:00 +12:00 +12:45 +13:00 + +14:00].freeze + + # El valor por defecto es un Hash con algunas llaves pero queremos que + # sea opcional. + # + # @return [Hash] + def default_value + {} + end + + def to_param + { name => {} } + end + + # Dates are required and need to be parseable + def validate + self.errors = [] + times = [] + + %w[dtstart dtend].each do |dt| + errors << I18n.t("metadata.#{type}.zone_missing") unless TIMEZONES.include? value.dig(dt, 'zone') + errors << I18n.t("metadata.#{type}.date_missing") if value.dig(dt, 'date').blank? + + begin + Date.parse value.dig(dt, 'date') + rescue ArgumentError + errors << I18n.t("metadata.#{type}.date_non_parseable") + end + + unless (time = value.dig(dt, 'time')).blank? + errors << I18n.t("metadata.#{type}.time_non_parseable") unless /[0-5][0-9]:[0-5][0-9]/ =~ time + end + + times << value.dig(dt, 'date') + ' ' + value.dig(dt, 'time') + end + + begin + dtstart, dtend = times.map { |t| Time.parse t } + errors << I18n.t("metadata.#{type}.end_in_the_past") if dtstart > dtend + rescue ArgumentError + errors << I18n.t("metadata.#{type}.time_non_parseable") + end + + errors.empty? + end + + def sanitize(hash) + self[:value] = %w[dtstart dtend].map do |dt| + time = hash.dig(dt, 'time') + + { + dt => { + 'zone' => hash.dig(dt, 'zone'), + 'date' => Date.parse(hash.dig(dt, 'date')).to_s, + 'time' => time.blank? ? '00:00' : time + } + } + end.inject(:merge) + end +end diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index e6c0763..27cc3e7 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -22,9 +22,6 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, end # Valor actual o por defecto - # - # XXX: No estamos sanitizando la entrada, cada tipo tiene que - # auto-sanitizarse. def value self[:value] || document.data.fetch(name.to_s, default_value) end diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index 12e814d..3d32529 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -8,15 +8,17 @@ if post.new? url = site_posts_path(site, locale: @locale) method = :post + extra_class = 'new' else url = site_post_path(site, post.id, locale: @locale) method = :patch + extra_class = 'edit' end - dir = t("locales.#{@locale}.dir") -# Comienza el formulario -= form_tag url, method: method, class: 'form post', multipart: true do += form_tag url, method: method, class: 'form post ' + extra_class, multipart: true do -# Botones de guardado = render 'posts/submit', site: site, post: post diff --git a/app/views/posts/attribute_ro/_event.haml b/app/views/posts/attribute_ro/_event.haml new file mode 100644 index 0000000..2348174 --- /dev/null +++ b/app/views/posts/attribute_ro/_event.haml @@ -0,0 +1,7 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + %dl + - %i[dtstart dtend].each do |dt| + %dt= post_label_t(attribute, dt, post: post) + %dl= Time.parse %w[date time zone].map { |x| metadata.value[dt.to_s][x] }.join(' ') diff --git a/app/views/posts/attributes/_event.haml b/app/views/posts/attributes/_event.haml new file mode 100644 index 0000000..8ff9286 --- /dev/null +++ b/app/views/posts/attributes/_event.haml @@ -0,0 +1,32 @@ +-# + El evento tiene dos grupos, comienzo y final del evento, cada uno con + fecha, hora y zona horaria + +%fieldset.event + %legend= post_label_t(attribute, post: post) + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata + + - %i[dtstart dtend].each do |dt| + .row{ class: dt } + .col + .date.form-group + = label_tag "post_#{attribute}_#{dt}_date", + post_label_t(attribute, :date, post: post) + = date_field_tag(*field_name_for('post', attribute, dt, :date), + value: metadata.value.dig(dt.to_s, 'date'), + **field_options(attribute, metadata, required: true)) + .col + .time.form-group + = label_tag "post_#{attribute}_#{dt}_time", + post_label_t(attribute, :time, post: post) + = time_field_tag(*field_name_for('post', attribute, dt, :time), + value: metadata.value.dig(dt.to_s, 'time'), + **field_options(attribute, metadata)) + .col + .zone.form-group + = label_tag "post_#{attribute}_#{dt}_zone", + post_label_t(attribute, :zone, post: post) + = select_tag(plain_field_name_for('post', attribute, dt, :zone), + options_for_select(MetadataEvent::TIMEZONES, metadata.value.dig(dt.to_s, 'zone') || '00:00'), + **field_options(attribute, metadata)) diff --git a/config/locales/en.yml b/config/locales/en.yml index 1d3a086..f8add92 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -37,6 +37,12 @@ en: cant_be_empty: 'This field cannot be empty' image: cant_be_empty: 'This field cannot be empty' + event: + zone_missing: 'Timezone is incorrect' + date_missing: 'Event date is required' + date_non_parseable: 'Time is not in the correct format' + time_non_parseable: 'Date is not in the correct format' + end_in_the_past: "Event end can't happen before the start" exceptions: post: site_missing: 'Needs an instance of Site' diff --git a/config/locales/es.yml b/config/locales/es.yml index 10c756d..e94d212 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -39,6 +39,12 @@ es: cant_be_empty: 'El campo no puede estar vacío' not_an_image: 'No es una imagen' path_required: 'Se necesita un archivo de imagen' + event: + zone_missing: 'El huso horario no es correcto' + date_missing: 'La fecha es obligatoria' + date_non_parseable: 'La fecha no está en el formato correcto' + time_non_parseable: 'La hora no está en el formato correcto' + end_in_the_past: 'El fin del evento no puede ser anterior al comienzo' exceptions: post: site_missing: 'Necesita una instancia de Site' @@ -446,9 +452,13 @@ es: image: multiple: 'Puedes seleccionar varias imágenes usando la tecla Ctrl o Cmd en tu teclado.' url: 'La dirección debe comenzar con http:// o https://' - blank: En blanco + blank: Vacío destroy: Borrar - confirm_destroy: ¿Estás segurx? + confirm_destroy: ¿Estás segure? + form: + errors: + title: Hay errores en el formulario + help: Por favor, verifica que todos los valores sean correctos. usuaries: invite_as: usuaries: usuaries