class Material < ApplicationRecord include MyDiscard SOURCE_KINDS = ['uri', 'google_drive_path', 'google_drive_file', 'legacy_drive_path'].freeze default_scope -> { kept } belongs_to :parent, class_name: 'Material', optional: true has_many :children, class_name: 'Material', foreign_key: :parent_id, dependent: :nullify belongs_to :tag, optional: true belongs_to :created_by_user, class_name: 'User', optional: true belongs_to :updated_by_user, class_name: 'User', optional: true has_many :material_versions, dependent: :destroy has_many :material_export_items, dependent: :destroy has_one_attached :file, dependent: :purge has_one_attached :thumbnail, dependent: :purge before_validation :assign_normalized_source_key validates :tag_id, uniqueness: true, allow_nil: true validates :source_kind, inclusion: { in: SOURCE_KINDS }, allow_blank: true validate :file_must_be_attached validate :tag_must_be_material_category def content_type return nil unless file&.attached? file.blob.content_type end def file_byte_size return nil unless file&.attached? file.blob.byte_size end def file_filename return nil unless file&.attached? file.blob.filename.to_s end def snapshot_export_paths material_export_items.order(:profile).pluck(:profile, :export_path).to_h end private def assign_normalized_source_key if source_kind.blank? self.normalized_source_key = nil return end self.normalized_source_key = MaterialSyncSuppression.normalize_source_key(source_kind:, source_uri:, drive_path: source_path, drive_file_id: source_file_id) end def file_must_be_attached return if url.present? || file.attached? errors.add(:url, 'URL かファイルのどちらかは必須です.') end def tag_must_be_material_category return if tag.blank? || tag.character? || tag.material? errors.add(:tag, '素材カテゴリのタグを指定してください.') end end