Newer
Older
dmpopidor / app / models / madmp_fragment.rb
# == Schema Information
#
# Table name: madmp_fragments
#
#  id                        :integer          not null, primary key
#  data                      :json
#  answer_id                 :integer
#  madmp_schema_id :integer
#  created_at                :datetime         not null
#  updated_at                :datetime         not null
#  classname                 :string
#  dmp_id                    :integer
#  parent_id                 :integer
#
# Indexes
#
#  index_madmp_fragments_on_answer_id                  (answer_id)
#  index_madmp_fragments_on_madmp_schema_id  (madmp_schema_id)
#
require "jsonpath"

class MadmpFragment < ActiveRecord::Base

  include ValidationMessages
  include DynamicFormHelper
  include ApiFragment
  include CodebaseFragment

  # ================
  # = Associations =
  # ================

  belongs_to :answer
  belongs_to :madmp_schema, class_name: "MadmpSchema"
  belongs_to :dmp, class_name: "Fragment::Dmp", foreign_key: "dmp_id"
  has_many :children, class_name: "MadmpFragment", foreign_key: "parent_id", dependent: :destroy
  belongs_to :parent, class_name: "MadmpFragment", foreign_key: "parent_id"

  # ===============
  # = Validations =
  # ===============

  # validates :madmp_schema, presence: { message: PRESENCE_MESSAGE }

  # ================
  # = Single Table Inheritence =
  # ================
  self.inheritance_column = :classname
  scope :backup_policies, -> { where(classname: "backup_policy") }
  scope :budgets, -> { where(classname: "budgets") }
  scope :contributors, -> { where(classname: "contributor") }
  scope :costs, -> { where(classname: "cost") }
  scope :data_collections, -> { where(classname: "data_collection") }
  scope :data_preservations, -> { where(classname: "data_preservation") }
  scope :data_processings, -> { where(classname: "data_processing") }
  scope :data_reuses, -> { where(classname: "data_reuse") }
  scope :data_sharings, -> { where(classname: "data_sharing") }
  scope :data_storages, -> { where(classname: "data_storage") }
  scope :distributions, -> { where(classname: "distribution") }
  scope :dmps, -> { where(classname: "dmp") }
  scope :documentation_qualities, -> { where(classname: "documentation_quality") }
  scope :ethical_issues, -> { where(classname: "ethical_issue") }
  scope :funders, -> { where(classname: "funder") }
  scope :fundings, -> { where(classname: "funding") }
  scope :legal_issues, -> { where(classname: "legal_issue") }
  scope :licences, -> { where(classname: "licence") }
  scope :metas, -> { where(classname: "meta") }
  scope :metadata_standards, -> { where(classname: "metadata_standard") }
  scope :partners, -> { where(classname: "partner") }
  scope :persons, -> { where(classname: "person") }
  scope :personal_data_issues, -> { where(classname: "personal_data_issue") }
  scope :projects, -> { where(classname: "project") }
  scope :research_outputs, -> { where(classname: "research_output") }
  scope :research_output_descriptions, -> { where(classname: "research_output_description") }
  scope :resource_references, -> { where(classname: "resource_reference") }
  scope :reused_datas, -> { where(classname: "reused_data") }
  scope :specific_datas, -> { where(classname: "specific_data") }
  scope :technical_resources, -> { where(classname: "technical_resource") }

  # =============
  # = Callbacks =
  # =============

  before_save   :set_defaults
  after_create  :update_parent_references
  after_destroy :update_parent_references

  # =====================
  # = Nested Attributes =
  # =====================
  accepts_nested_attributes_for :answer, allow_destroy: true

  # ========================
  # = Public class methods =
  # ========================

  def plan
    if dmp.nil?
      Plan.find(data["plan_id"])
    else
      dmp.plan
    end
  end

  # Returns the schema associated to the JSON fragment
  def json_schema
    madmp_schema.schema
  end

  def get_dmp_fragments
    MadmpFragment.where(dmp_id: dmp.id)
  end

  # Returns a human readable version of the structured answer
  def to_s
    return additional_info["custom_value"] if additional_info["custom_value"].present?

    full_data = get_full_fragment
    displayable = ""
    if json_schema["to_string"]
      json_schema["to_string"].each do |pattern|
        # if it's a JsonPath pattern
        if pattern.first == "$"
          match = JsonPath.on(full_data, pattern)

          next if match.empty? || match.first.nil?

          if match.first.is_a?(Array)
            displayable += match.first.join("/")
          elsif match.first.is_a?(Integer) || match.first.is_a?(Float)
            displayable += match.first.to_s
          else
            displayable += match.first
          end
        else
          displayable += pattern
        end
      end
    else
      displayable = full_data.to_s
    end
    displayable
  end

  # This method generates references to the child fragments in the parent fragment
  # it updates the json "data" field in the database
  # it groups the children fragment by classname and extracts the list of ids
  # to create the json structure needed to update the "data" field
  # this method should be called when creating or deleting a child fragment
  def update_children_references
    updated_data = data
    classified_children = children.group_by {
      |t| t.additional_info["property_name"] unless t.additional_info.nil?
    }

    madmp_schema.schema["properties"].each do |key, prop|
      if prop["type"].eql?("array") && prop["items"]["type"].eql?("object")
        updated_data[key] = []
        if classified_children[key].present?
          updated_data[key] = classified_children[key].map { |c| { "dbid" => c.id } }
          next
        end
      elsif prop["type"].eql?("object") && prop["schema_id"].present?
        next if classified_children[key].nil?

        updated_data[key] = { "dbid" => classified_children[key][0].id }
      end
    end
    update!(data: updated_data)
  end

  def update_parent_references
    return if classname.nil? || parent.nil?

    parent.update_children_references
  end

  # This method return the fragment full record
  # It integrates its children into the JSON
  def get_full_fragment(with_ids: false, with_template_name: false)
    return { "custom_value" => additional_info["custom_value"] } if additional_info["custom_value"].present?

    children = self.children
    editable_data = data
    editable_data.each do |prop, value|
      if value.is_a?(Hash) && value["dbid"].present?
        child = children.exists?(value["dbid"]) ? children.find(value["dbid"]) : MadmpFragment.find(value["dbid"])
        child_data = nil
        if child.additional_info["custom_value"].present?
          child_data = { "custom_value" => child.additional_info["custom_value"] }
        else
          child_data = child.get_full_fragment(
            with_ids: with_ids,
            with_template_name: with_template_name
          )
        end
        editable_data = editable_data.merge(prop => child_data)
      end

      if value.is_a?(Array) && !value.empty?
        fragment_tab = []
        value.each do |v|
          next if v.nil?

          if v.is_a?(Hash) && v["dbid"].present?
            child_data = children.exists?(v["dbid"]) ? children.find(v["dbid"]) : MadmpFragment.find(v["dbid"])
            fragment_tab.push(
              child_data.get_full_fragment(
                with_ids: with_ids,
                with_template_name: with_template_name
              )
            )
          else
            fragment_tab.push(v)
          end
        end
        editable_data = editable_data.merge(prop => fragment_tab)
      end
    end
    if with_ids
      editable_data = { "id" => id, "schema_id" => madmp_schema_id }.merge(editable_data)
    end
    if with_template_name
      editable_data = { "template_name" => madmp_schema.name }.merge(editable_data)
    end
    editable_data
  end

  # This method take a fragment and convert its data with the target schema
  def schema_conversion(target_schema)
    origin_schema_properties = madmp_schema.schema["properties"]
    converted_data = {}

    target_schema.schema["properties"].each do |key, target_prop|
      origin_prop = origin_schema_properties[key]
      next if origin_prop.nil?

      if target_prop["type"].eql?("array")
        converted_data[key] = data[key].is_a?(Array) ? data[key] : [data[key]]
        if target_prop["items"]["type"].eql?("object")
          next if converted_data[key].empty? || converted_data[key].first.nil?

          target_sub_schema = MadmpSchema.find(target_prop["items"]["schema_id"])
          converted_data[key].map { |v| MadmpFragment.find(v["dbid"]).schema_conversion(target_sub_schema) }
        end
      elsif origin_prop["type"].eql?("object")
        converted_data[key] = data[key]
        next if origin_prop["inputType"].present? && origin_prop["inputType"].eql?("pickOrCreate")

        sub_fragment = MadmpFragment.find(data[key]["dbid"])
        target_sub_schema = MadmpSchema.find(target_prop["schema_id"])
        sub_fragment.schema_conversion(target_sub_schema)
      elsif origin_prop["type"].eql?("array")
        if target_prop["type"].eql?("object")
          target_sub_schema = MadmpSchema.find(target_prop["schema_id"])
          data[key] = [] if data[key].nil?
          if data[key].empty?
            sub_fragment = MadmpFragment.new(
              data: {},
              answer_id: nil,
              dmp_id: dmp.id,
              parent_id: id,
              madmp_schema: target_sub_schema,
              additional_info: { property_name: key }
            )
            sub_fragment.assign_attributes(classname: sub_fragment.classname)
            sub_fragment.instantiate
          else
            first_id = data[key].first["dbid"]
            MadmpFragment.find(first_id).schema_conversion(target_sub_schema)
            converted_data[key] = { "dbid" => first_id }
          end
        else
          converted_data[key] = data[key].first
        end
      else
        converted_data[key] = data[key]
      end
    end
    update!(
      data: converted_data,
      madmp_schema_id: target_schema.id
    )
    update_children_references
  end

  # This method is called when a form is opened for the first time
  # It creates the whole tree of sub_fragments
  def instantiate
    save! unless id.present?

    new_data = data
    madmp_schema.schema["properties"].each do |key, prop|
      if prop["type"].eql?("object") && prop["schema_id"].present?
        sub_schema = MadmpSchema.find(prop["schema_id"])

        next if sub_schema.classname.eql?("person") || new_data[key].present?

        sub_fragment = MadmpFragment.new(
          data: {},
          answer_id: nil,
          dmp_id: dmp.id,
          parent_id: id,
          madmp_schema: sub_schema,
          additional_info: { property_name: key }
        )
        sub_fragment.assign_attributes(classname: sub_schema.classname)
        sub_fragment.instantiate
        new_data[key] = { "dbid" => sub_fragment.id }
      end
    end
    update!(data: new_data)
  end

  def save_form_fragment(param_data, schema)
    fragmented_data = {}
    param_data.each do |prop, content|
      schema_prop = schema.schema["properties"][prop]

      next if schema_prop&.dig("type").nil?

      if schema_prop["type"].eql?("object") &&
         schema_prop["schema_id"].present?
        sub_data = content # TMP: for readability
        sub_schema = MadmpSchema.find(schema_prop["schema_id"])
        instantiate unless data[prop].present?
        next if param_data.nil?

        if schema_prop&.dig("inputType").eql?("pickOrCreate")
          fragmented_data[prop] = content
        elsif schema_prop["overridable"].present? &&
              param_data.dig(prop, "custom_value").present?
          # if the property is overridable & value is custom, take the value as is
          sub_fragment = MadmpFragment.find(data[prop]["dbid"])
          additional_info = param_data.dig(prop, "custom_value").eql?("__DELETED__") ? {} : sub_fragment.additional_info.merge(sub_data)
          sub_fragment.update(
            data: {},
            additional_info: additional_info
          )
        elsif data.dig(prop, "dbid")
          sub_fragment = MadmpFragment.find(data[prop]["dbid"])
          sub_fragment.save_form_fragment(sub_data, sub_schema)
        end
      else
        fragmented_data[prop] = content
      end
    end
    update!(
      data: data.merge(fragmented_data),
      additional_info: additional_info.except!("custom_value")
    )
  end

  def get_property(property_name)
    return if data.empty? || data[property_name].nil?

    if data[property_name]["dbid"].present?
      MadmpFragment.find(data[property_name]["dbid"])
    else
      data[property_name]
    end
  end

  # Get the research output fragment from the fragment hierarchy
  def research_output_fragment
    return nil if %w[meta dmp project].include?(classname)

    return self if classname.eql?("research_output")

    parent.research_output_fragment
  end
  # =================
  # = Class methods =
  # =================

  # Validate the fragment data with the linked schema
  # and saves the result with the fragment data
  def self.validate_data(data, schema)
    schemer = JSONSchemer.schema(schema)
    unformated = schemer.validate(data).to_a
    validations = {}
    unformated.each do |valid|
      next if valid["type"].eql?("object")

      key = valid["data_pointer"][1..-1]
      if valid["type"].eql?("required")
        required = JsonPath.on(valid, "$..missing_keys").flatten
        required.each do |req|
          validations[req] ? validations[req].push("required") : validations[req] = ["required"]
        end
      else
        validations[key] ? validations[key].push(valid["type"]) : validations[key] = [valid["type"]]
      end
    end
    validations
  end

  # Checks for a given dmp_id (and parent_id) if a fragment exists in the database
  def self.fragment_exists?(data, schema, dmp_id, parent_id = nil, current_fragment_id = nil)
    return false if schema.schema["unicity"].nil? || schema.schema["unicity"].empty?

    classname = schema.classname
    parent_id = nil if classname.eql?("person")
    unicity_properties = schema.schema["unicity"]
    dmp_fragments = MadmpFragment.where(
      dmp_id: dmp_id,
      parent_id: parent_id,
      classname: classname
    ).where.not(id: current_fragment_id)

    dmp_fragments.each do |fragment|
      filtered_db_data = fragment.data.slice(*unicity_properties)
      filtered_incoming_data = data.slice(*unicity_properties)
      next if filtered_db_data.empty?

      return true if filtered_db_data.eql?(filtered_incoming_data)
    end
    false
  end

  def self.find_sti_class(type_name)
    self
  end

  private

  # Initialize the data field
  def set_defaults
    self.data ||= {}
    self.additional_info ||= {}
    self.parent_id = nil if classname.eql?("person")
  end

end