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) @post = find_post(@site)
end 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 def edit
@site = find_site @site = find_site
@post = find_post(@site) @post = find_post(@site)
@ -20,12 +36,8 @@ class PostsController < ApplicationController
@site = find_site @site = find_site
@post = find_post(@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) @post.update_attributes(p)
if @post.save if @post.save
redirect_to site_post_path(@site, @post) redirect_to site_post_path(@site, @post)
else else

View file

@ -3,119 +3,180 @@ require 'jekyll/utils'
# Esta clase representa un post en un sitio jekyll e incluye métodos # Esta clase representa un post en un sitio jekyll e incluye métodos
# para modificarlos y crear nuevos # 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 class Post
attr_accessor :content, :front_matter 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 # Trabajar con posts. Si estamos creando uno nuevo, el **site** y
# el **front_matter** son necesarios, sino, **site** y **post**. # el **front_matter** son necesarios, sino, **site** y **post**.
# XXX chequear que se den las condiciones # XXX chequear que se den las condiciones
def initialize(site:, post: nil, front_matter: {}) def initialize(site:, post: nil, front_matter: {})
raise ArgumentError, I18n.t('posts.errors.site') unless site.is_a?(Site) unless site.is_a?(Site)
raise ArgumentError, I18n.t('posts.errors.post') unless post.is_a?(Jekyll::Document) 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 @site = site
@post = post @post = post
# los errores tienen que ser un hash para que # los errores tienen que ser un hash para que
# ActiveModel pueda traer los errores normalmente # ActiveModel pueda traer los errores normalmente
@errors = {} @errors = {}
@front_matter = front_matter
# Crea un post nuevo si no especificamos el post # sincronizar los datos del document
new_post unless @post if new?
load_data! unless new? @front_matter = {}
update_attributes front_matter
else
load_front_matter!
merge_with_front_matter! front_matter.stringify_keys
end
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? def new?
!File.exist? @post.path @post.nil? || !File.exist?(@post.try(:path))
end 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 def save
merge_data_with_front_matter! cleanup!
clean_content!
return false unless valid?
new_post if new?
return unless write return unless write
return unless detect_file_rename! return unless detect_file_rename!
# Vuelve a leer el post para tomar los cambios # Vuelve a leer el post para tomar los cambios
@post.read @post.read
add_post_to_site!
true true
end end
alias :save! :save alias :save! :save
def title def title
get_metadata 'title' get_front_matter 'title'
end end
def date def date
get_metadata 'date' get_front_matter 'date'
end end
def tags def tags
get_metadata 'tags' get_front_matter 'tags'
end end
def categories def categories
get_metadata 'categories' get_front_matter 'categories'
end end
alias :category :categories 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 def path
basename_changed? ? File.join(@site.path, '_posts', basename_from_front_matter) : @post.try(:path) basename_changed? ? File.join(@site.path, '_posts', basename_from_front_matter) : @post.try(:path)
end end
# TODO los slugs se pueden repetir, el identificador real sería
# fecha+slug, pero se ve feo en las urls?
def id def id
get_metadata 'slug' get_front_matter 'slug'
end end
alias :slug :id alias :slug :id
alias :to_s :id alias :to_s :id
def basename_changed? def basename_changed?
@post.basename != basename_from_front_matter @post.try(:basename) != basename_from_front_matter
end
def data
@post.data
end end
def content def content
@content ||= @post.content @content ||= @post.try :content
end end
# imita Model.update_attributes de ActiveRecord # imita Model.update_attributes de ActiveRecord
def update_attributes(attrs) def update_attributes(attrs)
attrs.each_pair do |k,i| # el cuerpo se maneja por separado
# el contenido no es metadata @content = attrs.delete('content') if attrs.key? 'content'
if k == 'content' merge_with_front_matter! attrs.stringify_keys
@content = i
else
set_metadata k.to_sym, i
end 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 end
def valid?
reset_errors!
validate
@errors.empty?
end
# Permite ordenar los posts
def <=>(other)
@post <=> other.post
end end
private private
# Genera un post nuevo y lo agrega a la colección del sitio.
def new_post def new_post
opts = { site: @site, collection: @site.jekyll.posts } opts = { site: @site.jekyll, collection: @site.jekyll.posts }
@post = Jekyll::Document.new(path, opts) @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 << @post
@site.jekyll.posts.docs.sort! @site.jekyll.posts.docs.sort!
@post @site.posts << self unless @site.posts.include? self
@site.posts.sort!
end end
# Obtiene metadatos # Obtiene metadatos asegurándose que siempre trabajamos con strings
def get_metadata(name) def get_front_matter(name)
@front_matter.key?(name) ? @front_matter.dig(name) : @post.data[name] @front_matter.dig(name.to_s)
end end
def set_metadata(name, value) # Los define, asegurandose que las llaves siempre son strings, para no
@front_matter[name] = value # tener incompatibilidades con jekyll
def set_front_matter(name, value)
@front_matter[name.to_s] = value
end end
# Cambiar el nombre del archivo si cambió el título o la fecha. # 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. # engañamos eliminando la instancia de @post y recargando otra.
def detect_file_rename! def detect_file_rename!
return true unless basename_changed? 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 Rails.logger.info I18n.t('posts.logger.rm', path: path)
add_error path: I18n.t('posts.errors.path') FileUtils.rm @post.path
return replace_post!
end end
FileUtils.mv @post.path, path # Reemplaza el post en el sitio por uno nuevo
replace_post! path def replace_post!
end @old_post = @site.jekyll.posts.docs.delete @post
def replace_post!(path)
@site.jekyll.posts.docs.delete @post
new_post new_post
end end
@ -142,40 +202,47 @@ class Post
# Obtiene el nombre del archivo a partir de los datos que le # Obtiene el nombre del archivo a partir de los datos que le
# pasemos # pasemos
def basename_from_front_matter def basename_from_front_matter
_date = @front_matter[:date] date = get_front_matter('date').strftime('%F')
date = _date.respond_to?(:strftime) ? _date.strftime('%F') : _date slug = get_front_matter('slug')
title = get_metadata 'slug' || Jekyll::Utils.slugify(@front_matter[:title]) slug =
ext = get_metadata 'ext' || '.markdown' ext = get_front_matter('ext') || '.markdown'
"#{date}-#{title}#{ext}" "#{date}-#{slug}#{ext}"
end end
# Toma los datos del front matter local y los mueve a los datos # 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, # 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 # porque Jekyll trabaja con cadenas. Se excluyen otros datos que no
# van en el frontmatter # van en el frontmatter
def merge_data_with_front_matter! def merge_with_front_matter!(params)
@data.merge! Hash[@front_matter.map do |k, v| @front_matter.merge! Hash[params.to_hash.map do |k, v|
[k.to_s, v] unless REJECT_FROM_DATA.include? k [k, v] unless REJECT_FROM_DATA.include? k
end.compact] end.compact]
end end
# Carga una copia de los datos del post original excluyendo datos # Carga una copia de los datos del post original excluyendo datos
# que no nos interesan # que no nos interesan
def load_data! def load_front_matter!
@data ||= @post.data.reject do |key, _| @front_matter = @post.data.reject do |key, _|
REJECT_FROM_DATA.include? key REJECT_FROM_DATA.include? key
end end
end end
def cleanup!
things_to_arrays!
date_to_time!
clean_content!
slugify_title!
end
# Aplica limpiezas básicas del contenido # Aplica limpiezas básicas del contenido
def clean_content! def clean_content!
@content = @content.delete("\r") @content.try(:delete!, "\r")
end end
# Guarda los cambios en el archivo destino # Guarda los cambios en el archivo destino
def write 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 # Bloquear el archivo para que no sea accedido por otro
# proceso u otra editora # proceso u otra editora
f.flock(File::LOCK_EX) f.flock(File::LOCK_EX)
@ -197,8 +264,14 @@ class Post
false false
end end
# Genera el post con front matter, menos los campos que no necesitamos
# que estén en el front matter
def full_content 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 end
def add_error(hash) def add_error(hash)
@ -212,4 +285,23 @@ class Post
@errors @errors
end 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 end

