diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index fe1a05e2..8a289346 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 9887a2a7..f3e8bc09 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 00000000..a4589262
--- /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 e6c0763a..27cc3e75 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 12e814dc..3d325292 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 00000000..2348174a
--- /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 00000000..8ff9286d
--- /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 1d3a0864..f8add921 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 10c756d6..e94d2121 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