diff --git a/Gemfile b/Gemfile index 4a62fcd..f6b2a2b 100644 --- a/Gemfile +++ b/Gemfile @@ -40,6 +40,10 @@ gem 'jquery-rails' gem 'font-awesome-rails' gem 'exception_notification' gem 'whenever', require: false +gem 'carrierwave' +gem 'carrierwave-i18n' +gem 'mini_magick' +gem 'carrierwave-bombshelter' group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index 2a1d590..09bb96a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,6 +75,15 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (>= 2.0, < 4.0) + carrierwave (1.2.3) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) + mime-types (>= 1.16) + carrierwave-bombshelter (0.2.2) + activesupport (>= 3.2.0) + carrierwave + fastimage + carrierwave-i18n (0.2.0) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) @@ -101,6 +110,7 @@ GEM actionmailer (>= 4.0, < 6) activesupport (>= 4.0, < 6) execjs (2.7.0) + fastimage (2.1.3) ffi (1.9.23) font-awesome-rails (4.7.0.4) railties (>= 3.2, < 6.0) @@ -161,6 +171,10 @@ GEM mini_mime (>= 0.1.1) mercenary (0.3.6) method_source (0.9.0) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mini_magick (4.8.0) mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.11.3) @@ -296,6 +310,9 @@ DEPENDENCIES capistrano-rails capistrano-rbenv capybara (~> 2.13) + carrierwave + carrierwave-bombshelter + carrierwave-i18n commonmarker dotenv-rails email_address @@ -306,6 +323,7 @@ DEPENDENCIES jekyll jquery-rails listen (>= 3.0.5, < 3.2) + mini_magick pry puma (~> 3.7) rails (~> 5.1.4) diff --git a/app/models/post.rb b/app/models/post.rb index 1680cad..02c1e38 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -199,9 +199,23 @@ class Post # HashWithIndifferentAccess attrs = attrs.to_h.map do |k,v| t = template_fields.find { |t| t.key == k } - if t && t.nested? - v = t.array? ? v.values.map(&:to_h) : v.to_h + if t + # Subir la imagen! + if t.image? + begin + i = Post::ImageUploader.new(site) + i.store! v.tempfile + v = i.url + rescue CarrierWave::IntegrityError => e + v = e.message + end + end + + if t.nested? + v = t.array? ? v.values.map(&:to_h) : v.to_h + end end + { k => v } end.reduce(Hash.new, :merge).stringify_keys @@ -209,10 +223,20 @@ class Post end # Requisitos para que el post sea válido + # TODO verificar que el id sea único + # TODO validar los parametros de la plantilla def validate add_error validate: I18n.t('posts.errors.date') unless date.is_a? Time add_error validate: I18n.t('posts.errors.title') if title.blank? - # TODO verificar que el id sea único + # XXX este es un principio de validación de plantillas, aunque no es + # recursivo + template_fields.each do |tf| + # TODO descubrir de mejor forma que el archivo no es del formato + # correcto + if tf.image? && get_front_matter(tf.key).split('/').to_a.count <= 1 + add_error validate: get_front_matter(tf.key) + end + end end def valid? diff --git a/app/models/post/image_uploader.rb b/app/models/post/image_uploader.rb new file mode 100644 index 0000000..ec46e34 --- /dev/null +++ b/app/models/post/image_uploader.rb @@ -0,0 +1,39 @@ +# Una clase que permite adjuntar imágenes a artículos +class Post::ImageUploader < CarrierWave::Uploader::Base + + attr_accessor :site + + # Necesitamos pasar el sitio para poder acceder a los archivos locales + def initialize(site) + super + + @site = site + end + + # Solo aceptamos imágenes + def content_type_whitelist + /\Aimage\// + end + + # Devuelve la ubicación del directorio dentro de Jekyll, para eso, + # tenemos que tener acceso al sitio que estamos editando + def store_dir + File.join site.path, 'public', 'images' + end + + # Almacenar en el 'tmp' local + def cache_dir + File.join Rails.root, 'tmp', 'images' + end + + # XXX los nombres de los archivos siempre son únicos, no chequeamos si + # están repetidos. + def filename + [SecureRandom.uuid, '.', file.extension].join + end + + # Obtener la URL dentro del proyecto de Jekyll + def url + CGI.unescape(super).remove site.path + end +end diff --git a/app/models/post/template_field.rb b/app/models/post/template_field.rb index b77ce49..e768fb6 100644 --- a/app/models/post/template_field.rb +++ b/app/models/post/template_field.rb @@ -4,7 +4,11 @@ class Post class TemplateField attr_reader :post, :contents, :key - STRING_VALUES = %w[string text url number email password date year].freeze + STRING_VALUES = %w[string text url number email password date year + image video audio document].freeze + + # Tipo de valores que son archivos + FILE_TYPES = %w[image video audio document].freeze def initialize(post, key, contents) @post = post @@ -32,6 +36,8 @@ class Post return @type if @type case + when image? + @type = 'image' when email? @type = 'email' when url? @@ -120,6 +126,14 @@ class Post value == 'year' end + def file? + string? && FILE_TYPES.include?(value) + end + + def image? + value == 'image' + end + # Si la plantilla es simple no está admitiendo Hashes como valores def simple? !complex? diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index d354cd9..327f349 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -15,7 +15,7 @@ - else - url = site_post_path(@site, @post, lang: @lang) - method = :patch -= form_tag url, method: method, class: 'form', novalidate: true do += form_tag url, method: method, class: 'form', novalidate: true, multipart: true do = hidden_field_tag 'template', params[:template] .form-group = submit_tag t('posts.save'), class: 'btn btn-success' diff --git a/app/views/posts/template_field/_image.haml b/app/views/posts/template_field/_image.haml new file mode 100644 index 0000000..2aa6c64 --- /dev/null +++ b/app/views/posts/template_field/_image.haml @@ -0,0 +1,3 @@ += file_field_tag field_name_for_post_as_string(name), + class: 'form-control', + required: template.required? diff --git a/config/environments/development.rb b/config/environments/development.rb index 5187e22..b7e11ef 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -51,4 +51,10 @@ Rails.application.configure do # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + CarrierWave.configure do |config| + config.ignore_integrity_errors = false + config.ignore_processing_errors = false + config.ignore_download_errors = false + end end diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb new file mode 100644 index 0000000..2ff728f --- /dev/null +++ b/config/initializers/carrierwave.rb @@ -0,0 +1,5 @@ +CarrierWave.configure do |config| + config.permissions = 0640 + config.directory_permissions = 0750 + config.storage = :file +end diff --git a/doc/uploads.md b/doc/uploads.md new file mode 100644 index 0000000..4771e31 --- /dev/null +++ b/doc/uploads.md @@ -0,0 +1,21 @@ +# Subida de archivos + +La subida de archivos se genera a partir de agregar el valor a una +plantilla: + +```yaml +--- +cover: image +attachment: document +video: video +audio: audio +--- +``` + +Donde `image` solo admite imágenes, `document` cualquier tipo de documento y +`video` y `audio` medios. + +Al subir los archivos, se guardan en el directorio `public/` en la raíz +del proyecto Jekyll. + +En el frontmatter solo sale la URL del archivo asociado.