escribir articulos nuevos y modificar los que existen

This commit is contained in:
f 2018-02-03 19:37:09 -03:00
parent 2fcc631548
commit b83528cd96
No known key found for this signature in database
GPG key ID: F3FDAB97B5F9F7E7
11 changed files with 249 additions and 114 deletions

View file

@ -10,6 +10,22 @@ class PostsController < ApplicationController
@post = find_post(@site)
end
def new
@site = find_site
@post = Post.new(site: @site, front_matter: { date: Time.now })
end
def create
@site = find_site
@post = Post.new(site: @site, front_matter: post_params.to_hash)
if @post.save
redirect_to site_posts_path(@site)
else
render 'posts/new'
end
end
def edit
@site = find_site
@post = find_post(@site)
@ -20,12 +36,8 @@ class PostsController < ApplicationController
@site = find_site
@post = find_post(@site)
# crear un array a partir de una cadena separada por comas
[:tags,:categories].each do |comma|
p[comma] = p.dig(comma).split(',').map(&:strip)
end
@post.update_attributes(p)
if @post.save
redirect_to site_post_path(@site, @post)
else

View file

@ -3,119 +3,180 @@ require 'jekyll/utils'
# Esta clase representa un post en un sitio jekyll e incluye métodos
# para modificarlos y crear nuevos
#
# Cuando estamos editando un post, instanciamos este modelo y le
# asociamos el Jekyll::Document correspondiente.
#
# Cuando estamos creando un post, no creamos su Jekyll::Document
# hasta que se guardan los datos, porque para poder guardarlo
# necesitamos su front_matter completo.
#
# El front matter está duplicado. El Post mantiene una copia de los
# datos y los sincroniza al momento de leer y de escribir el Document.
class Post
attr_accessor :content, :front_matter
attr_reader :post, :site, :errors
attr_reader :post, :site, :errors, :old_post
REJECT_FROM_DATA = %w[excerpt slug draft date ext].freeze
REJECT_FROM_DATA = %w[excerpt].freeze
# datos que no tienen que terminar en el front matter
REJECT_FROM_FRONT_MATTER = %w[date slug draft ext].freeze
# Trabajar con posts. Si estamos creando uno nuevo, el **site** y
# el **front_matter** son necesarios, sino, **site** y **post**.
# XXX chequear que se den las condiciones
def initialize(site:, post: nil, front_matter: {})
raise ArgumentError, I18n.t('posts.errors.site') unless site.is_a?(Site)
raise ArgumentError, I18n.t('posts.errors.post') unless post.is_a?(Jekyll::Document)
unless site.is_a?(Site)
raise ArgumentError,
I18n.t('errors.argument_error', argument: :site, class: Site)
end
unless post.nil? || post.is_a?(Jekyll::Document)
raise ArgumentError,
I18n.t('errors.argument_error', argument: :post,
class: Jekyll::Document)
end
@site = site
@post = post
# los errores tienen que ser un hash para que
# ActiveModel pueda traer los errores normalmente
@errors = {}
@front_matter = front_matter
# Crea un post nuevo si no especificamos el post
new_post unless @post
load_data! unless new?
# sincronizar los datos del document
if new?
@front_matter = {}
update_attributes front_matter
else
load_front_matter!
merge_with_front_matter! front_matter.stringify_keys
end
end
# Si el archivo destino no existe, el post es nuevo
# Limpiar los errores
def reset_errors!
@errors = {}
end
# El post es nuevo si no hay un documento asociado
def new?
!File.exist? @post.path
@post.nil? || !File.exist?(@post.try(:path))
end
# Guarda los cambios
# Guarda los cambios.
#
# Recién cuando vamos a guardar creamos el Post, porque ya tenemos
# todos los datos para escribir el archivo, que es la condición
# necesaria para poder crearlo :P
def save
merge_data_with_front_matter!
clean_content!
cleanup!
return false unless valid?
new_post if new?
return unless write
return unless detect_file_rename!
# Vuelve a leer el post para tomar los cambios
@post.read
add_post_to_site!
true
end
alias :save! :save
def title
get_metadata 'title'
get_front_matter 'title'
end
def date
get_metadata 'date'
get_front_matter 'date'
end
def tags
get_metadata 'tags'
get_front_matter 'tags'
end
def categories
get_metadata 'categories'
get_front_matter 'categories'
end
alias :category :categories
# Devuelve la ruta del post, si se cambió alguno de los datos,
# generamos una ruta nueva para tener siempre la ruta actualizada.
def path
basename_changed? ? File.join(@site.path, '_posts', basename_from_front_matter) : @post.try(:path)
end
# TODO los slugs se pueden repetir, el identificador real sería
# fecha+slug, pero se ve feo en las urls?
def id
get_metadata 'slug'
get_front_matter 'slug'
end
alias :slug :id
alias :to_s :id
def basename_changed?
@post.basename != basename_from_front_matter
end
def data
@post.data
@post.try(:basename) != basename_from_front_matter
end
def content
@content ||= @post.content
@content ||= @post.try :content
end
# imita Model.update_attributes de ActiveRecord
def update_attributes(attrs)
attrs.each_pair do |k,i|
# el contenido no es metadata
if k == 'content'
@content = i
else
set_metadata k.to_sym, i
# el cuerpo se maneja por separado
@content = attrs.delete('content') if attrs.key? 'content'
merge_with_front_matter! attrs.stringify_keys
end
# Requisitos para que el post sea válido
def validate
add_error validate: I18n.t('posts.errors.date') unless get_front_matter('date').is_a? Time
add_error validate: I18n.t('posts.errors.title') if get_front_matter('title').blank?
# TODO verificar que el id sea único
end
def valid?
reset_errors!
validate
@errors.empty?
end
# Permite ordenar los posts
def <=>(other)
@post <=> other.post
end
private
# Genera un post nuevo y lo agrega a la colección del sitio.
def new_post
opts = { site: @site, collection: @site.jekyll.posts }
opts = { site: @site.jekyll, collection: @site.jekyll.posts }
@post = Jekyll::Document.new(path, opts)
end
# Solo agregar el post al sitio una vez que lo guardamos
#
# TODO no sería la forma correcta de hacerlo en Rails
def add_post_to_site!
@site.jekyll.posts.docs << @post
@site.jekyll.posts.docs.sort!
@post
@site.posts << self unless @site.posts.include? self
@site.posts.sort!
end
# Obtiene metadatos
def get_metadata(name)
@front_matter.key?(name) ? @front_matter.dig(name) : @post.data[name]
# Obtiene metadatos asegurándose que siempre trabajamos con strings
def get_front_matter(name)
@front_matter.dig(name.to_s)
end
def set_metadata(name, value)
@front_matter[name] = value
# Los define, asegurandose que las llaves siempre son strings, para no
# tener incompatibilidades con jekyll
def set_front_matter(name, value)
@front_matter[name.to_s] = value
end
# Cambiar el nombre del archivo si cambió el título o la fecha.
@ -123,18 +184,17 @@ class Post
# engañamos eliminando la instancia de @post y recargando otra.
def detect_file_rename!
return true unless basename_changed?
# No eliminamos el archivo a menos que ya exista el reemplazo!
return false unless File.exist? path
if File.exist? path
add_error path: I18n.t('posts.errors.path')
return
Rails.logger.info I18n.t('posts.logger.rm', path: path)
FileUtils.rm @post.path
replace_post!
end
FileUtils.mv @post.path, path
replace_post! path
end
def replace_post!(path)
@site.jekyll.posts.docs.delete @post
# Reemplaza el post en el sitio por uno nuevo
def replace_post!
@old_post = @site.jekyll.posts.docs.delete @post
new_post
end
@ -142,40 +202,47 @@ class Post
# Obtiene el nombre del archivo a partir de los datos que le
# pasemos
def basename_from_front_matter
_date = @front_matter[:date]
date = _date.respond_to?(:strftime) ? _date.strftime('%F') : _date
title = get_metadata 'slug' || Jekyll::Utils.slugify(@front_matter[:title])
ext = get_metadata 'ext' || '.markdown'
date = get_front_matter('date').strftime('%F')
slug = get_front_matter('slug')
slug =
ext = get_front_matter('ext') || '.markdown'
"#{date}-#{title}#{ext}"
"#{date}-#{slug}#{ext}"
end
# Toma los datos del front matter local y los mueve a los datos
# que van a ir al post. Si hay símbolos se convierten a cadenas,
# porque Jekyll trabaja con cadenas. Se excluyen otros datos que no
# van en el frontmatter
def merge_data_with_front_matter!
@data.merge! Hash[@front_matter.map do |k, v|
[k.to_s, v] unless REJECT_FROM_DATA.include? k
def merge_with_front_matter!(params)
@front_matter.merge! Hash[params.to_hash.map do |k, v|
[k, v] unless REJECT_FROM_DATA.include? k
end.compact]
end
# Carga una copia de los datos del post original excluyendo datos
# que no nos interesan
def load_data!
@data ||= @post.data.reject do |key, _|
def load_front_matter!
@front_matter = @post.data.reject do |key, _|
REJECT_FROM_DATA.include? key
end
end
def cleanup!
things_to_arrays!
date_to_time!
clean_content!
slugify_title!
end
# Aplica limpiezas básicas del contenido
def clean_content!
@content = @content.delete("\r")
@content.try(:delete!, "\r")
end
# Guarda los cambios en el archivo destino
def write
r = File.open(@post.path, File::RDWR | File::CREAT, 0o640) do |f|
r = File.open(path, File::RDWR | File::CREAT, 0o640) do |f|
# Bloquear el archivo para que no sea accedido por otro
# proceso u otra editora
f.flock(File::LOCK_EX)
@ -197,8 +264,14 @@ class Post
false
end
# Genera el post con front matter, menos los campos que no necesitamos
# que estén en el front matter
def full_content
"#{@data.to_yaml}---\n\n#{@content}"
yaml = @front_matter.reject do |k,_|
REJECT_FROM_FRONT_MATTER.include? k
end
"#{yaml.to_yaml}---\n\n#{@content}"
end
def add_error(hash)
@ -212,4 +285,23 @@ class Post
@errors
end
def date_to_time!
unless @front_matter.dig(:date).is_a? Time
@front_matter['date'] = @front_matter.dig('date').try(:to_time) || Time.now
end
end
def things_to_arrays!
[:tags,:categories].each do |c|
@front_matter[c.to_s] = @front_matter.dig(c.to_s).split(',').map(&:strip)
end
end
def slugify_title!
if get_front_matter('slug').blank?
title = get_front_matter('title')
@front_matter['slug'] = Jekyll::Utils.slugify(title)
end
end
end

View file

@ -26,6 +26,8 @@ class Site
def posts
return @posts if @posts
Rails.logger.info 'Procesando posts'
if @jekyll.posts.docs.empty?
@jekyll.read
# Queremos saber cuantas veces releemos los articulos

View file

@ -1,10 +1,17 @@
- unless @post.errors.empty?
.alert.alert-danger
%ul
- @post.errors.each do |error|
- @post.errors.each do |_,error|
%li= error
= form_tag site_post_path(@site, @post), method: :patch, class: 'form' do
-# TODO habilitar form_for
- if @post.new?
- url = site_posts_path(@site)
- method = :post
- else
- url = site_post_path(@site, @post)
- method = :patch
= form_tag url, method: method, class: 'form' do
.form-group
= submit_tag t('posts.save'), class: 'btn btn-success'
.form-group
@ -12,20 +19,21 @@
placeholder: t('posts.title')
.form-group
= text_area_tag 'post[content]', @post.content, autofocus: true,
class: 'form-control post-content', data: { provide: 'markdown' }
class: 'form-control post-content', data: { provide: 'markdown' },
cols: 72, wrap: 'hard'
.form-group
= label_tag 'post_date', t('posts.date')
= date_field 'post', 'date', value: @post.date.strftime('%F'),
= date_field 'post', 'date', value: @post.date.try(:strftime, '%F'),
class: 'form-control'
%small.text-muted.form-text= t('posts.date_help')
.form-group
= label_tag 'post_categories', t('posts.categories')
= text_field 'post', 'categories', value: @post.categories.join(', '),
= text_field 'post', 'categories', value: @post.categories.try(:join, ', '),
class: 'form-control'
%small.text-muted.form-text= t('posts.tags_help')
.form-group
= label_tag 'post_tags', t('posts.tags')
= text_field 'post', 'tags', value: @post.tags.join(', '),
= text_field 'post', 'tags', value: @post.tags.try(:join, ', '),
class: 'form-control'
%small.text-muted.form-text= t('posts.tags_help')
.form-group

View file

@ -1,9 +1,3 @@
.container-fluid
.row
%h1
= t('posts.editing')
= @post.title
.row
.row
.col
= render 'posts/form'

View file

@ -1,6 +1,15 @@
%h1= @site.name
.row
.col
%h1= @site.name
%table.table.table-condensed.table-striped
.row
.col
= link_to t('posts.new'), new_site_post_path(@site),
class: 'btn btn-success'
.row
.col
%table.table.table-condensed.table-striped
%tbody
- @site.posts.each do |post|
%tr

3
app/views/posts/new.haml Normal file
View file

@ -0,0 +1,3 @@
.row
.col
= render 'posts/form'

View file

@ -1,17 +1,21 @@
.row
.col
%h1= @post.title
.row
.col
= link_to t('posts.edit'), edit_site_post_path(@site, @post),
class: 'btn btn-info'
.row
.col
.content
:markdown
#{@post.content}
%table.table.table-condensed.table-striped
%tbody
- @post.data.each do |key, data|
- @post.front_matter.each do |key, data|
%tr
%th= key
%td

View file

@ -1,4 +1,6 @@
en:
errors:
argument_error: 'Argument `%{argument}` must be an instance of %{class}'
login:
email: 'E-mail'
password: 'Password'
@ -19,8 +21,11 @@ en:
categories_help: 'Comma separated!'
slug: 'Slug'
slug_help: 'This is the name of the article on the URL, ie. /title/. You can leave it empty.'
logger:
rm: 'Removed %{path}'
errors:
site: 'Argument `site` must be of class Site'
post: 'Argument `post` must be of class Post'
path: 'File already exist'
file: "Couldn't write the file"
title: 'Post needs a title'
date: 'Post needs a valid date'

View file

@ -1,4 +1,6 @@
es:
errors:
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
login:
email: 'Dirección de correo'
password: 'Contraseña'
@ -17,6 +19,10 @@ es:
categories_help: '¡Separadas por comas!'
slug: 'Nombre la URL'
slug_help: 'Esto es el nombre del artículo en la URL, por ejemplo /título/. Puedes dejarlo vacío.'
logger:
rm: 'Eliminado %{path}'
errors:
path: 'El archivo destino ya existe'
file: 'No se pudo escribir el archivo'
title: 'Necesita un título'
date: 'Necesita una fecha'

View file

@ -8,7 +8,7 @@ abbrev@1:
"bootstrap-markdown@https://0xacab.org/itacate-kefir/bootstrap-markdown.git":
version "2.10.0"
resolved "https://0xacab.org/itacate-kefir/bootstrap-markdown.git#449141351caa8d96e99fe09f02772ab91e578ab4"
resolved "https://0xacab.org/itacate-kefir/bootstrap-markdown.git#580184dc214ea2364fca0fdcb70e3c5e08bd5605"
markdown@^0.5.0:
version "0.5.0"