WIP: vistas para trabajar con artículos

This commit is contained in:
f 2019-08-13 16:09:23 -03:00
parent f65a7f5fe2
commit 9cb877c5aa
No known key found for this signature in database
GPG key ID: 2AE5A13E321F953D
31 changed files with 321 additions and 401 deletions

View file

@ -226,7 +226,7 @@ GEM
net-ssh (5.2.0) net-ssh (5.2.0)
netaddr (2.0.3) netaddr (2.0.3)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.10.3) nokogiri (1.10.4)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
parallel (1.17.0) parallel (1.17.0)

View file

@ -1,4 +0,0 @@
$(document).on('turbolinks:load', function() {
var md = window.markdownit();
$('#post_content').markdown({ parser: md.render.bind(md) });
});

View file

@ -1,4 +1,5 @@
$(document).on('turbolinks:load', function() { $(document).on('turbolinks:load', function() {
// Previene el envío del formulario al presionar <Enter>
$(document).on('keypress', '.post :input:not(textarea):not([type=submit])', function(e) { $(document).on('keypress', '.post :input:not(textarea):not([type=submit])', function(e) {
if (e.keyCode == 13) { if (e.keyCode == 13) {
e.preventDefault(); e.preventDefault();
@ -6,6 +7,7 @@ $(document).on('turbolinks:load', function() {
} }
}); });
// Al enviar el formulario del artículo, aplicar la validación
$('.submit-post').click(function(e) { $('.submit-post').click(function(e) {
var form = $(this).parents('form.form'); var form = $(this).parents('form.form');
var invalid_help = $('.invalid_help'); var invalid_help = $('.invalid_help');
@ -13,11 +15,22 @@ $(document).on('turbolinks:load', function() {
invalid_help.addClass('d-none'); invalid_help.addClass('d-none');
sending_help.addClass('d-none'); sending_help.addClass('d-none');
form.find('[aria-invalid="true"]')
.attr('aria-invalid', false)
.attr('aria-describedby', function() {
return $(this).siblings('.feedback').attr('id');
});
if (form[0].checkValidity() === false) { if (form[0].checkValidity() === false) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
invalid_help.removeClass('d-none'); invalid_help.removeClass('d-none');
form.find(':invalid')
.attr('aria-invalid', true)
.attr('aria-describedby', function() {
return $(this).siblings('.invalid-feedback').attr('id');
});
} else { } else {
sending_help.removeClass('d-none'); sending_help.removeClass('d-none');
} }

View file

@ -1,3 +1,9 @@
// TODO: Encontrar la forma de generar esto desde los locales de Rails
$custom-file-text: (
en: 'Browse',
es: 'Buscar archivo'
);
@import "bootstrap"; @import "bootstrap";
@import "bootstrap-markdown/css/bootstrap-markdown.min"; @import "bootstrap-markdown/css/bootstrap-markdown.min";
@import "font-awesome"; @import "font-awesome";

View file

@ -7,11 +7,12 @@ class PostsController < ApplicationController
def index def index
authorize Post authorize Post
@site = find_site @site = find_site
@lang = find_lang(@site) # TODO: por qué no lo está leyendo @site.posts?
@site.read
@category = session[:category] = params.dig(:category) @category = session[:category] = params.dig(:category)
@posts = policy_scope(@site.posts_for(@lang), # TODO: Aplicar policy_scope
policy_scope_class: PostPolicy::Scope) @posts = @site.posts(lang: I18n.locale)
if params[:sort_by].present? if params[:sort_by].present?
begin begin
@ -33,12 +34,8 @@ class PostsController < ApplicationController
def new def new
authorize Post authorize Post
@site = find_site @site = find_site
@lang = find_lang(@site) # TODO: Implementar layout
@template = find_template(@site) @post = @site.posts.build(lang: I18n.locale)
@post = Post.new(site: @site,
front_matter: { date: Time.now },
lang: @lang,
template: @template)
end end
def create def create

View file

@ -2,26 +2,23 @@
# Helpers # Helpers
module ApplicationHelper module ApplicationHelper
# Devuelve el atributo name de un campo posiblemente anidado # Devuelve el atributo name de un campo anidado en el formato que
def field_name_for_post(names) # esperan los helpers *_field
return ['post', names] if names.is_a? String #
# [ 'post', :image, :description ]
names = names.dup # [ 'post[image]', :description ]
root = 'post' # 'post[image][description]'
def field_name_for(*names)
name = names.pop name = names.pop
root = names.shift
names.each do |n| names.each do |n|
root = "#{root}[#{n}]" root += "[#{n}]"
end end
[root, name] [root, name]
end end
def field_name_for_post_as_string(names)
f = field_name_for_post(names)
"#{f.first}[#{f.last}]"
end
def distance_of_time_in_words_if_more_than_a_minute(seconds) def distance_of_time_in_words_if_more_than_a_minute(seconds)
if seconds > 60 if seconds > 60
distance_of_time_in_words seconds distance_of_time_in_words seconds
@ -49,4 +46,53 @@ module ApplicationHelper
def form_class(model) def form_class(model)
model.errors.messages.empty? ? 'needs-validation' : 'was-validated' model.errors.messages.empty? ? 'needs-validation' : 'was-validated'
end end
# Opciones por defecto para el campo de un formulario
def field_options(attribute, metadata)
{
class: 'form-control',
required: metadata.required,
aria: {
describedby: id_for_help(attribute),
required: metadata.required
}
}
end
# Devuelve la clase is-invalid si el campo tiene un error
def invalid(post, attribute)
'is-invalid' if post.errors[attribute].present?
end
# Busca la traducción de una etiqueta en los metadatos de un post
def post_label_t(*attribute, post:)
label = post_t(*attribute, post: post, type: :label)
if post.send(attribute.first).required
label += I18n.t('posts.attributes.required.label')
end
label
end
def post_help_t(*attribute, post:)
post_t(*attribute, post: post, type: :help)
end
def id_for_help(*attribute)
"#{attribute.join('-')}-help"
end
def id_for_feedback(*attribute)
"#{attribute.join('-')}-feedback"
end
private
def post_t(*attribute, post:, type:)
post.layout.metadata.dig(*attribute, type.to_s, I18n.locale.to_s) ||
post.layout.metadata.dig(*attribute,
type.to_s, I18n.default_locale.to_s) ||
I18n.t("posts.attributes.#{attribute.join('.')}.#{type}")
end
end end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
# Se encarga del contenido del artículo y quizás otros campos que
# requieran texto largo.
class MetadataContent < MetadataTemplate
include ActionView::Helpers::SanitizeHelper
def default_value
''
end
def value
sanitize(self[:value] || document.content || default_value,
sanitize_options)
end
private
# Etiquetas y atributos HTML a permitir
#
# No queremos permitir mucho más que cosas de las que nos falten en
# CommonMark.
#
# TODO: Permitir una lista de atributos y etiquetas en el Layout
#
# XXX: Vamos a generar un reproductor de video/audio directamente
# desde un plugin de Jekyll
def sanitize_options
{
tags: %w[span],
attributes: %w[title class lang]
}
end
end

View file

@ -1,3 +1,4 @@
class MetadataDate < MetadataTemplate # frozen_string_literal: true
class MetadataDate < MetadataTemplate
end end

View file

@ -4,7 +4,7 @@
class MetadataImage < MetadataTemplate class MetadataImage < MetadataTemplate
# Una ruta vacía a la imagen con una descripción vacía # Una ruta vacía a la imagen con una descripción vacía
def default_value def default_value
{ path: '', description: '' } { 'path' => '', 'description' => '' }
end end
def empty? def empty?

View file

@ -17,6 +17,10 @@ class MetadataPath < MetadataTemplate
Pathname.new(value).relative_path_from(Pathname.new(site.path)).to_s Pathname.new(value).relative_path_from(Pathname.new(site.path)).to_s
end end
def basename
File.basename(value, ext)
end
private private
def ext def ext

View file

@ -6,4 +6,8 @@ class MetadataString < MetadataTemplate
def default_value def default_value
'' ''
end end
def value
super.strip
end
end end

View file

@ -10,10 +10,10 @@ require 'jekyll/utils'
# rubocop:disable Style/MissingRespondToMissing # rubocop:disable Style/MissingRespondToMissing
class Post < OpenStruct class Post < OpenStruct
# Atributos por defecto # Atributos por defecto
# XXX: Volver document opcional cuando estemos creando
DEFAULT_ATTRIBUTES = %i[site document layout].freeze DEFAULT_ATTRIBUTES = %i[site document layout].freeze
# Otros atributos que no vienen en los metadatos # Otros atributos que no vienen en los metadatos
ATTRIBUTES = %i[content lang path date slug attributes errors].freeze PRIVATE_ATTRIBUTES = %i[lang path slug attributes errors].freeze
PUBLIC_ATTRIBUTES = %i[date].freeze
# Redefinir el inicializador de OpenStruct # Redefinir el inicializador de OpenStruct
# #
@ -27,15 +27,8 @@ class Post < OpenStruct
super(args) super(args)
# Genera un método con todos los atributos disponibles # Genera un método con todos los atributos disponibles
self.attributes = DEFAULT_ATTRIBUTES + self.attributes = layout.metadata.keys.map(&:to_sym) + PUBLIC_ATTRIBUTES
ATTRIBUTES + self.errors = {}
layout.metadata.keys.map(&:to_sym)
# El contenido
# TODO: Mover a su propia clase para poder hacer limpiezas
# independientemente
self.content = document.content
self.errors = {}
# Genera un atributo por cada uno de los campos de la plantilla, # Genera un atributo por cada uno de los campos de la plantilla,
# MetadataFactory devuelve un tipo de campo por cada campo. A # MetadataFactory devuelve un tipo de campo por cada campo. A
@ -63,6 +56,10 @@ class Post < OpenStruct
end end
# rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/AbcSize
def id
path.basename
end
# Levanta un error si al construir el artículo no pasamos un atributo. # Levanta un error si al construir el artículo no pasamos un atributo.
def default_attributes_missing(**args) def default_attributes_missing(**args)
DEFAULT_ATTRIBUTES.each do |attr| DEFAULT_ATTRIBUTES.each do |attr|
@ -100,10 +97,11 @@ class Post < OpenStruct
# Detecta si es un atributo válido o no, a partir de la tabla de la # Detecta si es un atributo válido o no, a partir de la tabla de la
# plantilla # plantilla
def attribute?(mid) def attribute?(mid)
attrs = DEFAULT_ATTRIBUTES + PRIVATE_ATTRIBUTES + PUBLIC_ATTRIBUTES
if singleton_class.method_defined? :attributes if singleton_class.method_defined? :attributes
attributes.include? attribute_name(mid) (attrs + attributes).include? attribute_name(mid)
else else
(DEFAULT_ATTRIBUTES + ATTRIBUTES).include? attribute_name(mid) attrs.include? attribute_name(mid)
end end
end end
@ -119,7 +117,7 @@ class Post < OpenStruct
# 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}" "#{yaml.to_yaml}---\n\n#{content.value}"
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

View file

@ -32,10 +32,10 @@ class PostRelation < Array
private private
def build_layout(layout = :post) def build_layout(layout = nil)
return layout if layout.is_a? Layout return layout if layout.is_a? Layout
site.layouts[layout] site.layouts[layout || :post]
end end
# Devuelve una colección Jekyll que hace pasar el documento # Devuelve una colección Jekyll que hace pasar el documento

View file

@ -162,7 +162,7 @@ class Site < ApplicationRecord
@layouts ||= data.fetch('layouts', {}).map do |name, metadata| @layouts ||= data.fetch('layouts', {}).map do |name, metadata|
{ name.to_sym => Layout.new(site: self, { name.to_sym => Layout.new(site: self,
name: name.to_sym, name: name.to_sym,
metadata: metadata) } metadata: metadata.with_indifferent_access) }
end.inject(:merge) end.inject(:merge)
end end

View file

@ -57,12 +57,14 @@ class PostPolicy
# Las usuarias pueden ver todos los posts # Las usuarias pueden ver todos los posts
# #
# Les invitades solo pueden ver sus propios posts # Les invitades solo pueden ver sus propios posts
#
# TODO: Arreglar
def resolve def resolve
return scope if scope.try(:first).try(:site).try(:usuarie?, usuarie) return scope if scope.try(:first).try(:site).try(:usuarie?, usuarie)
# Asegurarse que al menos devolvemos [] # Asegurarse que al menos devolvemos []
[scope.find do |post| [scope.find do |post|
post.author == usuarie.email post.author.value == usuarie.email
end].flatten.compact end].flatten.compact
end end
end end

View file

@ -1,5 +1,5 @@
!!! !!!
%html %html{ lang: I18n.locale, dir: t('dir') }
%head %head
%meta{ content: 'text/html; charset=UTF-8', %meta{ content: 'text/html; charset=UTF-8',
'http-equiv': 'Content-Type' }/ 'http-equiv': 'Content-Type' }/
@ -11,10 +11,7 @@
= javascript_include_tag 'application', = javascript_include_tag 'application',
'data-turbolinks-track': 'reload' 'data-turbolinks-track': 'reload'
- if @site.try(:persisted?) && @site.try(:config).try(:dig, 'css') -# TODO: Reimplementar get_url_from_site
%link{ rel: 'stylesheet',
type: 'text/css',
href: @site.get_url_from_site(@site.config.dig('css')) }
- style = "background-image: url(#{@site.try(:cover)})" - style = "background-image: url(#{@site.try(:cover)})"
-# haml-lint:disable InlineStyles -# haml-lint:disable InlineStyles

View file

@ -0,0 +1,5 @@
%small.feedback.form-text.text-muted{ id: id_for_help(*attribute) }
= post_help_t(*attribute, post: post)
- if metadata.required
.invalid-feedback{ id: id_for_feedback(*attribute) }
= t('posts.attributes.required.feedback')

View file

@ -1,144 +1,34 @@
- unless @post.errors.empty? - unless post.errors.empty?
.alert.alert-danger .alert.alert-danger
%ul %ul
- @post.errors.each do |key, error| - post.errors.each do |key, error|
%li %li
%strong= @post.template_fields.find { |tf| tf.key == key.to_s }.try(:label) || key %strong
= key.capitalize
= [error].flatten.join("\n") = [error].flatten.join("\n")
-# TODO seleccionar la dirección por defecto según el idioma actual -# TODO: habilitar form_for
- direction = @post.get_front_matter('dir') || 'ltr' :ruby
-# string para configurar la clase con direccion de texto if post.new?
- field_class = "form-control #{direction}" url = site_posts_path(site)
-# TODO habilitar form_for method = :post
- if @post.new? else
- url = site_posts_path(@site, lang: @lang) url = site_post_path(site, post)
- method = :post method = :patch
- else end
- url = site_post_path(@site, @post, lang: @lang)
- method = :patch
- if pre = @post.template.try(:get_front_matter, 'pre')
= render 'layouts/help', help: CommonMarker.render_doc(pre).to_html
= form_tag url,
method: method,
class: "form post #{@invalid ? 'was-validated' : ''}",
novalidate: true,
multipart: true do
= hidden_field_tag 'template', params[:template] -# Comienza el formulario
.form-group = form_tag url, method: method, class: 'form post', multipart: true do
= submit_tag t('posts.save'), class: 'btn btn-success submit-post'
= submit_tag t('posts.save_incomplete'), class: 'btn btn-info submit-post-incomplete', name: 'commit_incomplete' -# Botones de guardado
.invalid_help.alert.alert-danger.d-none= @site.config.dig('invalid_help') || t('posts.invalid_help') = render 'posts/submit', site: site
.sending_help.alert.alert-success.d-none= @site.config.dig('sending_help') || t('posts.sending_help')
- if @site.usuarie? current_user -# Dibuja cada atributo
.form-group - post.attributes.each do |attribute|
= label_tag 'post_author', t('posts.author') - type = post.send(attribute).type
- todxs = (@site.usuaries + @site.invitades).compact.uniq.map(&:email) = render "posts/attributes/#{type}",
= select_tag 'post[author]', post: post, attribute: attribute,
options_for_select(todxs, @post.author || current_user.email), metadata: post.send(attribute)
{ class: 'form-control select2',
data: { tags: true, -# Botones de guardado
placeholder: t('posts.select.placeholder'), = render 'posts/submit', site: site
'allow-clear': true } }
%small.text-muted.form-text= t('posts.author_help')
- if @post.has_field? :dir
.form-group
= label_tag 'post_dir', t('posts.dir')
= select_tag 'post[dir]',
options_for_select([[t('posts.ltr'), 'ltr'], [t('posts.rtl'), 'rtl']], direction),
{ class: 'form-control' }
%small.text-muted.form-text= t('posts.dir_help')
- if @post.has_field? :title
.form-group
= label_tag 'post_title', t('posts.title')
= text_field 'post', 'title', value: @post.title, class: field_class, required: true
- if @post.content?
.form-group{class: direction}
= label_tag 'post_content', t('posts.content')
= render 'layouts/help', help: [ t('help.markdown.intro'),
t('help.distraction_free_html'),
t('help.preview_html') ]
= text_area_tag 'post[content]', @post.content,
class: 'post-content'
- if @post.has_field? :date
.form-group
= label_tag 'post_date', t('posts.date')
= date_field 'post', 'date', value: @post.date.try(:strftime, '%F'),
class: 'form-control'
%small.text-muted.form-text= t('posts.date_help')
= render 'layouts/help', help: t('help.autocomplete_html')
- if @post.has_field? :categories
.form-group
= label_tag 'post_categories', t('posts.categories')
= select_tag 'post[categories][]',
options_for_select(@site.categories(lang: @lang), @post.categories),
{ class: 'form-control select2', multiple: 'multiple',
data: { tags: true,
placeholder: t('posts.select.placeholder'),
'allow-clear': true } }
- if @post.has_field? :tags
.form-group
= label_tag 'post_tags', t('posts.tags')
= select_tag 'post[tags][]',
options_for_select(@site.tags(lang: @lang), @post.tags),
{ class: 'form-control select2', multiple: 'multiple',
data: { tags: true,
placeholder: t('posts.select.placeholder'),
'allow-clear': true } }
- if @post.has_field? :slug
.form-group
= label_tag 'post_slug', t('posts.slug')
= text_field 'post', 'slug', value: @post.slug,
class: 'form-control'
%small.text-muted.form-text= t('posts.slug_help')
- if @post.has_field? :permalink
.form-group
= label_tag 'post_permalink', t('posts.permalink')
= text_field 'post', 'permalink', value: @post.get_front_matter('permalink'),
class: 'form-control'
%small.text-muted.form-text= t('posts.permalink_help')
- if @post.has_field? :layout
.form-group
= label_tag 'post_layout', t('posts.layout')
= select_tag 'post[layout]',
options_for_select(@site.layouts, @post.get_front_matter('layout')),
{ class: 'form-control select2' }
%small.text-muted.form-text= t('posts.layout_help')
- if @site.i18n?
- @site.translations.each do |lang|
- next if lang == @lang
.form-group
= label_tag 'post_lang', t("posts.lang.#{lang}")
= select_tag "post[lang][#{lang}]",
options_for_select(@site.posts_for(lang).map { |p| [p.title, p.id] },
@post.get_front_matter('lang').try(:dig, lang)),
{ class: 'form-control select2' }
%small.text-muted.form-text= t('posts.lang_help')
-# Genera todos los campos de la plantilla
- @post.template_fields.each do |template|
- next unless type = template.type
- if template.title.present?
%h1{id: template.title.tr(' ', '_').titleize}= template.title
- if template.subtitle.present?
%p= template.subtitle
- value = @post.new? ? template.values : @post.get_front_matter(template.key)
.form-group
= label_tag "post_#{template}", id: template do
= link_to '#' + template.key, class: 'text-muted',
data: { turbolinks: 'false' } do
= fa_icon 'link', title: t('posts.anchor')
- if template.private?
= fa_icon 'lock', title: t('posts.private')
= sanitize_markdown template.label, tags: %w[a]
- if template.help
%small.text-muted.form-text= template.help
= render "posts/template_field/#{type}", template: template, name: template.key, value: value
.invalid-feedback= t('posts.invalid')
.form-group
= submit_tag t('posts.save'), class: 'btn btn-success submit-post'
= submit_tag t('posts.save_incomplete'), class: 'btn btn-info submit-post-incomplete', name: 'commit_incomplete'
.invalid_help.alert.alert-danger.d-none= @site.config.dig('invalid_help') || t('posts.invalid_help')
.sending_help.alert.alert-success.d-none= @site.config.dig('sending_help') || t('posts.sending_help')
- if post = @post.template.try(:get_front_matter, 'post')
= render 'layouts/help', help: CommonMarker.render_doc(post).to_html

View file

@ -0,0 +1,9 @@
.form-group
= submit_tag t('.save'), class: 'btn btn-success submit-post'
= submit_tag t('.save_incomplete'),
class: 'btn btn-info submit-post-incomplete',
name: 'commit_incomplete'
.invalid_help.alert.alert-danger.d-none
= site.config.fetch('invalid_help', t('.invalid_help'))
.sending_help.alert.alert-success.d-none
= site.config.fetch('sending_help', t('.sending_help'))

View file

@ -0,0 +1,7 @@
-# TODO: Convertir a select2 o nuestro reemplazo
.form-group{ class: invalid(post, attribute) }
= label_tag "post_#{attribute}", post_label_t(attribute, post: post)
= text_field 'post', attribute, value: metadata.value.join(', '),
**field_options(attribute, metadata)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -0,0 +1,6 @@
.form-group{ class: invalid(post, attribute) }
= label_tag "post_#{attribute}", post_label_t(attribute, post: post)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata
= text_area_tag "post[#{attribute}]", metadata.value,
**field_options(attribute, metadata)

View file

@ -0,0 +1,6 @@
.form-group{ class: invalid(post, attribute) }
= label_tag "post_#{attribute}", post_label_t(attribute, post: post)
= date_field 'post', attribute, value: metadata.value.strftime('%F'),
**field_options(attribute, metadata)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -0,0 +1,20 @@
.form-group{ class: invalid(post, attribute) }
- if metadata.value['path'].present?
= image_tag metadata.value[:path], alt: metadata.value['description']
.custom-file
= file_field(*field_name_for('post', attribute, :path),
**field_options(attribute, metadata), class: 'custom-file-input')
= label_tag "post_#{attribute}_path",
post_label_t(attribute, :path, post: post), class: 'custom-file-label'
= render 'posts/attribute_feedback',
post: post, attribute: [attribute, :path], metadata: metadata
.form-group{ class: invalid(post, attribute) }
= label_tag "post_#{attribute}_description",
post_label_t(attribute, :description, post: post)
= text_field(*field_name_for('post', attribute, :description),
value: metadata.value['description'],
**field_options(attribute, metadata))
= render 'posts/attribute_feedback',
post: post, attribute: [attribute, :description], metadata: metadata

View file

@ -0,0 +1,6 @@
.form-group{ class: invalid(post, attribute) }
= label_tag "post_#{attribute}", post_label_t(attribute, post: post)
= text_field 'post', attribute, value: metadata.value,
**field_options(attribute, metadata)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -0,0 +1,6 @@
.form-group{ class: invalid(post, attribute) }
= label_tag "post_#{attribute}", post_label_t(attribute, post: post)
= text_field 'post', attribute, value: metadata.value,
**field_options(attribute, metadata)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata

View file

@ -1,44 +1,21 @@
.row .row
.col .col
= render 'layouts/breadcrumb', = render 'layouts/breadcrumb',
crumbs: [ link_to(t('sites.index'), sites_path), @site.name, link_to(t('posts.index'), site_posts_path(@site)), @category ] crumbs: [link_to(t('sites.index'), sites_path),
@site.name,
link_to(t('posts.index'),
site_posts_path(@site)),
@category]
= render 'layouts/help', help: t('help.breadcrumbs') = render 'layouts/help', help: t('help.breadcrumbs')
.row .row
.col .col
%h1= @site.config.fetch('title', @site.name_with_i18n(@lang)) %h1= @site.title
.row .row
.col .col
.btn-group .btn-group
- if @site.templates.empty? = link_to t('posts.new'), new_site_post_path(@site),
= link_to t('posts.new'), new_site_post_path(@site, lang: @lang), class: 'btn btn-success'
class: 'btn btn-success'
- else
= link_to t('posts.new_with_template', template: @site.templates.first.id.humanize),
new_site_post_path(@site, lang: @lang, template: @site.templates.first.id),
class: 'btn btn-success'
- if @site.usuarie? current_usuarie
%button.btn.btn-success.dropdown-toggle.dropdown-toggle-split{data: { toggle: 'split' },
aria: { haspopup: 'true', expanded: 'false' }}
%span.sr-only= t('posts.dropdown')
.dropdown-menu
- @site.templates.each do |template|
= link_to template.id.humanize,
new_site_post_path(@site, lang: @lang, template: template.id),
class: 'dropdown-item'
- @site.translations.each do |l|
= link_to t("i18n.#{l}"), site_posts_path(@site, category: @category, lang: l),
class: 'btn btn-info'
.btn-group.pull-right
= link_to t('posts.categories'), site_posts_path(@site, lang: @lang), class: 'btn btn-secondary'
%button.btn.btn-secondary.dropdown-toggle.dropdown-toggle-split{data: { toggle: 'split' },
aria: { haspopup: 'true', expanded: 'false' }}
%span.sr-only= t('posts.dropdown')
.dropdown-menu
- @site.categories.each do |c|
= link_to c.split(':').first,
site_posts_path(@site, lang: @lang, category: c),
class: (params[:category] == c) ? 'dropdown-item active' : 'dropdown-item'
.row .row
.col .col
@ -54,80 +31,35 @@
category: @category, category: @category,
lang: @lang, lang: @lang,
sort_by: s) sort_by: s)
= form_tag site_reorder_posts_path, method: :post do %table.table.table-condensed.table-striped
= hidden_field 'posts', 'lang', value: @lang %tbody
- if policy(@site).reorder_posts? - @posts.each do |post|
- if @site.ordered? @lang -#
.reorder-posts-panel.alert.alert-info.alert-dismissible.fade.show{role: 'alert'} saltearse el post a menos que esté en la categoría por
= raw t('help.posts.reorder') la que estamos filtrando
%br - if @category
= submit_tag t('posts.reorder_posts'), class: 'btn btn-success' - next unless post.categories.value.include?(@category)
%button.close{type: 'button', %tr
'aria-label': t('help.close') }
%span{'aria-hidden': true} &times;
- else
.alert.alert-danger.alert-dismissible.fade.show{role: 'alert'}
= raw t('errors.posts.disordered')
%br
= hidden_field 'posts', 'force', value: true
= submit_tag t('errors.posts.disordered_button'), class: 'btn btn-danger'
%button.close{type: 'button',
data: { dismiss: 'alert' },
'aria-label': t('help.close') }
%span{'aria-hidden': true} &times;
%table.table.table-condensed.table-striped{class: (@site.ordered? @lang) ? 'table-draggable' : ''}
%tbody
- @posts.each_with_index do |post, i|
- if @category
-# saltearse el post a menos que esté en la categoría
-# por la que estamos filtrando
- next unless post.categories.include?(@category)
-# establecer la direccion del texto
- direction = post.get_front_matter(:dir)
%tr
- if policy(@site).reorder_posts? && @site.ordered?(@lang)
%td %td
= fa_icon 'arrows-v', class: 'handle' = link_to post.title.value,
= hidden_field 'posts[order]', i, value: post.order, class: 'post_order' site_post_path(@site, post.id)
%small - unless post.categories.value.empty?
%br %br
%span.order.is= post.order %small
%span.order.was.d-none{data: { order: post.order }}= "(#{post.order})" - post.categories.value.each do |c|
= link_to c, site_posts_path(@site, category: c)
%td{class: direction} %td= post.date.value.strftime('%F')
= link_to post.title, site_post_path(@site, post, lang: @lang) %td
- unless post.categories.empty? - if policy(post).edit?
%br = link_to t('posts.edit'),
%small edit_site_post_path(@site, post.id),
- post.categories.each do |c| class: 'btn btn-info'
= link_to c, site_posts_path(@site, category: c, lang: @lang), - if policy(post).destroy?
data: { toggle: 'tooltip' }, title: t('help.category') = link_to t('posts.destroy'),
- if post.draft? || post.incomplete? site_post_path(@site, post.id),
%br class: 'btn btn-danger',
- if post.draft? method: :delete,
%span.badge.badge-info= t('posts.draft') data: { confirm: t('posts.confirm_destroy') }
- if post.incomplete?
%span.badge.badge-warning= t('posts.incomplete')
%td
- if post.translations
%small
- post.translations.each do |pt|
= link_to pt.title, site_post_path(@site, pt, lang: pt.lang),
data: { toggle: 'tooltip' }, title: t("i18n.#{pt.lang}")
%br
%td= post.date.strftime('%F')
%td
- if policy(post).edit?
= link_to t('posts.edit'),
edit_site_post_path(@site, post, lang: @lang),
class: 'btn btn-info'
- if policy(post).destroy?
= link_to t('posts.destroy'),
site_post_path(@site, post, lang: @lang),
class: 'btn btn-danger',
method: :delete,
data: { confirm: t('posts.confirm_destroy') }
- else - else
%h2= t('posts.none') %h2= t('posts.none')

View file

@ -1,6 +1,11 @@
.row .row
.col .col
= render 'layouts/breadcrumb', crumbs: [ link_to(t('sites.index'), sites_path), @site.name, link_to(t('posts.index'), site_posts_path(@site)), t('posts.new') ] = render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index'), sites_path),
@site.name,
link_to(t('posts.index'),
site_posts_path(@site)), t('posts.new')]
.row.justify-content-center .row.justify-content-center
.col-md-8 .col-md-8
= render 'posts/form' = render 'posts/form', site: @site, post: @post

View file

@ -1,4 +1,5 @@
en: en:
dir: ltr
site_service: site_service:
create: 'Created %{name}' create: 'Created %{name}'
update: 'Updated %{name}' update: 'Updated %{name}'
@ -53,6 +54,8 @@ en:
lang: 'Main language' lang: 'Main language'
site: site:
name: 'Name' name: 'Name'
title: 'Title'
description: 'Description'
errors: errors:
models: models:
site: site:
@ -297,6 +300,17 @@ en:
en: 'English' en: 'English'
ar: 'Arabic' ar: 'Arabic'
posts: posts:
submit:
save: 'Save'
save_incomplete: 'Save as draft'
invalid_help: 'Some fields need attention! Please search for the fields marked as invalid.'
attributes:
date:
label: Date
help: Publication date for this post. If you use a date in the future the post won't be published until then.
required:
label: ' (required)'
feedback: 'This field cannot be empty!'
reorder_posts: 'Reorder posts' reorder_posts: 'Reorder posts'
sort: sort:
by: 'Sort by' by: 'Sort by'
@ -310,59 +324,8 @@ en:
categories: 'Everything' categories: 'Everything'
index: 'Posts' index: 'Posts'
edit: 'Edit' edit: 'Edit'
save: 'Send'
save_incomplete: 'Save for later'
draft: revision draft: revision
incomplete: draft incomplete: draft
author: 'Author'
author_help: 'You can change the authorship of the post. If your site accepts guests, changing the authorship to an e-mail address will allow them to edit the post.'
date: 'Publication date'
date_help: 'This changes the articles order!'
title: 'Title'
tags: 'Tags'
tags_help: 'Comma separated!'
tags: 'Tags'
slug: 'Slug'
slug_help: 'This is the name of the article on the URL, ie. /title/. You can leave it empty. If you changed the title and you want to change the file name, empty this field.'
cover: 'Cover'
cover_help: 'Path to the cover'
layout: 'Layout'
layout_help: 'The layout of this post'
objetivos: 'Objectives'
objetivos_help: 'Objectives of this session'
permalink: 'Permanent link'
permalink_help: "If you want to access the post from a specific URL, use this field. Don't forget to start with a /"
recomendaciones: 'Recommendations'
recomendaciones_help: 'Recommendations for this session'
duracion: 'Duration'
duracion_help: "How long does the session take to finish?"
habilidades: 'Skill level'
habilidades_help: 'Skills required for this session'
formato: 'Format'
formato_help: 'Format of this session'
conocimientos: 'Required knowledge'
conocimientos_help: 'Select all required knowledge for this session'
sesiones_ejercicios_relacionados: 'Related sessions/exercises'
sesiones_ejercicios_relacionados_help: 'Select all related sessions/exercises'
materiales_requeridos: 'Needed materials'
materiales_requeridos_help: 'Select all materials needed for this session'
lang:
es: 'Castillian Spanish'
en: 'English'
ar: 'Arabic'
lang_help: 'The same article in another language.'
rtl: 'Right to left'
ltr: 'Left to right'
dir: 'Text direction'
dir_help: 'The reading direction of the language'
logger:
rm: 'Removed %{path}'
errors:
path: 'File already exist'
file: "Couldn't write the file"
title: 'Post needs a title'
date: 'Post needs a valid date'
slug_with_path: "The slug is the short name for the article, as shown in the URL. It can't contain \"/\" ;)"
invalid: 'This field is required!' invalid: 'This field is required!'
open: 'Tip: You can add new options by typing them and pressing Enter' open: 'Tip: You can add new options by typing them and pressing Enter'
private: '&#128274; The values of this field will remain private' private: '&#128274; The values of this field will remain private'

View file

@ -1,4 +1,5 @@
es: es:
dir: ltr
site_service: site_service:
create: 'Creado %{name}' create: 'Creado %{name}'
update: 'Actualizado %{name}' update: 'Actualizado %{name}'
@ -312,6 +313,17 @@ es:
en: 'inglés' en: 'inglés'
ar: 'árabe' ar: 'árabe'
posts: posts:
submit:
save: 'Guardar'
save_incomplete: 'Guardar como borrador'
invalid_help: '¡Te faltan completar algunos campos! Busca los que estén marcados como inválidos'
attributes:
date:
label: Fecha
help: La fecha de publicación del artículo. Si colocas una fecha en el futuro no se publicará hasta ese día.
required:
label: ' (requerido)'
feedback: '¡Este campo no puede estar vacío!'
reorder_posts: 'Reordenar artículos' reorder_posts: 'Reordenar artículos'
sort: sort:
by: 'Ordenar por' by: 'Ordenar por'
@ -327,60 +339,6 @@ es:
edit: 'Editar' edit: 'Editar'
draft: en revisión draft: en revisión
incomplete: borrador incomplete: borrador
save: 'Enviar'
save_incomplete: 'Guardar para después'
author: 'Autorx'
author_help: 'Puedes cambiar la autoría del artículo aquí. Si el sitio acepta invitadxs, poner la dirección de correo de alguien aquí le permite editarlo.'
date: 'Fecha de publicación'
date_help: '¡Esto cambia el orden de los artículos!'
title: 'Título'
categories: 'Categorías'
tags: 'Etiquetas'
slug: 'Nombre la URL'
slug_help: 'Esto es el nombre del artículo en la URL, por ejemplo
/título/. Puedes dejarlo vacío. Si cambiaste el título y quieres
que la URL cambie, borra el contenido de este campo.'
cover: 'Portada'
cover_help: 'La dirección de la portada'
layout: 'Plantilla'
layout_help: 'El tipo de plantilla'
objetivos: 'Objetivos'
objetivos_help: 'Objetivos de esta sesión'
permalink: 'Dirección del enlace'
permalink_help: 'Si quieres que el artículo se acceda desde esta URL completa aquí, no te olvides de empezar con /'
habilidades: 'Habilidades'
habilidades_help: 'Habilidades requeridas para esta sesión'
formato: 'Formato'
formato_help: 'Formato de esta sesión'
conocimientos: 'Conocimientos'
conocimientos_help: 'Elige todos los conocimientos requeridos para abordar esta sesión'
sesiones_ejercicios_relacionados: 'Sesiones y ejercicios relacionados'
sesiones_ejercicios_relacionados_help: 'Elige todas las sesiones relacionadas con esta'
materiales_requeridos: 'Materiales requeridos'
materiales_requeridos_help: 'Materiales necesarios para esta sesión'
recomendaciones: 'Recomendaciones'
recomendaciones_help: 'Recomendaciones para esta sesión'
duracion: 'Duración'
duracion_help: '¿Cuánto dura la sesión?'
lang:
es: 'Artículo en castellano'
en: 'Artículo en inglés'
ar: 'Artículo en árabe'
lang_help: 'El mismo artículo en otro idioma'
rtl: 'Derecha a izquierda'
ltr: 'Izquierda a derecha'
dir: 'Dirección del texto'
dir_help: 'La dirección de lectura del idioma'
dir_help: 'Cambiar la dirección del texto, por ej. el árabe se lee de derecha a izquierda'
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'
slug_with_path: 'El slug es el nombre corto del artículo, tal como figura en la URL. No puede contener "/" ;)'
invalid: '¡Este campo es obligatorio!'
open: 'Nota: Puedes agregar más opciones a medida que las escribes y presionas Entrar' open: 'Nota: Puedes agregar más opciones a medida que las escribes y presionas Entrar'
private: '&#128274; Los valores de este campo serán privados' private: '&#128274; Los valores de este campo serán privados'
select: select:

View file

@ -98,3 +98,11 @@ Al instanciar un `Post`, se pasan el sitio y la plantilla por defecto.
utilizada) utilizada)
* Reimplementar orden de artículos (ver doc) * Reimplementar orden de artículos (ver doc)
* Convertir layout a params * 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í

View file

@ -162,6 +162,7 @@ class PostTest < ActiveSupport::TestCase
test 'se pueden crear nuevos' do test 'se pueden crear nuevos' do
post = @site.posts.build(layout: :post) post = @site.posts.build(layout: :post)
post.title.value = 'test' post.title.value = 'test'
post.content.value = 'test'
assert post.new? assert post.new?
assert post.save assert post.save