diff --git a/.rubocop.yml b/.rubocop.yml index eb261181..91d6fc6e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,24 +4,16 @@ AllCops: Style/AsciiComments: Enabled: false +# Sólo existe para molestarnos (?) +Metrics/AbcSize: + Enabled: false + Metrics/LineLength: Exclude: - 'db/schema.rb' - 'db/migrate/*.rb' - 'app/models/site.rb' -Metrics/AbcSize: - Exclude: - - 'db/schema.rb' - - 'db/migrate/*.rb' - - 'app/models/site.rb' - - 'app/controllers/sites_controller.rb' - - 'app/controllers/posts_controller.rb' - - 'app/controllers/invitadxs_controller.rb' - - 'app/controllers/i18n_controller.rb' - - 'app/controllers/usuaries_controller.rb' - - 'app/controllers/collaborations_controller.rb' - Metrics/MethodLength: Exclude: - 'db/schema.rb' diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index d4ea3f9b..71dce5a1 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -41,39 +41,13 @@ class PostsController < ApplicationController def create authorize Post @site = find_site - @lang = find_lang(@site) - @template = find_template(@site) - @post = Post.new(site: @site, lang: @lang, template: @template) - @post.update_attributes(repair_nested_params(post_params)) + service = PostService.new(site: @site, + usuarie: current_usuarie, + params: params) - # El post se guarda como incompleto si se envió con "guardar para - # después" - @post.update_attributes(incomplete: - params[:commit_incomplete].present?) - - # Las usuarias pueden especificar una autora, de la contrario por - # defecto es la usuaria actual - if @site.usuarie? current_usuarie - @post.update_attributes(author: params[:post][:author]) + if service.create.persisted? + redirect_to site_posts_path(@site) else - # Todo lo que crean les invitades es borrador - @post.update_attributes(draft: true) - end - unless @post.author - @post.update_attributes(author: - current_user.username) - end - - if @post.save - if @post.incomplete? - flash[:info] = @site.config.dig('incomplete') - else - flash[:success] = @site.config.dig('thanks') - end - - redirect_to site_posts_path(@site, lang: @lang) - else - @invalid = true render 'posts/new' end end @@ -129,26 +103,4 @@ class PostsController < ApplicationController redirect_to site_posts_path(@site, category: session[:category], lang: @lang) end - - private - - # Solo permitir cambiar estos atributos de cada articulo - def post_params - params.require(:post).permit(@post.template_params) - end - - # https://gist.github.com/bloudermilk/2884947#gistcomment-1915521 - def repair_nested_params(obj) - obj.each do |key, value| - if value.is_a?(ActionController::Parameters) || value.is_a?(Hash) - # If any non-integer keys - if value.keys.find { |k, _| k =~ /\D/ } - repair_nested_params(value) - else - obj[key] = value.values - obj[key].each { |h| repair_nested_params(h) } - end - end - end - end end diff --git a/app/models/metadata_array.rb b/app/models/metadata_array.rb index 06bd9474..ecdec085 100644 --- a/app/models/metadata_array.rb +++ b/app/models/metadata_array.rb @@ -6,4 +6,8 @@ class MetadataArray < MetadataTemplate def default_value [] end + + def to_param + { name => [] } + end end diff --git a/app/models/metadata_content.rb b/app/models/metadata_content.rb index ec5284ba..3573fc41 100644 --- a/app/models/metadata_content.rb +++ b/app/models/metadata_content.rb @@ -14,6 +14,10 @@ class MetadataContent < MetadataTemplate sanitize_options) end + def front_matter? + false + end + private # Etiquetas y atributos HTML a permitir diff --git a/app/models/metadata_document_date.rb b/app/models/metadata_document_date.rb index 3d98885c..2d785d0a 100644 --- a/app/models/metadata_document_date.rb +++ b/app/models/metadata_document_date.rb @@ -10,4 +10,9 @@ class MetadataDocumentDate < MetadataTemplate def value self[:value] || document.date || default_value end + + def value=(date) + date = date.to_time if date.is_a? String + super(date) + end end diff --git a/app/models/metadata_image.rb b/app/models/metadata_image.rb index c0bbebd5..e93e3fe3 100644 --- a/app/models/metadata_image.rb +++ b/app/models/metadata_image.rb @@ -10,4 +10,8 @@ class MetadataImage < MetadataTemplate def empty? value == default_value end + + def to_param + { name => %i[description path] } + end end diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index 2a48983e..04cd21d0 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -3,6 +3,8 @@ # Representa la plantilla de un campo en los metadatos del artículo # # TODO: Validar el tipo de valor pasado a value= según el :type +# +# rubocop:disable Metrics/BlockLength MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, :value, :help, :required, :errors, :post, :layout, keyword_init: true) do @@ -40,6 +42,15 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, errors.empty? end + def to_param + name + end + + # Decide si el metadato se coloca en el front_matter o no + def front_matter? + true + end + private # Si es obligatorio no puede estar vacío @@ -47,3 +58,4 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, true unless required && empty? end end +# rubocop:enable Metrics/BlockLength diff --git a/app/models/post.rb b/app/models/post.rb index de506879..117e3c5a 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -21,7 +21,6 @@ class Post < OpenStruct # @param document: [Jekyll::Document] el documento leído por Jekyll # @param layout: [Layout] la plantilla # - # rubocop:disable Metrics/AbcSize def initialize(**args) default_attributes_missing(args) super(args) @@ -40,6 +39,7 @@ class Post < OpenStruct post: self, site: site, name: name, + value: args[name.to_sym], layout: layout, type: template['type'], label: template['label'], @@ -54,7 +54,6 @@ class Post < OpenStruct # Leer el documento read end - # rubocop:enable Metrics/AbcSize def id path.basename @@ -105,19 +104,34 @@ class Post < OpenStruct end end + # Devuelve los strong params para el layout + def params + attributes.map do |attr| + send(attr).to_param + end + end + # Genera el post con metadatos en YAML - # rubocop:disable Metrics/AbcSize def full_content + body = '' yaml = layout.metadata.keys.map(&:to_sym).map do |metadata| template = send(metadata) - { metadata.to_s => template.value } unless template.empty? + unless template.front_matter? + body += "\n\n" + body += template.value + next + end + + next if template.empty? + + { metadata.to_s => template.value } end.compact.inject(:merge) # Asegurarse que haya un layout yaml['layout'] = layout.name.to_s - "#{yaml.to_yaml}---\n\n#{content.value}" + "#{yaml.to_yaml}---\n\n#{body}" end # Eliminar el artículo del repositorio y de la lista de artículos del @@ -134,7 +148,6 @@ class Post < OpenStruct !File.exist?(path.absolute) && !site.posts(lang: lang).include?(self) end alias destroy! destroy - # rubocop:enable Metrics/AbcSize # Guarda los cambios def save @@ -199,9 +212,16 @@ class Post < OpenStruct File.exist?(path.absolute) && full_content == File.read(path.absolute) end + def update_attributes(hashable) + hashable.to_hash.each do |name, value| + self[name].value = value + end + + save + end + private - # rubocop:disable Metrics/AbcSize def new_attribute_was(method) attr_was = (attribute_name(method).to_s + '_was').to_sym return attr_was if singleton_class.method_defined? attr_was @@ -229,7 +249,6 @@ class Post < OpenStruct (send(name).try(:value) || send(name)) != send(name_was) end end - # rubocop:enable Metrics/AbcSize # Obtiene el nombre del atributo a partir del nombre del método def attribute_name(attr) diff --git a/app/models/site/writer.rb b/app/models/site/writer.rb index eb0ccc37..a8e5b77f 100644 --- a/app/models/site/writer.rb +++ b/app/models/site/writer.rb @@ -5,6 +5,8 @@ Site::Writer = Struct.new(:site, :file, :content, keyword_init: true) do # TODO: si el archivo está bloqueado, esperar al desbloqueo def save + mkdir_p + File.open(file, File::RDWR | File::CREAT, 0o640) do |f| # Bloquear el archivo para que no sea accedido por otro # proceso u otre editore @@ -26,4 +28,12 @@ Site::Writer = Struct.new(:site, :file, :content, keyword_init: true) do def relative_file Pathname.new(file).relative_path_from(Pathname.new(site.path)).to_s end + + def dirname + File.dirname file + end + + def mkdir_p + FileUtils.mkdir_p dirname + end end diff --git a/app/services/post_service.rb b/app/services/post_service.rb new file mode 100644 index 00000000..6d2a1fce --- /dev/null +++ b/app/services/post_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Este servicio se encarga de crear artículos y guardarlos en git, +# asignándoselos a une usuarie +PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do + # Crea un artículo nuevo + # + # @return Post + def create + # TODO: Implementar layout + self.post = site.posts.build + # TODO: No podemos pasar los post_params a build aun porque para + # saber los parámetros tenemos que haber instanciado el post + # primero. + post.update_attributes(post_params) && + site.repository.commit(file: post.path.absolute, + usuarie: usuarie, + message: I18n.t('post_service.created', + title: post.title.value)) + + # Devolver el post aunque no se haya salvado para poder rescatar los + # errores + post + end + + private + + # Solo permitir cambiar estos atributos de cada articulo + def post_params + params.require(:post).permit(post.params) + end +end diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index d098ded4..5eb89873 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -25,10 +25,11 @@ -# Dibuja cada atributo - post.attributes.each do |attribute| - - type = post.send(attribute).type + - metadata = post.send(attribute) + - type = metadata.type = render "posts/attributes/#{type}", post: post, attribute: attribute, - metadata: post.send(attribute) + metadata: metadata -# Botones de guardado = render 'posts/submit', site: site diff --git a/doc/posts.md b/doc/posts.md index 852f351b..00b95182 100644 --- a/doc/posts.md +++ b/doc/posts.md @@ -97,12 +97,9 @@ Al instanciar un `Post`, se pasan el sitio y la plantilla por defecto. * Reimplementar glosario (se crea un artículo por cada categoría utilizada) * Reimplementar orden de artículos (ver doc) -* Convertir layout a params -* Reimplementar plantillas * Reimplementar subida de imagenes/archivos * Reimplementar campo 'pre' y 'post' en los layouts.yml * Implementar autoría como un array * Reimplementar draft e incomplete (por qué eran distintos?) - * Convertir idiomas disponibles a pestañas? * Implementar traducciones sin adivinar. Vincular artículos entre sí diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb new file mode 100644 index 00000000..6a095a56 --- /dev/null +++ b/test/controllers/posts_controller_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PostsControllerTest < ActionDispatch::IntegrationTest + setup do + @rol = create :rol + @site = @rol.site + @usuarie = @rol.usuarie + @post = @site.posts.build(title: SecureRandom.hex) + @post.save + + @authorization = { + Authorization: ActionController::HttpAuthentication::Basic + .encode_credentials(@usuarie.email, @usuarie.password) + } + end + + teardown do + @site.destroy + end + + test 'se pueden ver' do + get site_posts_url(@site), headers: @authorization + + assert_match @site.name, response.body + assert_match @post.title.value, response.body + end + + test 'se pueden crear nuevos' do + title = SecureRandom.hex + post site_posts_url(@site), headers: @authorization, + params: { + post: { + title: title, + date: 2.days.ago.strftime('%F') + } + } + + # TODO: implementar reload? + site = Site.find(@site.id) + site.read + new_post = site.posts.first + + assert_equal 302, response.status + + # XXX: No usamos follow_redirect! porque pierde la autenticación + get site_posts_url(@site), headers: @authorization + + assert_match new_post.title.value, response.body + assert_equal title, new_post.title.value + assert_equal I18n.t('post_service.created', title: new_post.title.value), + @site.repository.rugged.head.target.message + end +end diff --git a/test/factories/usuarie.rb b/test/factories/usuarie.rb index 18a4042e..0cff24f7 100644 --- a/test/factories/usuarie.rb +++ b/test/factories/usuarie.rb @@ -5,5 +5,6 @@ FactoryBot.define do email { SecureRandom.hex + '@sutty.nl' } password { SecureRandom.hex } confirmed_at { Date.today } + lang { I18n.default_locale.to_s } end end diff --git a/test/models/post_test.rb b/test/models/post_test.rb index a0ea5eb0..7b992403 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -127,7 +127,7 @@ class PostTest < ActiveSupport::TestCase end test 'al cambiar slug o fecha cambia el archivo de ubicacion' do - hoy = Date.today.to_time + hoy = 2.days.ago.to_time path_was = @post.path.absolute @post.slug.value = 'test' @post.date.value = hoy @@ -168,4 +168,12 @@ class PostTest < ActiveSupport::TestCase assert post.save assert File.exist?(post.path.absolute) end + + test 'se pueden inicializar con valores' do + post = @site.posts.build(title: 'test', content: 'test') + + assert_equal 'test', post.title.value + assert_equal 'test', post.content.value + assert post.save + end end