mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-14 06:51:42 +00:00
WIP: vistas para trabajar con artículos
This commit is contained in:
parent
f65a7f5fe2
commit
9cb877c5aa
31 changed files with 321 additions and 401 deletions
|
@ -226,7 +226,7 @@ GEM
|
|||
net-ssh (5.2.0)
|
||||
netaddr (2.0.3)
|
||||
nio4r (2.3.1)
|
||||
nokogiri (1.10.3)
|
||||
nokogiri (1.10.4)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.17.0)
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
$(document).on('turbolinks:load', function() {
|
||||
var md = window.markdownit();
|
||||
$('#post_content').markdown({ parser: md.render.bind(md) });
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
$(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) {
|
||||
if (e.keyCode == 13) {
|
||||
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) {
|
||||
var form = $(this).parents('form.form');
|
||||
var invalid_help = $('.invalid_help');
|
||||
|
@ -13,11 +15,22 @@ $(document).on('turbolinks:load', function() {
|
|||
|
||||
invalid_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) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
invalid_help.removeClass('d-none');
|
||||
|
||||
form.find(':invalid')
|
||||
.attr('aria-invalid', true)
|
||||
.attr('aria-describedby', function() {
|
||||
return $(this).siblings('.invalid-feedback').attr('id');
|
||||
});
|
||||
} else {
|
||||
sending_help.removeClass('d-none');
|
||||
}
|
||||
|
|
|
@ -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-markdown/css/bootstrap-markdown.min";
|
||||
@import "font-awesome";
|
||||
|
|
|
@ -7,11 +7,12 @@ class PostsController < ApplicationController
|
|||
|
||||
def index
|
||||
authorize Post
|
||||
@site = find_site
|
||||
@lang = find_lang(@site)
|
||||
@site = find_site
|
||||
# TODO: por qué no lo está leyendo @site.posts?
|
||||
@site.read
|
||||
@category = session[:category] = params.dig(:category)
|
||||
@posts = policy_scope(@site.posts_for(@lang),
|
||||
policy_scope_class: PostPolicy::Scope)
|
||||
# TODO: Aplicar policy_scope
|
||||
@posts = @site.posts(lang: I18n.locale)
|
||||
|
||||
if params[:sort_by].present?
|
||||
begin
|
||||
|
@ -33,12 +34,8 @@ class PostsController < ApplicationController
|
|||
def new
|
||||
authorize Post
|
||||
@site = find_site
|
||||
@lang = find_lang(@site)
|
||||
@template = find_template(@site)
|
||||
@post = Post.new(site: @site,
|
||||
front_matter: { date: Time.now },
|
||||
lang: @lang,
|
||||
template: @template)
|
||||
# TODO: Implementar layout
|
||||
@post = @site.posts.build(lang: I18n.locale)
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -2,26 +2,23 @@
|
|||
|
||||
# Helpers
|
||||
module ApplicationHelper
|
||||
# Devuelve el atributo name de un campo posiblemente anidado
|
||||
def field_name_for_post(names)
|
||||
return ['post', names] if names.is_a? String
|
||||
|
||||
names = names.dup
|
||||
root = 'post'
|
||||
# Devuelve el atributo name de un campo anidado en el formato que
|
||||
# esperan los helpers *_field
|
||||
#
|
||||
# [ 'post', :image, :description ]
|
||||
# [ 'post[image]', :description ]
|
||||
# 'post[image][description]'
|
||||
def field_name_for(*names)
|
||||
name = names.pop
|
||||
root = names.shift
|
||||
|
||||
names.each do |n|
|
||||
root = "#{root}[#{n}]"
|
||||
root += "[#{n}]"
|
||||
end
|
||||
|
||||
[root, name]
|
||||
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)
|
||||
if seconds > 60
|
||||
distance_of_time_in_words seconds
|
||||
|
@ -49,4 +46,53 @@ module ApplicationHelper
|
|||
def form_class(model)
|
||||
model.errors.messages.empty? ? 'needs-validation' : 'was-validated'
|
||||
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
|
||||
|
|
34
app/models/metadata_content.rb
Normal file
34
app/models/metadata_content.rb
Normal 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
|
|
@ -1,3 +1,4 @@
|
|||
class MetadataDate < MetadataTemplate
|
||||
# frozen_string_literal: true
|
||||
|
||||
class MetadataDate < MetadataTemplate
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class MetadataImage < MetadataTemplate
|
||||
# Una ruta vacía a la imagen con una descripción vacía
|
||||
def default_value
|
||||
{ path: '', description: '' }
|
||||
{ 'path' => '', 'description' => '' }
|
||||
end
|
||||
|
||||
def empty?
|
||||
|
|
|
@ -17,6 +17,10 @@ class MetadataPath < MetadataTemplate
|
|||
Pathname.new(value).relative_path_from(Pathname.new(site.path)).to_s
|
||||
end
|
||||
|
||||
def basename
|
||||
File.basename(value, ext)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ext
|
||||
|
|
|
@ -6,4 +6,8 @@ class MetadataString < MetadataTemplate
|
|||
def default_value
|
||||
''
|
||||
end
|
||||
|
||||
def value
|
||||
super.strip
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,10 +10,10 @@ require 'jekyll/utils'
|
|||
# rubocop:disable Style/MissingRespondToMissing
|
||||
class Post < OpenStruct
|
||||
# Atributos por defecto
|
||||
# XXX: Volver document opcional cuando estemos creando
|
||||
DEFAULT_ATTRIBUTES = %i[site document layout].freeze
|
||||
# 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
|
||||
#
|
||||
|
@ -27,15 +27,8 @@ class Post < OpenStruct
|
|||
super(args)
|
||||
|
||||
# Genera un método con todos los atributos disponibles
|
||||
self.attributes = DEFAULT_ATTRIBUTES +
|
||||
ATTRIBUTES +
|
||||
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 = {}
|
||||
self.attributes = layout.metadata.keys.map(&:to_sym) + PUBLIC_ATTRIBUTES
|
||||
self.errors = {}
|
||||
|
||||
# Genera un atributo por cada uno de los campos de la plantilla,
|
||||
# MetadataFactory devuelve un tipo de campo por cada campo. A
|
||||
|
@ -63,6 +56,10 @@ class Post < OpenStruct
|
|||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def id
|
||||
path.basename
|
||||
end
|
||||
|
||||
# Levanta un error si al construir el artículo no pasamos un atributo.
|
||||
def default_attributes_missing(**args)
|
||||
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
|
||||
# plantilla
|
||||
def attribute?(mid)
|
||||
attrs = DEFAULT_ATTRIBUTES + PRIVATE_ATTRIBUTES + PUBLIC_ATTRIBUTES
|
||||
if singleton_class.method_defined? :attributes
|
||||
attributes.include? attribute_name(mid)
|
||||
(attrs + attributes).include? attribute_name(mid)
|
||||
else
|
||||
(DEFAULT_ATTRIBUTES + ATTRIBUTES).include? attribute_name(mid)
|
||||
attrs.include? attribute_name(mid)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -119,7 +117,7 @@ class Post < OpenStruct
|
|||
# Asegurarse que haya un layout
|
||||
yaml['layout'] = layout.name.to_s
|
||||
|
||||
"#{yaml.to_yaml}---\n\n#{content}"
|
||||
"#{yaml.to_yaml}---\n\n#{content.value}"
|
||||
end
|
||||
|
||||
# Eliminar el artículo del repositorio y de la lista de artículos del
|
||||
|
|
|
@ -32,10 +32,10 @@ class PostRelation < Array
|
|||
|
||||
private
|
||||
|
||||
def build_layout(layout = :post)
|
||||
def build_layout(layout = nil)
|
||||
return layout if layout.is_a? Layout
|
||||
|
||||
site.layouts[layout]
|
||||
site.layouts[layout || :post]
|
||||
end
|
||||
|
||||
# Devuelve una colección Jekyll que hace pasar el documento
|
||||
|
|
|
@ -162,7 +162,7 @@ class Site < ApplicationRecord
|
|||
@layouts ||= data.fetch('layouts', {}).map do |name, metadata|
|
||||
{ name.to_sym => Layout.new(site: self,
|
||||
name: name.to_sym,
|
||||
metadata: metadata) }
|
||||
metadata: metadata.with_indifferent_access) }
|
||||
end.inject(:merge)
|
||||
end
|
||||
|
||||
|
|
|
@ -57,12 +57,14 @@ class PostPolicy
|
|||
# Las usuarias pueden ver todos los posts
|
||||
#
|
||||
# Les invitades solo pueden ver sus propios posts
|
||||
#
|
||||
# TODO: Arreglar
|
||||
def resolve
|
||||
return scope if scope.try(:first).try(:site).try(:usuarie?, usuarie)
|
||||
|
||||
# Asegurarse que al menos devolvemos []
|
||||
[scope.find do |post|
|
||||
post.author == usuarie.email
|
||||
post.author.value == usuarie.email
|
||||
end].flatten.compact
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
!!!
|
||||
%html
|
||||
%html{ lang: I18n.locale, dir: t('dir') }
|
||||
%head
|
||||
%meta{ content: 'text/html; charset=UTF-8',
|
||||
'http-equiv': 'Content-Type' }/
|
||||
|
@ -11,10 +11,7 @@
|
|||
= javascript_include_tag 'application',
|
||||
'data-turbolinks-track': 'reload'
|
||||
|
||||
- if @site.try(:persisted?) && @site.try(:config).try(:dig, 'css')
|
||||
%link{ rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: @site.get_url_from_site(@site.config.dig('css')) }
|
||||
-# TODO: Reimplementar get_url_from_site
|
||||
|
||||
- style = "background-image: url(#{@site.try(:cover)})"
|
||||
-# haml-lint:disable InlineStyles
|
||||
|
|
5
app/views/posts/_attribute_feedback.haml
Normal file
5
app/views/posts/_attribute_feedback.haml
Normal 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')
|
|
@ -1,144 +1,34 @@
|
|||
- unless @post.errors.empty?
|
||||
- unless post.errors.empty?
|
||||
.alert.alert-danger
|
||||
%ul
|
||||
- @post.errors.each do |key, error|
|
||||
- post.errors.each do |key, error|
|
||||
%li
|
||||
%strong= @post.template_fields.find { |tf| tf.key == key.to_s }.try(:label) || key
|
||||
%strong
|
||||
= key.capitalize
|
||||
= [error].flatten.join("\n")
|
||||
|
||||
-# TODO seleccionar la dirección por defecto según el idioma actual
|
||||
- direction = @post.get_front_matter('dir') || 'ltr'
|
||||
-# string para configurar la clase con direccion de texto
|
||||
- field_class = "form-control #{direction}"
|
||||
-# TODO habilitar form_for
|
||||
- if @post.new?
|
||||
- url = site_posts_path(@site, lang: @lang)
|
||||
- method = :post
|
||||
- else
|
||||
- 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
|
||||
-# TODO: habilitar form_for
|
||||
:ruby
|
||||
if post.new?
|
||||
url = site_posts_path(site)
|
||||
method = :post
|
||||
else
|
||||
url = site_post_path(site, post)
|
||||
method = :patch
|
||||
end
|
||||
|
||||
= hidden_field_tag 'template', params[:template]
|
||||
.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 @site.usuarie? current_user
|
||||
.form-group
|
||||
= label_tag 'post_author', t('posts.author')
|
||||
- todxs = (@site.usuaries + @site.invitades).compact.uniq.map(&:email)
|
||||
= select_tag 'post[author]',
|
||||
options_for_select(todxs, @post.author || current_user.email),
|
||||
{ class: 'form-control select2',
|
||||
data: { tags: true,
|
||||
placeholder: t('posts.select.placeholder'),
|
||||
'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
|
||||
-# Comienza el formulario
|
||||
= form_tag url, method: method, class: 'form post', multipart: true do
|
||||
|
||||
-# Botones de guardado
|
||||
= render 'posts/submit', site: site
|
||||
|
||||
-# Dibuja cada atributo
|
||||
- post.attributes.each do |attribute|
|
||||
- type = post.send(attribute).type
|
||||
= render "posts/attributes/#{type}",
|
||||
post: post, attribute: attribute,
|
||||
metadata: post.send(attribute)
|
||||
|
||||
-# Botones de guardado
|
||||
= render 'posts/submit', site: site
|
||||
|
|
9
app/views/posts/_submit.haml
Normal file
9
app/views/posts/_submit.haml
Normal 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'))
|
7
app/views/posts/attributes/_array.haml
Normal file
7
app/views/posts/attributes/_array.haml
Normal 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
|
6
app/views/posts/attributes/_content.haml
Normal file
6
app/views/posts/attributes/_content.haml
Normal 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)
|
6
app/views/posts/attributes/_document_date.haml
Normal file
6
app/views/posts/attributes/_document_date.haml
Normal 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
|
20
app/views/posts/attributes/_image.haml
Normal file
20
app/views/posts/attributes/_image.haml
Normal 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
|
6
app/views/posts/attributes/_slug.haml
Normal file
6
app/views/posts/attributes/_slug.haml
Normal 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
|
6
app/views/posts/attributes/_string.haml
Normal file
6
app/views/posts/attributes/_string.haml
Normal 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
|
|
@ -1,44 +1,21 @@
|
|||
.row
|
||||
.col
|
||||
= 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')
|
||||
.row
|
||||
.col
|
||||
%h1= @site.config.fetch('title', @site.name_with_i18n(@lang))
|
||||
%h1= @site.title
|
||||
|
||||
.row
|
||||
.col
|
||||
.btn-group
|
||||
- if @site.templates.empty?
|
||||
= link_to t('posts.new'), new_site_post_path(@site, lang: @lang),
|
||||
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'
|
||||
= link_to t('posts.new'), new_site_post_path(@site),
|
||||
class: 'btn btn-success'
|
||||
|
||||
.row
|
||||
.col
|
||||
|
@ -54,80 +31,35 @@
|
|||
category: @category,
|
||||
lang: @lang,
|
||||
sort_by: s)
|
||||
= form_tag site_reorder_posts_path, method: :post do
|
||||
= hidden_field 'posts', 'lang', value: @lang
|
||||
- if policy(@site).reorder_posts?
|
||||
- if @site.ordered? @lang
|
||||
.reorder-posts-panel.alert.alert-info.alert-dismissible.fade.show{role: 'alert'}
|
||||
= raw t('help.posts.reorder')
|
||||
%br
|
||||
= submit_tag t('posts.reorder_posts'), class: 'btn btn-success'
|
||||
%button.close{type: 'button',
|
||||
'aria-label': t('help.close') }
|
||||
%span{'aria-hidden': true} ×
|
||||
- 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} ×
|
||||
%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)
|
||||
%table.table.table-condensed.table-striped
|
||||
%tbody
|
||||
- @posts.each do |post|
|
||||
-#
|
||||
saltearse el post a menos que esté en la categoría por
|
||||
la que estamos filtrando
|
||||
- if @category
|
||||
- next unless post.categories.value.include?(@category)
|
||||
%tr
|
||||
%td
|
||||
= fa_icon 'arrows-v', class: 'handle'
|
||||
= hidden_field 'posts[order]', i, value: post.order, class: 'post_order'
|
||||
%small
|
||||
= link_to post.title.value,
|
||||
site_post_path(@site, post.id)
|
||||
- unless post.categories.value.empty?
|
||||
%br
|
||||
%span.order.is= post.order
|
||||
%span.order.was.d-none{data: { order: post.order }}= "(#{post.order})"
|
||||
%small
|
||||
- post.categories.value.each do |c|
|
||||
= link_to c, site_posts_path(@site, category: c)
|
||||
|
||||
%td{class: direction}
|
||||
= link_to post.title, site_post_path(@site, post, lang: @lang)
|
||||
- unless post.categories.empty?
|
||||
%br
|
||||
%small
|
||||
- post.categories.each do |c|
|
||||
= link_to c, site_posts_path(@site, category: c, lang: @lang),
|
||||
data: { toggle: 'tooltip' }, title: t('help.category')
|
||||
- if post.draft? || post.incomplete?
|
||||
%br
|
||||
- if post.draft?
|
||||
%span.badge.badge-info= t('posts.draft')
|
||||
- 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') }
|
||||
%td= post.date.value.strftime('%F')
|
||||
%td
|
||||
- if policy(post).edit?
|
||||
= link_to t('posts.edit'),
|
||||
edit_site_post_path(@site, post.id),
|
||||
class: 'btn btn-info'
|
||||
- if policy(post).destroy?
|
||||
= link_to t('posts.destroy'),
|
||||
site_post_path(@site, post.id),
|
||||
class: 'btn btn-danger',
|
||||
method: :delete,
|
||||
data: { confirm: t('posts.confirm_destroy') }
|
||||
- else
|
||||
%h2= t('posts.none')
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
.row
|
||||
.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
|
||||
.col-md-8
|
||||
= render 'posts/form'
|
||||
= render 'posts/form', site: @site, post: @post
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
en:
|
||||
dir: ltr
|
||||
site_service:
|
||||
create: 'Created %{name}'
|
||||
update: 'Updated %{name}'
|
||||
|
@ -53,6 +54,8 @@ en:
|
|||
lang: 'Main language'
|
||||
site:
|
||||
name: 'Name'
|
||||
title: 'Title'
|
||||
description: 'Description'
|
||||
errors:
|
||||
models:
|
||||
site:
|
||||
|
@ -297,6 +300,17 @@ en:
|
|||
en: 'English'
|
||||
ar: 'Arabic'
|
||||
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'
|
||||
sort:
|
||||
by: 'Sort by'
|
||||
|
@ -310,59 +324,8 @@ en:
|
|||
categories: 'Everything'
|
||||
index: 'Posts'
|
||||
edit: 'Edit'
|
||||
save: 'Send'
|
||||
save_incomplete: 'Save for later'
|
||||
draft: revision
|
||||
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!'
|
||||
open: 'Tip: You can add new options by typing them and pressing Enter'
|
||||
private: '🔒 The values of this field will remain private'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
es:
|
||||
dir: ltr
|
||||
site_service:
|
||||
create: 'Creado %{name}'
|
||||
update: 'Actualizado %{name}'
|
||||
|
@ -312,6 +313,17 @@ es:
|
|||
en: 'inglés'
|
||||
ar: 'árabe'
|
||||
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'
|
||||
sort:
|
||||
by: 'Ordenar por'
|
||||
|
@ -327,60 +339,6 @@ es:
|
|||
edit: 'Editar'
|
||||
draft: en revisión
|
||||
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'
|
||||
private: '🔒 Los valores de este campo serán privados'
|
||||
select:
|
||||
|
|
|
@ -98,3 +98,11 @@ Al instanciar un `Post`, se pasan el sitio y la plantilla por defecto.
|
|||
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í
|
||||
|
|
|
@ -162,6 +162,7 @@ class PostTest < ActiveSupport::TestCase
|
|||
test 'se pueden crear nuevos' do
|
||||
post = @site.posts.build(layout: :post)
|
||||
post.title.value = 'test'
|
||||
post.content.value = 'test'
|
||||
|
||||
assert post.new?
|
||||
assert post.save
|
||||
|
|
Loading…
Reference in a new issue