diff --git a/app/models/metadata_has_one_nested.rb b/app/models/metadata_has_one_nested.rb new file mode 100644 index 00000000..ed509013 --- /dev/null +++ b/app/models/metadata_has_one_nested.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class MetadataHasOneNested < MetadataHasOne + def nested + @nested ||= layout.metadata.dig(name, 'nested') + end + + def nested? + true + end +end diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index a9765918..886b5f5d 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -12,6 +12,10 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, false end + def nested? + false + end + def inspect "#<#{self.class} site=#{site.name.inspect} post=#{post.id.inspect} value=#{value.inspect}>" end @@ -38,18 +42,10 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, "#{cache_key}-#{cache_version}" 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 # vez. def value_was - return @value_was if instance_variable_defined? '@value_was' - - @value_was = document_value + @value_was ||= document_value.nil? ? default_value : document_value end 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 def writable? case layout.metadata.dig(name, 'writable') - when 'once' then value.blank? + when 'once' then value_was.blank? else true end end diff --git a/app/models/post.rb b/app/models/post.rb index 591821eb..053c995f 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -15,6 +15,9 @@ class Post PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze ATTR_SUFFIXES = %w[? =].freeze + class PostError < StandardError; end + class UnknownAttributeError < PostError; end + attr_reader :attributes, :errors, :layout, :site, :document # TODO: Modificar el historial de Git con callbacks en lugar de @@ -54,12 +57,33 @@ class Post @errors = {} @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| - public_send(attr)&.value = args[attr] if args.key?(attr) + self[attr].value = attrs[attr] if attrs.key?(attr) && self[attr].writable? 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 def inspect @@ -169,8 +193,7 @@ class Post # Limpiar el nombre del atributo, para que todos los ayudantes # reciban el método en limpio unless attribute? name - raise NoMethodError, I18n.t('exceptions.post.no_method', - method: name) + raise UnknownAttributeError, I18n.t('exceptions.post.no_method', method: name) end define_singleton_method(name) do @@ -390,11 +413,7 @@ class Post end def update_attributes(hashable) - hashable.to_hash.each do |attr, value| - next unless self[attr].writable? - - self[attr].value = value - end + assign_attributes(hashable) save end @@ -408,6 +427,13 @@ class Post @usuaries ||= document_usuaries.empty? ? [] : Usuarie.where(id: document_usuaries).to_a end + # Todos los atributos anidados + # + # @return [Array] + def nested_attributes + @nested_attributes ||= attributes.map { |a| self[a] }.select(&:nested?).map(&:name) + end + private # Levanta un error si al construir el artículo no pasamos un atributo. diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 4631a9a4..08a46ae7 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -10,13 +10,18 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do self.post = site.posts(lang: locale) .build(layout: layout) 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| post.slug.value = p[:slug] if p[:slug].present? end - commit(action: :created, add: update_related_posts) if post.update(post_params) + # Crea los posts anidados + create_nested_posts! post, params[:post] + update_related_posts + + commit(action: :created, add: files) if post.save update_site_license! @@ -34,7 +39,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # Los artículos anónimos siempre son borradores 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 end @@ -47,9 +52,11 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do rm = [] rm << post.path.value_was if post.path.changed? + update_related_posts + # Es importante que el artículo se guarde primero y luego los # relacionados. - commit(action: :updated, add: update_related_posts, rm: rm) + commit(action: :updated, add: files, rm: rm) update_site_license! end @@ -96,6 +103,15 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do 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: []) site.repository.commit(add: add, rm: rm, @@ -108,7 +124,7 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # Solo permitir cambiar estos atributos de cada articulo def post_params - params.require(:post).permit(post.params) + @post_params ||= params.require(:post).permit(post.params).to_h end # Eliminar metadatos internos @@ -146,8 +162,10 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do end posts.map do |p| - p.path.absolute if p.save(validate: false) - end.compact << post.path.absolute + next unless p.save(validate: false) + + files << p.path.absolute + end end # Si les usuaries modifican o crean una licencia, considerarla @@ -157,4 +175,20 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do site.update licencia: Licencia.find_by_icons('custom') 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 = 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 diff --git a/app/views/posts/_attributes.haml b/app/views/posts/_attributes.haml new file mode 100644 index 00000000..ed958d08 --- /dev/null +++ b/app/views/posts/_attributes.haml @@ -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)) diff --git a/app/views/posts/_attributes_nested.haml b/app/views/posts/_attributes_nested.haml new file mode 100644 index 00000000..33996aa3 --- /dev/null +++ b/app/views/posts/_attributes_nested.haml @@ -0,0 +1,19 @@ +-# + @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] + - 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: false diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index 7de0ea79..92bee939 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -41,16 +41,7 @@ = hidden_field_tag 'post[layout]', post.layout.name -# Dibuja cada atributo - - post.attributes.each do |attribute| - - 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)) + = render 'posts/attributes', site: site, post: post, dir: dir, base: 'post', locale: @locale -# Botones de guardado = render 'posts/submit', site: site, post: post diff --git a/app/views/posts/attribute_ro/_has_one_nested.haml b/app/views/posts/attribute_ro/_has_one_nested.haml new file mode 100644 index 00000000..425e659e --- /dev/null +++ b/app/views/posts/attribute_ro/_has_one_nested.haml @@ -0,0 +1,6 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ dir: dir, lang: locale } + - p = metadata.has_one + - if p + = link_to p.title.value, site_post_path(site, p.id) diff --git a/app/views/posts/attributes/_has_one_nested.haml b/app/views/posts/attributes/_has_one_nested.haml new file mode 100644 index 00000000..e98bff47 --- /dev/null +++ b/app/views/posts/attributes/_has_one_nested.haml @@ -0,0 +1,6 @@ +- new_post = 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: new_post, dir: dir, base: base, locale: locale, inverse: metadata.inverse