View file

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

View file

@ -1,10 +1,17 @@
- unless @post.errors.empty? - unless @post.errors.empty?
.alert.alert-danger .alert.alert-danger
%ul %ul
- @post.errors.each do |error| - @post.errors.each do |_,error|
%li= 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 .form-group
= submit_tag t('posts.save'), class: 'btn btn-success' = submit_tag t('posts.save'), class: 'btn btn-success'
.form-group .form-group
@ -12,20 +19,21 @@
placeholder: t('posts.title') placeholder: t('posts.title')
.form-group .form-group
= text_area_tag 'post[content]', @post.content, autofocus: true, = 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 .form-group
= label_tag 'post_date', t('posts.date') = 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' class: 'form-control'
%small.text-muted.form-text= t('posts.date_help') %small.text-muted.form-text= t('posts.date_help')
.form-group .form-group
= label_tag 'post_categories', t('posts.categories') = 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' class: 'form-control'
%small.text-muted.form-text= t('posts.tags_help') %small.text-muted.form-text= t('posts.tags_help')
.form-group .form-group
= label_tag 'post_tags', t('posts.tags') = 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' class: 'form-control'
%small.text-muted.form-text= t('posts.tags_help') %small.text-muted.form-text= t('posts.tags_help')
.form-group .form-group

View file

@ -1,9 +1,3 @@
.container-fluid .row
.row
%h1
= t('posts.editing')
= @post.title
.row
.col .col
= render 'posts/form' = 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 %tbody
- @site.posts.each do |post| - @site.posts.each do |post|
%tr %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 .row
.col
%h1= @post.title %h1= @post.title
.row
.col
= link_to t('posts.edit'), edit_site_post_path(@site, @post), = link_to t('posts.edit'), edit_site_post_path(@site, @post),
class: 'btn btn-info' class: 'btn btn-info'
.row
.col
.content .content
:markdown :markdown
#{@post.content} #{@post.content}
%table.table.table-condensed.table-striped %table.table.table-condensed.table-striped
%tbody %tbody
- @post.data.each do |key, data| - @post.front_matter.each do |key, data|
%tr %tr
%th= key %th= key
%td %td

View file

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

View file

@ -1,4 +1,6 @@
es: es:
errors:
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
login: login:
email: 'Dirección de correo' email: 'Dirección de correo'
password: 'Contraseña' password: 'Contraseña'
@ -17,6 +19,10 @@ es:
categories_help: '¡Separadas por comas!' categories_help: '¡Separadas por comas!'
slug: 'Nombre la URL' slug: 'Nombre la URL'
slug_help: 'Esto es el nombre del artículo en la URL, por ejemplo /título/. Puedes dejarlo vacío.' 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: errors:
path: 'El archivo destino ya existe' path: 'El archivo destino ya existe'
file: 'No se pudo escribir el archivo' 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": "bootstrap-markdown@https://0xacab.org/itacate-kefir/bootstrap-markdown.git":
version "2.10.0" 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: markdown@^0.5.0:
version "0.5.0" version "0.5.0"