next-l/enju_leaf

View on GitHub
app/models/manifestation.rb

Summary

Maintainability
F
4 days
Test Coverage
A
94%
class Manifestation < ApplicationRecord
  include EnjuCirculation::EnjuManifestation
  include EnjuSubject::EnjuManifestation
  include EnjuNdl::EnjuManifestation
  include EnjuNii::EnjuManifestation
  include EnjuLoc::EnjuManifestation
  include EnjuManifestationViewer::EnjuManifestation
  include EnjuBookmark::EnjuManifestation
  include EnjuOai::OaiModel

  has_many :creates, -> { order('creates.position') }, dependent: :destroy, foreign_key: 'work_id', inverse_of: :work
  has_many :creators, through: :creates, source: :agent
  has_many :realizes, -> { order('realizes.position') }, dependent: :destroy, foreign_key: 'expression_id', inverse_of: :expression
  has_many :contributors, through: :realizes, source: :agent
  has_many :produces, -> { order('produces.position') }, dependent: :destroy, foreign_key: 'manifestation_id', inverse_of: :manifestation
  has_many :publishers, through: :produces, source: :agent
  has_many :items, dependent: :destroy
  has_many :children, foreign_key: 'parent_id', class_name: 'ManifestationRelationship', dependent: :destroy, inverse_of: :parent
  has_many :parents, foreign_key: 'child_id', class_name: 'ManifestationRelationship', dependent: :destroy, inverse_of: :child
  has_many :derived_manifestations, through: :children, source: :child
  has_many :original_manifestations, through: :parents, source: :parent
  has_many :picture_files, as: :picture_attachable, dependent: :destroy
  belongs_to :language
  belongs_to :carrier_type
  belongs_to :manifestation_content_type, class_name: 'ContentType', foreign_key: 'content_type_id', inverse_of: :manifestations
  has_many :series_statements, dependent: :destroy
  belongs_to :frequency
  belongs_to :required_role, class_name: 'Role'
  has_one :resource_import_result
  has_many :identifiers, dependent: :destroy
  has_many :manifestation_custom_values, -> { joins(:manifestation_custom_property).order(:position) }, inverse_of: :manifestation, dependent: :destroy
  has_one :periodical_record, class_name: 'Periodical', dependent: :destroy
  has_one :periodical_and_manifestation, dependent: :destroy
  has_one :periodical, through: :periodical_and_manifestation, dependent: :destroy
  has_many :isbn_record_and_manifestations, dependent: :destroy
  has_many :isbn_records, through: :isbn_record_and_manifestations
  has_many :issn_record_and_manifestations, dependent: :destroy
  has_many :issn_records, through: :issn_record_and_manifestations
  has_one :doi_record, dependent: :destroy
  has_one :jpno_record, dependent: :destroy
  has_one :ncid_record, dependent: :destroy
  has_one :lccn_record, dependent: :destroy
  has_one :ndl_bib_id_record, dependent: :destroy

  accepts_nested_attributes_for :creators, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :contributors, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :publishers, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :series_statements, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :identifiers, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :manifestation_custom_values, reject_if: :all_blank
  accepts_nested_attributes_for :doi_record, reject_if: :all_blank
  accepts_nested_attributes_for :jpno_record, reject_if: :all_blank
  accepts_nested_attributes_for :ncid_record, reject_if: :all_blank
  accepts_nested_attributes_for :lccn_record, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :isbn_records, allow_destroy: true, reject_if: :all_blank
  accepts_nested_attributes_for :issn_records, allow_destroy: true, reject_if: :all_blank

  searchable do
    text :title, default_boost: 2 do
      titles
    end
    [:fulltext, :note, :creator, :contributor, :publisher, :abstract, :description, :statement_of_responsibility].each do |field|
      text field do
        if series_master?
          derived_manifestations.map{|c| c.send(field) }.flatten.compact
        else
          self.send(field)
        end
      end
    end
    text :item_identifier do
      if series_master?
        root_series_statement.root_manifestation.items.pluck(:item_identifier, :binding_item_identifier).flatten.compact
      else
        items.pluck(:item_identifier, :binding_item_identifier).flatten.compact
      end
    end
    string :call_number, multiple: true do
      items.pluck(:call_number)
    end
    string :title, multiple: true
    # text フィールドだと区切りのない文字列の index が上手く作成
    #できなかったので。 downcase することにした。
    #他の string 項目も同様の問題があるので、必要な項目は同様の処置が必要。
    string :connect_title do
      title.join('').gsub(/\s/, '').downcase
    end
    string :connect_creator do
      creator.join('').gsub(/\s/, '').downcase
    end
    string :connect_publisher do
      publisher.join('').gsub(/\s/, '').downcase
    end
    string :isbn, multiple: true do
      isbn_characters
    end
    string :issn, multiple: true do
      if series_statements.exists?
        [issn_records.pluck(:body), (series_statements.map{|s| s.manifestation.issn_records.pluck(:body)})].flatten.uniq.compact
      else
        issn_records.pluck(:body)
      end
    end
    string :lccn do
      lccn_record&.body
    end
    string :jpno, multiple: true do
      jpno_record&.body
    end
    string :carrier_type do
      carrier_type.name
    end
    string :library, multiple: true do
      if series_master?
        root_series_statement.root_manifestation.items.map{|i| i.shelf.library.name}.flatten.uniq
      else
        items.map{|i| i.shelf.library.name}
      end
    end
    string :language do
      language&.name
    end
    string :item_identifier, multiple: true do
      if series_master?
        root_series_statement.root_manifestation.items.pluck(:item_identifier, :binding_item_identifier).flatten.compact
      else
        items.pluck(:item_identifier, :binding_item_identifier).flatten.compact
      end
    end
    string :shelf, multiple: true do
      items.collect{|i| "#{i.shelf.library.name}_#{i.shelf.name}"}
    end
    time :created_at
    time :updated_at
    time :pub_date, multiple: true do
      if series_master?
        root_series_statement.root_manifestation.pub_dates
      else
        pub_dates
      end
    end
    time :date_of_publication
    integer :pub_year do
      date_of_publication&.year
    end
    integer :creator_ids, multiple: true
    integer :contributor_ids, multiple: true
    integer :publisher_ids, multiple: true
    integer :item_ids, multiple: true
    integer :original_manifestation_ids, multiple: true
    integer :parent_ids, multiple: true do
      original_manifestations.pluck(:id)
    end
    integer :required_role_id
    integer :height
    integer :width
    integer :depth
    integer :volume_number
    integer :issue_number
    integer :serial_number
    integer :start_page
    integer :end_page
    integer :number_of_pages
    float :price
    integer :series_statement_ids, multiple: true
    boolean :repository_content
    # for OpenURL
    text :aulast do
      creators.pluck(:last_name)
    end
    text :aufirst do
      creators.pluck(:first_name)
    end
    # OTC start
    string :creator, multiple: true do
      creator.map{|au| au.gsub(' ', '')}
    end
    text :au do
      creator
    end
    text :atitle do
      if serial? && root_series_statement.nil?
        titles
      end
    end
    text :btitle do
      title unless serial?
    end
    text :jtitle do
      if serial?
        if root_series_statement
          root_series_statement.titles
        else
          titles
        end
      end
    end
    text :isbn do # 前方一致検索のためtext指定を追加
      isbn_characters
    end
    text :issn do # 前方一致検索のためtext指定を追加
      if series_statements.exists?
        [issn_records.pluck(:body), (series_statements.map{|s| s.manifestation.issn_records.pluck(:body)})].flatten.uniq.compact
      else
        issn_records.pluck(:body)
      end
    end
    text :identifier do
      identifiers.pluck(:body)
    end
    string :sort_title
    string :doi do
      doi_record&.body
    end
    boolean :serial do
      serial?
    end
    boolean :series_master do
      series_master?
    end
    boolean :resource_master do
      if serial?
        if series_master?
          true
        else
          false
        end
      else
        true
      end
    end
    time :acquired_at
  end

  has_one_attached :attachment

  validates :original_title, presence: true
  validates :start_page, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :end_page, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :height, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :width, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :depth, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :manifestation_identifier, uniqueness: true, allow_blank: true
  validates :pub_date, format: {with: /\A\[{0,1}\d+([\/-]\d{0,2}){0,2}\]{0,1}\z/}, allow_blank: true
  validates :access_address, url: true, allow_blank: true, length: {maximum: 255}
  validates :issue_number, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :volume_number, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :serial_number, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  validates :edition, numericality: {greater_than_or_equal_to: 0}, allow_blank: true
  before_save :set_date_of_publication, :set_number
  before_save do
    attachment.purge if delete_attachment == '1'
  end
  after_create :clear_cached_numdocs
  after_destroy :index_series_statement
  after_save :index_series_statement
  after_touch do |manifestation|
    manifestation.index
    manifestation.index_series_statement
    Sunspot.commit
  end
  strip_attributes only: [:manifestation_identifier, :pub_date, :original_title]
  paginates_per 10

  attr_accessor :during_import, :parent_id, :delete_attachment

  def set_date_of_publication
    return if pub_date.blank?

    year = Time.utc(pub_date.rjust(4, "0").to_i).year rescue nil
    begin
      date = Time.zone.parse(pub_date.rjust(4, "0"))
      if date.year != year
        raise ArgumentError
      end
    rescue ArgumentError, TZInfo::AmbiguousTime
      date = nil
    end

    pub_date_string = pub_date.rjust(4, "0").split(';').first.gsub(/[\[\]]/, '')
    if pub_date_string.length == 4
      date = Time.zone.parse(Time.utc(pub_date_string).to_s).beginning_of_day
    else
      while date.nil?
        pub_date_string += '-01'
        break if pub_date_string =~ /-01-01-01$/

        begin
          date = Time.zone.parse(pub_date_string)
          if date.year != year
            raise ArgumentError
          end
        rescue ArgumentError
          date = nil
        rescue TZInfo::AmbiguousTime
          date = nil
          self.year_of_publication = pub_date_string.to_i if pub_date_string =~ /^\d+$/
          break
        end
      end
    end

    if date
      self.year_of_publication = date.year
      self.month_of_publication = date.month
      if date.year.positive?
        self.date_of_publication = date
      end
    end
  end

  def self.cached_numdocs
    Rails.cache.fetch("manifestation_search_total"){Manifestation.search.total}
  end

  def clear_cached_numdocs
    Rails.cache.delete("manifestation_search_total")
  end

  def parent_of_series
    original_manifestations
  end

  def number_of_pages
    if start_page && end_page
      end_page.to_i - start_page.to_i + 1
    end
  end

  def titles
    title = []
    title << original_title.to_s.strip
    title << title_transcription.to_s.strip
    title << title_alternative.to_s.strip
    title << volume_number_string
    title << issue_number_string
    title << serial_number.to_s
    title << edition_string
    title << series_statements.map{|s| s.titles}
    #title << original_title.wakati
    #title << title_transcription.wakati rescue nil
    #title << title_alternative.wakati rescue nil
    title.flatten
  end

  def url
    #access_address
    "#{LibraryGroup.site_config.url}#{self.class.to_s.tableize}/#{id}"
  end

  def creator
    creators.collect(&:name).flatten
  end

  def contributor
    contributors.collect(&:name).flatten
  end

  def publisher
    publishers.collect(&:name).flatten
  end

  def title
    titles
  end

  # TODO: よりよい推薦方法
  def self.pickup(keyword = nil, current_user = nil)
    return nil if self.cached_numdocs < 5

    if current_user&.role
      current_role_id = current_user.role.id
    else
      current_role_id = 1
    end

    # TODO: ヒット件数が0件のキーワードがあるときに指摘する
    response = Manifestation.search do
      fulltext keyword if keyword
      with(:required_role_id).less_than_or_equal_to current_role_id
      order_by(:random)
      paginate page: 1, per_page: 1
    end
    response.results.first
  end

  def extract_text
    return unless attachment.attached?
    return unless ENV['ENJU_LEAF_EXTRACT_TEXT'] == 'true'

    if ENV['ENJU_LEAF_EXTRACT_FILESIZE_LIMIT'].present?
      filesize_limit = ENV['ENJU_LEAF_EXTRACT_FILESIZE_LIMIT'].to_i
    else
      filesize_limit = 2097152
    end

    if attachment.byte_size > filesize_limit
      Rails.logger.error("#{attachment.filename} (size: #{attachment.byte_size} byte(s)) exceeds filesize limit #{ENV['ENJU_LEAF_EXTRACT_FILESIZE_LIMIT']} bytes")
      return ''
    end

    client = Faraday.new(url: ENV['TIKA_URL'] || 'http://tika:9998') do |conn|
      conn.adapter :net_http
    end

    response = client.put('/tika/text') do |req|
      req.headers['Content-Type'] = attachment.content_type
      req.headers['Content-Length'] = attachment.byte_size.to_s
      req.body = Faraday::UploadIO.new(StringIO.new(attachment.download), attachment.content_type)
    end

    payload = JSON.parse(response.body)['X-TIKA:content'].strip.tr("\t", " ").gsub(/\r?\n/, "")
    payload
  end

  def created(agent)
    creates.find_by(agent_id: agent.id)
  end

  def realized(agent)
    realizes.find_by(agent_id: agent.id)
  end

  def produced(agent)
    produces.find_by(agent_id: agent.id)
  end

  def sort_title
    if series_master?
      if root_series_statement.title_transcription?
        NKF.nkf('-w --katakana', root_series_statement.title_transcription)
      else
        root_series_statement.original_title
      end
    elsif title_transcription?
      NKF.nkf('-w --katakana', title_transcription)
      else
        original_title
    end
  end

  def self.find_by_isbn(isbn)
    IsbnRecord.find_by(body: isbn)&.manifestations
  end

  def index_series_statement
    series_statements.map{|s| s.index
                              s.root_manifestation&.index}
  end

  def acquired_at
    items.order(:acquired_at).first.try(:acquired_at)
  end

  def series_master?
    return true if root_series_statement

    false
  end

  def web_item
    items.find_by(shelf_id: Shelf.web.id)
  end

  def set_agent_role_type(agent_lists, options = {scope: :creator})
    agent_lists.each do |agent_list|
      name_and_role = agent_list[:full_name].split('||')
      if agent_list[:agent_identifier].present?
        agent = Agent.find_by(agent_identifier: agent_list[:agent_identifier])
      end
      agent ||= Agent.find_by(full_name: name_and_role[0])
      next unless agent

      type = name_and_role[1].to_s.strip

      case options[:scope]
      when :creator
        type = 'author' if type.blank?
        role_type = CreateType.find_by(name: type)
        create = Create.find_by(work_id: id, agent_id: agent.id)
        if create
          create.create_type = role_type
          create.save(validate: false)
        end
      when :publisher
        type = 'publisher' if role_type.blank?
        produce = Produce.find_by(manifestation_id: id, agent_id: agent.id)
        if produce
          produce.produce_type = ProduceType.find_by(name: type)
          produce.save(validate: false)
        end
      else
        raise "#{options[:scope]} is not supported!"
      end
    end
  end

  def set_number
    self.volume_number = volume_number_string.scan(/\d*/).map{|s| s.to_i if s =~ /\d/}.compact.first if volume_number_string && !volume_number?
    self.issue_number = issue_number_string.scan(/\d*/).map{|s| s.to_i if s =~ /\d/}.compact.first if issue_number_string && !issue_number?
    self.edition = edition_string.scan(/\d*/).map{|s| s.to_i if s =~ /\d/}.compact.first if edition_string && !edition?
  end

  def pub_dates
    return [] unless pub_date

    pub_date_array = pub_date.split(';')
    pub_date_array.map{|pub_date_string|
      date = nil
      while date.nil? do
        pub_date_string += '-01'
        break if pub_date_string =~ /-01-01-01$/

        begin
          date = Time.zone.parse(pub_date_string)
        rescue ArgumentError
        rescue TZInfo::AmbiguousTime
        end
      end
      date
    }.compact
  end

  def latest_issue
    if series_master?
      derived_manifestations.where.not(date_of_publication: nil).order('date_of_publication DESC').first
    end
  end

  def first_issue
    if series_master?
      derived_manifestations.where.not(date_of_publication: nil).order('date_of_publication DESC').first
    end
  end

  def identifier_contents(name)
    identifiers.id_type(name).order(:position).pluck(:body)
  end

  # CSVのヘッダ
  # @param [String] role 権限
  def self.csv_header(role: 'Guest')
    Manifestation.new.to_hash(role: role).keys
  end

  # CSV出力用のハッシュ
  # @param [String] role 権限
  def to_hash(role: 'Guest')
    record = {
      manifestation_id: id,
      original_title: original_title,
      title_alternative: title_alternative,
      title_transcription: title_transcription,
      statement_of_responsibility: statement_of_responsibility,
      serial: serial,
      manifestation_identifier: manifestation_identifier,
      creator: creates.map{|create|
        if create.create_type
          "#{create.agent.full_name}||#{create.create_type.name}"
        else
          "#{create.agent.full_name}"
        end
      }.join('//'),
      contributor: realizes.map{|realize|
        if realize.realize_type
          "#{realize.agent.full_name}||#{realize.realize_type.name}"
        else
          "#{realize.agent.full_name}"
        end
      }.join('//'),
      publisher: produces.map{|produce|
        if produce.produce_type
          "#{produce.agent.full_name}||#{produce.produce_type.name}"
        else
          "#{produce.agent.full_name}"
        end
      }.join('//'),
      pub_date: pub_date,
      date_of_publication: date_of_publication,
      year_of_publication: year_of_publication,
      publication_place: publication_place,
      manifestation_created_at: created_at,
      manifestation_updated_at: updated_at,
      carrier_type: carrier_type.name,
      content_type: manifestation_content_type.name,
      frequency: frequency.name,
      language: language.name,
      isbn: isbn_records.pluck(:body).join('//'),
      issn: issn_records.pluck(:body).join('//'),
      volume_number: volume_number,
      volume_number_string: volume_number_string,
      edition: edition,
      edition_string: edition_string,
      issue_number: issue_number,
      issue_number_string: issue_number_string,
      serial_number: serial_number,
      extent: extent,
      start_page: start_page,
      end_page: end_page,
      dimensions: dimensions,
      height: height,
      width: width,
      depth: depth,
      manifestation_price: price,
      access_address: access_address,
      manifestation_required_role: required_role.name,
      abstract: abstract,
      description: description,
      note: note
    }

    IdentifierType.find_each do |type|
      next if ['isbn', 'issn', 'jpno', 'ncid', 'lccn', 'doi'].include?(type.name.downcase.strip)

      record[:"identifier:#{type.name.to_sym}"] = identifiers.where(identifier_type: type).pluck(:body).join('//')
    end

    series = series_statements.order(:position)
    record.merge!(
      series_statement_id: series.pluck(:id).join('//'),
      series_statement_original_title: series.pluck(:original_title).join('.//'),
      series_statement_title_subseries: series.pluck(:title_subseries).join('//'),
      series_statement_title_subseries_transcription: series.pluck(:title_subseries_transcription).join('//'),
      series_statement_title_transcription: series.pluck(:title_transcription).join('//'),
      series_statement_creator: series.pluck(:creator_string).join('//'),
      series_statement_volume_number: series.pluck(:volume_number_string).join('//'),
      series_statement_series_master: series.pluck(:series_master).join('//'),
      series_statement_root_manifestation_id: series.pluck(:root_manifestation_id).join('//'),
      series_statement_manifestation_id: series.pluck(:manifestation_id).join('//'),
      series_statement_position: series.pluck(:position).join('//'),
      series_statement_note: series.pluck(:note).join('//'),
      series_statement_created_at: series.pluck(:created_at).join('//'),
      series_statement_updated_at: series.pluck(:updated_at).join('//')
    )

    if ['Administrator', 'Librarian'].include?(role)
      record.merge!({
        memo: memo
      })
      ManifestationCustomProperty.order(:position).each do |custom_property|
        custom_value = manifestation_custom_values.find_by(manifestation_custom_property: custom_property)
        record[:"manifestation:#{custom_property.name}"] = custom_value&.value
      end
    end

    if defined?(EnjuSubject)
      SubjectHeadingType.find_each do |type|
        record[:"subject:#{type.name}"] = subjects.where(subject_heading_type: type).pluck(:term).join('//')
      end
      ClassificationType.find_each do |type|
        record[:"classification:#{type.name}"] = classifications.where(classification_type: type).pluck(:category).map(&:to_s).join('//')
      end
    end

    record["doi"] = doi_record&.body
    record["jpno"] = jpno_record&.body
    record["ncid"] = ncid_record&.body
    record["lccn"] = lccn_record&.body
    record["iss_itemno"] = ndl_bib_id_record&.body

    record
  end

  # TSVでのエクスポート
  # @param [String] role 権限
  # @param [String] col_sep 区切り文字
  def self.export(role: 'Guest', col_sep: "\t")
    file = Tempfile.create('manifestation_export') do |f|
      f.write (Manifestation.csv_header(role: role) + Item.csv_header(role: role)).to_csv(col_sep: col_sep)
      Manifestation.find_each do |manifestation|
        if manifestation.items.exists?
          manifestation.items.each do |item|
            f.write (manifestation.to_hash(role: role).values + item.to_hash(role: role).values).to_csv(col_sep: col_sep)
          end
        else
          f.write manifestation.to_hash(role: role).values.to_csv(col_sep: col_sep)
        end
      end

      f.rewind
      f.read
    end

    file
  end

  def root_series_statement
    series_statements.find_by(root_manifestation_id: id)
  end

  def isbn_characters
    isbn_records.pluck(:body).map{|i|
      isbn10 = isbn13 = isbn10_dash = isbn13_dash = nil
      isbn10 = Lisbn.new(i).isbn10
      isbn13 = Lisbn.new(i).isbn13
      isbn10_dash = Lisbn.new(isbn10).isbn_with_dash if isbn10
      isbn13_dash = Lisbn.new(isbn13).isbn_with_dash if isbn13
      [
        isbn10, isbn13, isbn10_dash, isbn13_dash
      ]
    }.flatten
  end

  def set_custom_property(row)
    ManifestationCustomProperty.find_each do |property|
      if row[property]
        custom_value = ManifestationCustomValue.new(
          manifestation: self,
          manifestation_custom_property: property,
          value: row[property]
        )
      end
    end
  end
