crear artículos
This commit is contained in:
parent
cd933c7b38
commit
9469cb41e6
15 changed files with 175 additions and 79 deletions
16
.rubocop.yml
16
.rubocop.yml
|
@ -4,24 +4,16 @@ AllCops:
|
||||||
Style/AsciiComments:
|
Style/AsciiComments:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
# Sólo existe para molestarnos (?)
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Metrics/LineLength:
|
Metrics/LineLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/schema.rb'
|
- 'db/schema.rb'
|
||||||
- 'db/migrate/*.rb'
|
- 'db/migrate/*.rb'
|
||||||
- 'app/models/site.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:
|
Metrics/MethodLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/schema.rb'
|
- 'db/schema.rb'
|
||||||
|
|
|
@ -41,39 +41,13 @@ class PostsController < ApplicationController
|
||||||
def create
|
def create
|
||||||
authorize Post
|
authorize Post
|
||||||
@site = find_site
|
@site = find_site
|
||||||
@lang = find_lang(@site)
|
service = PostService.new(site: @site,
|
||||||
@template = find_template(@site)
|
usuarie: current_usuarie,
|
||||||
@post = Post.new(site: @site, lang: @lang, template: @template)
|
params: params)
|
||||||
@post.update_attributes(repair_nested_params(post_params))
|
|
||||||
|
|
||||||
# El post se guarda como incompleto si se envió con "guardar para
|
if service.create.persisted?
|
||||||
# después"
|
redirect_to site_posts_path(@site)
|
||||||
@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])
|
|
||||||
else
|
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'
|
render 'posts/new'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -129,26 +103,4 @@ class PostsController < ApplicationController
|
||||||
redirect_to site_posts_path(@site, category: session[:category],
|
redirect_to site_posts_path(@site, category: session[:category],
|
||||||
lang: @lang)
|
lang: @lang)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -6,4 +6,8 @@ class MetadataArray < MetadataTemplate
|
||||||
def default_value
|
def default_value
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
{ name => [] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,10 @@ class MetadataContent < MetadataTemplate
|
||||||
sanitize_options)
|
sanitize_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def front_matter?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Etiquetas y atributos HTML a permitir
|
# Etiquetas y atributos HTML a permitir
|
||||||
|
|
|
@ -10,4 +10,9 @@ class MetadataDocumentDate < MetadataTemplate
|
||||||
def value
|
def value
|
||||||
self[:value] || document.date || default_value
|
self[:value] || document.date || default_value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def value=(date)
|
||||||
|
date = date.to_time if date.is_a? String
|
||||||
|
super(date)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,4 +10,8 @@ class MetadataImage < MetadataTemplate
|
||||||
def empty?
|
def empty?
|
||||||
value == default_value
|
value == default_value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
{ name => %i[description path] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
# Representa la plantilla de un campo en los metadatos del artículo
|
# 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
|
# 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,
|
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
:value, :help, :required, :errors, :post,
|
:value, :help, :required, :errors, :post,
|
||||||
:layout, keyword_init: true) do
|
:layout, keyword_init: true) do
|
||||||
|
@ -40,6 +42,15 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
errors.empty?
|
errors.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
name
|
||||||
|
end
|
||||||
|
|
||||||
|
# Decide si el metadato se coloca en el front_matter o no
|
||||||
|
def front_matter?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Si es obligatorio no puede estar vacío
|
# Si es obligatorio no puede estar vacío
|
||||||
|
@ -47,3 +58,4 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
true unless required && empty?
|
true unless required && empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/BlockLength
|
||||||
|
|
|
@ -21,7 +21,6 @@ class Post < OpenStruct
|
||||||
# @param document: [Jekyll::Document] el documento leído por Jekyll
|
# @param document: [Jekyll::Document] el documento leído por Jekyll
|
||||||
# @param layout: [Layout] la plantilla
|
# @param layout: [Layout] la plantilla
|
||||||
#
|
#
|
||||||
# rubocop:disable Metrics/AbcSize
|
|
||||||
def initialize(**args)
|
def initialize(**args)
|
||||||
default_attributes_missing(args)
|
default_attributes_missing(args)
|
||||||
super(args)
|
super(args)
|
||||||
|
@ -40,6 +39,7 @@ class Post < OpenStruct
|
||||||
post: self,
|
post: self,
|
||||||
site: site,
|
site: site,
|
||||||
name: name,
|
name: name,
|
||||||
|
value: args[name.to_sym],
|
||||||
layout: layout,
|
layout: layout,
|
||||||
type: template['type'],
|
type: template['type'],
|
||||||
label: template['label'],
|
label: template['label'],
|
||||||
|
@ -54,7 +54,6 @@ class Post < OpenStruct
|
||||||
# Leer el documento
|
# Leer el documento
|
||||||
read
|
read
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/AbcSize
|
|
||||||
|
|
||||||
def id
|
def id
|
||||||
path.basename
|
path.basename
|
||||||
|
@ -105,19 +104,34 @@ class Post < OpenStruct
|
||||||
end
|
end
|
||||||
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
|
# Genera el post con metadatos en YAML
|
||||||
# rubocop:disable Metrics/AbcSize
|
|
||||||
def full_content
|
def full_content
|
||||||
|
body = ''
|
||||||
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata|
|
yaml = layout.metadata.keys.map(&:to_sym).map do |metadata|
|
||||||
template = send(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)
|
end.compact.inject(:merge)
|
||||||
|
|
||||||
# Asegurarse que haya un layout
|
# Asegurarse que haya un layout
|
||||||
yaml['layout'] = layout.name.to_s
|
yaml['layout'] = layout.name.to_s
|
||||||
|
|
||||||
"#{yaml.to_yaml}---\n\n#{content.value}"
|
"#{yaml.to_yaml}---\n\n#{body}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Eliminar el artículo del repositorio y de la lista de artículos del
|
# 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)
|
!File.exist?(path.absolute) && !site.posts(lang: lang).include?(self)
|
||||||
end
|
end
|
||||||
alias destroy! destroy
|
alias destroy! destroy
|
||||||
# rubocop:enable Metrics/AbcSize
|
|
||||||
|
|
||||||
# Guarda los cambios
|
# Guarda los cambios
|
||||||
def save
|
def save
|
||||||
|
@ -199,9 +212,16 @@ class Post < OpenStruct
|
||||||
File.exist?(path.absolute) && full_content == File.read(path.absolute)
|
File.exist?(path.absolute) && full_content == File.read(path.absolute)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_attributes(hashable)
|
||||||
|
hashable.to_hash.each do |name, value|
|
||||||
|
self[name].value = value
|
||||||
|
end
|
||||||
|
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# rubocop:disable Metrics/AbcSize
|
|
||||||
def new_attribute_was(method)
|
def new_attribute_was(method)
|
||||||
attr_was = (attribute_name(method).to_s + '_was').to_sym
|
attr_was = (attribute_name(method).to_s + '_was').to_sym
|
||||||
return attr_was if singleton_class.method_defined? attr_was
|
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)
|
(send(name).try(:value) || send(name)) != send(name_was)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/AbcSize
|
|
||||||
|
|
||||||
# Obtiene el nombre del atributo a partir del nombre del método
|
# Obtiene el nombre del atributo a partir del nombre del método
|
||||||
def attribute_name(attr)
|
def attribute_name(attr)
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
Site::Writer = Struct.new(:site, :file, :content, keyword_init: true) do
|
Site::Writer = Struct.new(:site, :file, :content, keyword_init: true) do
|
||||||
# TODO: si el archivo está bloqueado, esperar al desbloqueo
|
# TODO: si el archivo está bloqueado, esperar al desbloqueo
|
||||||
def save
|
def save
|
||||||
|
mkdir_p
|
||||||
|
|
||||||
File.open(file, File::RDWR | File::CREAT, 0o640) do |f|
|
File.open(file, File::RDWR | File::CREAT, 0o640) do |f|
|
||||||
# Bloquear el archivo para que no sea accedido por otro
|
# Bloquear el archivo para que no sea accedido por otro
|
||||||
# proceso u otre editore
|
# proceso u otre editore
|
||||||
|
@ -26,4 +28,12 @@ Site::Writer = Struct.new(:site, :file, :content, keyword_init: true) do
|
||||||
def relative_file
|
def relative_file
|
||||||
Pathname.new(file).relative_path_from(Pathname.new(site.path)).to_s
|
Pathname.new(file).relative_path_from(Pathname.new(site.path)).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dirname
|
||||||
|
File.dirname file
|
||||||
|
end
|
||||||
|
|
||||||
|
def mkdir_p
|
||||||
|
FileUtils.mkdir_p dirname
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
32
app/services/post_service.rb
Normal file
32
app/services/post_service.rb
Normal file
|
@ -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
|
|
@ -25,10 +25,11 @@
|
||||||
|
|
||||||
-# Dibuja cada atributo
|
-# Dibuja cada atributo
|
||||||
- post.attributes.each do |attribute|
|
- post.attributes.each do |attribute|
|
||||||
- type = post.send(attribute).type
|
- metadata = post.send(attribute)
|
||||||
|
- type = metadata.type
|
||||||
= render "posts/attributes/#{type}",
|
= render "posts/attributes/#{type}",
|
||||||
post: post, attribute: attribute,
|
post: post, attribute: attribute,
|
||||||
metadata: post.send(attribute)
|
metadata: metadata
|
||||||
|
|
||||||
-# Botones de guardado
|
-# Botones de guardado
|
||||||
= render 'posts/submit', site: site
|
= render 'posts/submit', site: site
|
||||||
|
|
|
@ -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
|
* Reimplementar glosario (se crea un artículo por cada categoría
|
||||||
utilizada)
|
utilizada)
|
||||||
* Reimplementar orden de artículos (ver doc)
|
* Reimplementar orden de artículos (ver doc)
|
||||||
* Convertir layout a params
|
|
||||||
* Reimplementar plantillas
|
|
||||||
* Reimplementar subida de imagenes/archivos
|
* Reimplementar subida de imagenes/archivos
|
||||||
* Reimplementar campo 'pre' y 'post' en los layouts.yml
|
* Reimplementar campo 'pre' y 'post' en los layouts.yml
|
||||||
* Implementar autoría como un array
|
* Implementar autoría como un array
|
||||||
* Reimplementar draft e incomplete (por qué eran distintos?)
|
* Reimplementar draft e incomplete (por qué eran distintos?)
|
||||||
|
|
||||||
* Convertir idiomas disponibles a pestañas?
|
* Convertir idiomas disponibles a pestañas?
|
||||||
* Implementar traducciones sin adivinar. Vincular artículos entre sí
|
* Implementar traducciones sin adivinar. Vincular artículos entre sí
|
||||||
|
|
55
test/controllers/posts_controller_test.rb
Normal file
55
test/controllers/posts_controller_test.rb
Normal file
|
@ -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
|
|
@ -5,5 +5,6 @@ FactoryBot.define do
|
||||||
email { SecureRandom.hex + '@sutty.nl' }
|
email { SecureRandom.hex + '@sutty.nl' }
|
||||||
password { SecureRandom.hex }
|
password { SecureRandom.hex }
|
||||||
confirmed_at { Date.today }
|
confirmed_at { Date.today }
|
||||||
|
lang { I18n.default_locale.to_s }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -127,7 +127,7 @@ class PostTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'al cambiar slug o fecha cambia el archivo de ubicacion' do
|
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
|
path_was = @post.path.absolute
|
||||||
@post.slug.value = 'test'
|
@post.slug.value = 'test'
|
||||||
@post.date.value = hoy
|
@post.date.value = hoy
|
||||||
|
@ -168,4 +168,12 @@ class PostTest < ActiveSupport::TestCase
|
||||||
assert post.save
|
assert post.save
|
||||||
assert File.exist?(post.path.absolute)
|
assert File.exist?(post.path.absolute)
|
||||||
end
|
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue