mirror of
https://0xacab.org/sutty/sutty
synced 2025-03-14 17:08:21 +00:00
Merge branch 'issue-15066' into 'rails'
Issue #15066 Closes #16100 See merge request sutty/sutty!258
This commit is contained in:
commit
ce3ebc0201
13 changed files with 188 additions and 63 deletions
|
@ -49,6 +49,8 @@ class MetadataFile < MetadataTemplate
|
||||||
value['path'] = relative_destination_path_with_filename.to_s if static_file
|
value['path'] = relative_destination_path_with_filename.to_s if static_file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self[:value] = self[:value].to_h
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
16
app/models/metadata_has_one_nested.rb
Normal file
16
app/models/metadata_has_one_nested.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MetadataHasOneNested < MetadataHasOne
|
||||||
|
def nested
|
||||||
|
@nested ||= layout.metadata.dig(name, 'nested')
|
||||||
|
end
|
||||||
|
|
||||||
|
def nested?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# No tener conflictos con related
|
||||||
|
def related_methods
|
||||||
|
@related_methods ||= [].freeze
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,10 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nested?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def inspect
|
def inspect
|
||||||
"#<#{self.class} site=#{site.name.inspect} post=#{post.id.inspect} value=#{value.inspect}>"
|
"#<#{self.class} site=#{site.name.inspect} post=#{post.id.inspect} value=#{value.inspect}>"
|
||||||
end
|
end
|
||||||
|
@ -38,18 +42,10 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
"#{cache_key}-#{cache_version}"
|
"#{cache_key}-#{cache_version}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# XXX: Deberíamos sanitizar durante la asignación?
|
|
||||||
def value=(new_value)
|
|
||||||
@value_was = value
|
|
||||||
self[:value] = new_value
|
|
||||||
end
|
|
||||||
|
|
||||||
# Siempre obtener el valor actual y solo obtenerlo del documento una
|
# Siempre obtener el valor actual y solo obtenerlo del documento una
|
||||||
# vez.
|
# vez.
|
||||||
def value_was
|
def value_was
|
||||||
return @value_was if instance_variable_defined? '@value_was'
|
@value_was ||= document_value.nil? ? default_value : document_value
|
||||||
|
|
||||||
@value_was = document_value
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def changed?
|
def changed?
|
||||||
|
@ -169,7 +165,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
# once => el campo solo se puede modificar si estaba vacío
|
# once => el campo solo se puede modificar si estaba vacío
|
||||||
def writable?
|
def writable?
|
||||||
case layout.metadata.dig(name, 'writable')
|
case layout.metadata.dig(name, 'writable')
|
||||||
when 'once' then value.blank?
|
when 'once' then value_was.blank?
|
||||||
else true
|
else true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,9 @@ class Post
|
||||||
PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze
|
PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze
|
||||||
ATTR_SUFFIXES = %w[? =].freeze
|
ATTR_SUFFIXES = %w[? =].freeze
|
||||||
|
|
||||||
|
class PostError < StandardError; end
|
||||||
|
class UnknownAttributeError < PostError; end
|
||||||
|
|
||||||
attr_reader :attributes, :errors, :layout, :site, :document
|
attr_reader :attributes, :errors, :layout, :site, :document
|
||||||
|
|
||||||
# TODO: Modificar el historial de Git con callbacks en lugar de
|
# TODO: Modificar el historial de Git con callbacks en lugar de
|
||||||
|
@ -50,12 +53,33 @@ class Post
|
||||||
@errors = {}
|
@errors = {}
|
||||||
@metadata = {}
|
@metadata = {}
|
||||||
|
|
||||||
# Inicializar valores
|
# Leer el documento si existe
|
||||||
|
# @todo Asignar todos los valores a self[:value] luego de leer
|
||||||
|
document&.read! unless new?
|
||||||
|
|
||||||
|
# Inicializar valores o modificar los que vengan del documento
|
||||||
|
assignable_attributes = args.slice(*attributes)
|
||||||
|
assign_attributes(assignable_attributes) if assignable_attributes.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Asignar atributos, ignorando atributos que no se pueden modificar
|
||||||
|
# o inexistentes
|
||||||
|
#
|
||||||
|
# @param attrs [Hash]
|
||||||
|
def assign_attributes(attrs)
|
||||||
|
attrs = attrs.transform_keys(&:to_sym)
|
||||||
|
|
||||||
attributes.each do |attr|
|
attributes.each do |attr|
|
||||||
public_send(attr)&.value = args[attr] if args.key?(attr)
|
self[attr].value = attrs[attr] if attrs.key?(attr) && self[attr].writable?
|
||||||
end
|
end
|
||||||
|
|
||||||
document.read! unless new?
|
unknown_attrs = attrs.keys.map(&:to_sym) - attributes
|
||||||
|
|
||||||
|
if unknown_attrs.present?
|
||||||
|
raise UnknownAttributeError, "Unknown attribute(s) #{unknown_attrs.map(&:to_s).join(', ')} for Post"
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def inspect
|
def inspect
|
||||||
|
@ -165,8 +189,7 @@ class Post
|
||||||
# Limpiar el nombre del atributo, para que todos los ayudantes
|
# Limpiar el nombre del atributo, para que todos los ayudantes
|
||||||
# reciban el método en limpio
|
# reciban el método en limpio
|
||||||
unless attribute? name
|
unless attribute? name
|
||||||
raise NoMethodError, I18n.t('exceptions.post.no_method',
|
raise NoMethodError, I18n.t('exceptions.post.no_method', method: name)
|
||||||
method: name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
define_singleton_method(name) do
|
define_singleton_method(name) do
|
||||||
|
@ -386,11 +409,7 @@ class Post
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_attributes(hashable)
|
def update_attributes(hashable)
|
||||||
hashable.to_hash.each do |attr, value|
|
assign_attributes(hashable)
|
||||||
next unless self[attr].writable?
|
|
||||||
|
|
||||||
self[attr].value = value
|
|
||||||
end
|
|
||||||
|
|
||||||
save
|
save
|
||||||
end
|
end
|
||||||
|
@ -404,6 +423,13 @@ class Post
|
||||||
@usuaries ||= document_usuaries.empty? ? [] : Usuarie.where(id: document_usuaries).to_a
|
@usuaries ||= document_usuaries.empty? ? [] : Usuarie.where(id: document_usuaries).to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Todos los atributos anidados
|
||||||
|
#
|
||||||
|
# @return [Array<Symbol>]
|
||||||
|
def nested_attributes
|
||||||
|
@nested_attributes ||= attributes.map { |a| self[a] }.select(&:nested?).map(&:name)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
|
@ -10,13 +10,19 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
self.post = site.posts(lang: locale)
|
self.post = site.posts(lang: locale)
|
||||||
.build(layout: layout)
|
.build(layout: layout)
|
||||||
post.usuaries << usuarie
|
post.usuaries << usuarie
|
||||||
params[:post][:draft] = true if site.invitade? usuarie
|
post.draft.value = true if site.invitade? usuarie
|
||||||
|
post.assign_attributes(post_params)
|
||||||
|
|
||||||
params.require(:post).permit(:slug).tap do |p|
|
params.require(:post).permit(:slug).tap do |p|
|
||||||
post.slug.value = p[:slug] if p[:slug].present?
|
post.slug.value = p[:slug] if p[:slug].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
commit(action: :created, add: update_related_posts) if post.update(post_params)
|
# Crea los posts anidados
|
||||||
|
create_nested_posts! post, params[:post]
|
||||||
|
post.save
|
||||||
|
update_related_posts
|
||||||
|
|
||||||
|
commit(action: :created, add: files)
|
||||||
|
|
||||||
update_site_license!
|
update_site_license!
|
||||||
|
|
||||||
|
@ -34,7 +40,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
# Los artículos anónimos siempre son borradores
|
# Los artículos anónimos siempre son borradores
|
||||||
params[:draft] = true
|
params[:draft] = true
|
||||||
|
|
||||||
commit(action: :created, add: [post.path.absolute]) if post.update(anon_post_params)
|
commit(action: :created, add: files) if post.update(anon_post_params)
|
||||||
post
|
post
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -47,9 +53,12 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
rm = []
|
rm = []
|
||||||
rm << post.path.value_was if post.path.changed?
|
rm << post.path.value_was if post.path.changed?
|
||||||
|
|
||||||
|
create_nested_posts! post, params[:post]
|
||||||
|
update_related_posts
|
||||||
|
|
||||||
# Es importante que el artículo se guarde primero y luego los
|
# Es importante que el artículo se guarde primero y luego los
|
||||||
# relacionados.
|
# relacionados.
|
||||||
commit(action: :updated, add: update_related_posts, rm: rm)
|
commit(action: :updated, add: files, rm: rm)
|
||||||
|
|
||||||
update_site_license!
|
update_site_license!
|
||||||
end
|
end
|
||||||
|
@ -96,6 +105,15 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Una lista de archivos a modificar
|
||||||
|
#
|
||||||
|
# @return [Set]
|
||||||
|
def files
|
||||||
|
@files ||= Set.new.tap do |f|
|
||||||
|
f << post.path.absolute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def commit(action:, add: [], rm: [])
|
def commit(action:, add: [], rm: [])
|
||||||
site.repository.commit(add: add,
|
site.repository.commit(add: add,
|
||||||
rm: rm,
|
rm: rm,
|
||||||
|
@ -108,7 +126,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
|
|
||||||
# Solo permitir cambiar estos atributos de cada articulo
|
# Solo permitir cambiar estos atributos de cada articulo
|
||||||
def post_params
|
def post_params
|
||||||
params.require(:post).permit(post.params)
|
@post_params ||= params.require(:post).permit(post.params).to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
# Eliminar metadatos internos
|
# Eliminar metadatos internos
|
||||||
|
@ -146,8 +164,10 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
end
|
end
|
||||||
|
|
||||||
posts.map do |p|
|
posts.map do |p|
|
||||||
p.path.absolute if p.save(validate: false)
|
next unless p.save(validate: false)
|
||||||
end.compact << post.path.absolute
|
|
||||||
|
files << p.path.absolute
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Si les usuaries modifican o crean una licencia, considerarla
|
# Si les usuaries modifican o crean una licencia, considerarla
|
||||||
|
@ -157,4 +177,20 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
site.update licencia: Licencia.find_by_icons('custom')
|
site.update licencia: Licencia.find_by_icons('custom')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Encuentra todos los posts anidados y los crea o modifica
|
||||||
|
def create_nested_posts!(post, params)
|
||||||
|
post.nested_attributes.each do |nested_attribute|
|
||||||
|
nested_metadata = post[nested_attribute]
|
||||||
|
# @todo find_or_initialize
|
||||||
|
nested_post = nested_metadata.has_one || site.posts(lang: post.lang.value).build(layout: nested_metadata.nested)
|
||||||
|
nested_params = params.require(nested_attribute).permit(nested_post.params).to_hash
|
||||||
|
|
||||||
|
# Completa la relación 1:1
|
||||||
|
nested_params[nested_metadata.inverse.to_s] = post.uuid.value
|
||||||
|
post[nested_attribute].value = nested_post.uuid.value
|
||||||
|
|
||||||
|
files << nested_post.path.absolute if nested_post.update(nested_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
@param :summary_class [String] Clases para el summary
|
@param :summary_class [String] Clases para el summary
|
||||||
|
|
||||||
- local_assigns[:summary_class] ||= 'h3'
|
- local_assigns[:summary_class] ||= 'h3'
|
||||||
|
- local_assigns[:details_class] ||= 'py-2'
|
||||||
|
|
||||||
%details.details.py-2{ id: local_assigns[:id], data: { controller: 'details', action: 'toggle->details#store' } }
|
%details.details{ id: local_assigns[:id], class: local_assigns[:details_class], data: { controller: 'details', action: 'toggle->details#store' } }
|
||||||
%summary.d-flex.flex-row.align-items-center.justify-content-between{ class: local_assigns[:summary_class] }
|
%summary.d-flex.flex-row.align-items-center.justify-content-between{ class: local_assigns[:summary_class] }
|
||||||
%span= summary
|
%span= summary
|
||||||
%span.hide-when-open ▶
|
%span.hide-when-open ▶
|
||||||
|
|
16
app/views/posts/_attributes.haml
Normal file
16
app/views/posts/_attributes.haml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
-#
|
||||||
|
@param base [String]
|
||||||
|
@param locale [String]
|
||||||
|
@param post [Post]
|
||||||
|
@param site [Site]
|
||||||
|
@param dir [String]
|
||||||
|
- post.attributes.each do |attribute|
|
||||||
|
- metadata = post[attribute]
|
||||||
|
- type = metadata.type
|
||||||
|
|
||||||
|
- cache [metadata, I18n.locale] do
|
||||||
|
= render("posts/attributes/#{type}",
|
||||||
|
base: base, post: post, attribute: attribute,
|
||||||
|
metadata: metadata, site: site,
|
||||||
|
dir: dir, locale: locale,
|
||||||
|
autofocus: (post.attributes.first == attribute))
|
18
app/views/posts/_attributes_nested.haml
Normal file
18
app/views/posts/_attributes_nested.haml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
-#
|
||||||
|
@param inverse [Symbol]
|
||||||
|
@param base [String]
|
||||||
|
@param locale [String]
|
||||||
|
@param post [Post]
|
||||||
|
@param site [Site]
|
||||||
|
@param dir [String]
|
||||||
|
- post.attributes.each do |attribute|
|
||||||
|
- next if attribute == :date
|
||||||
|
- next if attribute == :draft
|
||||||
|
- next if attribute == inverse
|
||||||
|
- metadata = post[attribute]
|
||||||
|
|
||||||
|
- cache [post, metadata, I18n.locale] do
|
||||||
|
= render "posts/attributes/#{metadata.type}",
|
||||||
|
base: base, post: post, attribute: attribute,
|
||||||
|
metadata: metadata, site: site,
|
||||||
|
dir: dir, locale: locale, autofocus: false
|
|
@ -41,16 +41,7 @@
|
||||||
= hidden_field_tag 'post[layout]', post.layout.name
|
= hidden_field_tag 'post[layout]', post.layout.name
|
||||||
|
|
||||||
-# Dibuja cada atributo
|
-# Dibuja cada atributo
|
||||||
- post.attributes.each do |attribute|
|
= render 'posts/attributes', site: site, post: post, dir: dir, base: 'post', locale: @locale
|
||||||
- metadata = post[attribute]
|
|
||||||
- type = metadata.type
|
|
||||||
|
|
||||||
- cache [metadata, I18n.locale] do
|
|
||||||
= render("posts/attributes/#{type}",
|
|
||||||
base: 'post', post: post, attribute: attribute,
|
|
||||||
metadata: metadata, site: site,
|
|
||||||
dir: dir, locale: @locale,
|
|
||||||
autofocus: (post.attributes.first == attribute))
|
|
||||||
|
|
||||||
-# Botones de guardado
|
-# Botones de guardado
|
||||||
= render 'posts/submit', site: site, post: post
|
= render 'posts/submit', site: site, post: post
|
||||||
|
|
34
app/views/posts/_table.haml
Normal file
34
app/views/posts/_table.haml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-#
|
||||||
|
Muestra una tabla con todos los atributos de un post
|
||||||
|
|
||||||
|
@param site [Site]
|
||||||
|
@param locale [Symbol]
|
||||||
|
@param dir [String]
|
||||||
|
@param post [Post]
|
||||||
|
@param title [String]
|
||||||
|
|
||||||
|
%table.table.table-condensed
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th.text-center{ colspan: 2 }= title
|
||||||
|
%tbody
|
||||||
|
- post.attributes.each do |attr|
|
||||||
|
- metadata = post[attr]
|
||||||
|
- next unless metadata.front_matter?
|
||||||
|
|
||||||
|
- cache [post, metadata, I18n.locale] do
|
||||||
|
= render("posts/attribute_ro/#{metadata.type}",
|
||||||
|
post: post, attribute: attr,
|
||||||
|
metadata: metadata,
|
||||||
|
site: site,
|
||||||
|
locale: locale,
|
||||||
|
dir: dir)
|
||||||
|
|
||||||
|
-# Mostrar todo lo que no va en el front_matter (el contenido)
|
||||||
|
- post.attributes.each do |attr|
|
||||||
|
- metadata = post[attr]
|
||||||
|
- next if metadata.front_matter?
|
||||||
|
|
||||||
|
- cache [post, metadata, I18n.locale] do
|
||||||
|
%section.content.pb-3{ id: attr, dir: dir }
|
||||||
|
= metadata.to_s.html_safe
|
6
app/views/posts/attribute_ro/_has_one_nested.haml
Normal file
6
app/views/posts/attribute_ro/_has_one_nested.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
%tr{ id: attribute }
|
||||||
|
%td{ dir: dir, lang: locale, colspan: 2 }
|
||||||
|
- if (p = metadata.has_one)
|
||||||
|
= render 'layouts/details', details_class: '', summary_class: 'font-weight-bold', summary: post_label_t(attribute, post: post) do
|
||||||
|
.mt-3
|
||||||
|
= render 'posts/table', site: site, post: p, dir: dir, locale: locale, title: p.layout.humanized_name
|
6
app/views/posts/attributes/_has_one_nested.haml
Normal file
6
app/views/posts/attributes/_has_one_nested.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- nested_post = metadata.has_one || site.posts(lang: locale).build(layout: metadata.nested)
|
||||||
|
- base = "#{base}[#{metadata.name}]"
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
= render 'layouts/details', id: metadata.nested, summary: site.layouts[metadata.nested].humanized_name do
|
||||||
|
= render 'posts/attributes_nested', site: site, post: nested_post, dir: dir, base: base, locale: locale, inverse: metadata.inverse
|
|
@ -1,4 +1,5 @@
|
||||||
- dir = @site.data.dig(params[:locale], 'dir')
|
- dir = @site.data.dig(params[:locale], 'dir')
|
||||||
|
|
||||||
.row.justify-content-center
|
.row.justify-content-center
|
||||||
.col-md-8
|
.col-md-8
|
||||||
%article.content.table-responsive-md
|
%article.content.table-responsive-md
|
||||||
|
@ -6,28 +7,4 @@
|
||||||
edit_site_post_path(@site, @post.id),
|
edit_site_post_path(@site, @post.id),
|
||||||
class: 'btn btn-secondary btn-block'
|
class: 'btn btn-secondary btn-block'
|
||||||
|
|
||||||
%table.table.table-condensed
|
= render 'table', dir: dir, site: @site, locale: @locale, post: @post, title: t('.front_matter')
|
||||||
%thead
|
|
||||||
%tr
|
|
||||||
%th.text-center{ colspan: 2 }= t('.front_matter')
|
|
||||||
%tbody
|
|
||||||
- @post.attributes.each do |attr|
|
|
||||||
- metadata = @post[attr]
|
|
||||||
- next unless metadata.front_matter?
|
|
||||||
|
|
||||||
- cache [metadata, I18n.locale] do
|
|
||||||
= render("posts/attribute_ro/#{metadata.type}",
|
|
||||||
post: @post, attribute: attr,
|
|
||||||
metadata: metadata,
|
|
||||||
site: @site,
|
|
||||||
locale: @locale,
|
|
||||||
dir: dir)
|
|
||||||
|
|
||||||
-# Mostrar todo lo que no va en el front_matter (el contenido)
|
|
||||||
- @post.attributes.each do |attr|
|
|
||||||
- metadata = @post[attr]
|
|
||||||
- next if metadata.front_matter?
|
|
||||||
|
|
||||||
- cache [metadata, I18n.locale] do
|
|
||||||
%section.content.pb-3{ id: attr, dir: dir }
|
|
||||||
= @post.public_send(attr).to_s.html_safe
|
|
||||||
|
|
Loading…
Reference in a new issue