end

# == Schema Information
#
# Table name: manifestations
#
#  id                              :bigint           not null, primary key
#  original_title                  :text             not null
#  title_alternative               :text
#  title_transcription             :text
#  classification_number           :string
#  manifestation_identifier        :string
#  date_of_publication             :datetime
#  date_copyrighted                :datetime
#  created_at                      :datetime         not null
#  updated_at                      :datetime         not null
#  access_address                  :string
#  language_id                     :bigint           default(1), not null
#  carrier_type_id                 :bigint           default(1), not null
#  start_page                      :integer
#  end_page                        :integer
#  height                          :integer
#  width                           :integer
#  depth                           :integer
#  price                           :integer
#  fulltext                        :text
#  volume_number_string            :string
#  issue_number_string             :string
#  serial_number_string            :string
#  edition                         :integer
#  note                            :text
#  repository_content              :boolean          default(FALSE), not null
#  lock_version                    :integer          default(0), not null
#  required_role_id                :bigint           default(1), not null
#  required_score                  :integer          default(0), not null
#  frequency_id                    :bigint           default(1), not null
#  subscription_master             :boolean          default(FALSE), not null
#  nii_type_id                     :bigint
#  title_alternative_transcription :text
#  description                     :text
#  abstract                        :text
#  available_at                    :datetime
#  valid_until                     :datetime
#  date_submitted                  :datetime
#  date_accepted                   :datetime
#  date_captured                   :datetime
#  pub_date                        :string
#  edition_string                  :string
#  volume_number                   :integer
#  issue_number                    :integer
#  serial_number                   :integer
#  content_type_id                 :bigint           default(1)
#  year_of_publication             :integer
#  month_of_publication            :integer
#  fulltext_content                :boolean
#  serial                          :boolean
#  statement_of_responsibility     :text
#  publication_place               :text
#  extent                          :text
#  dimensions                      :text
#  memo                            :text
#