1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
class Asset < ApplicationRecord
IMAGE_CONTENT_TYPES = ["image/gif", "image/jpeg", "image/png", "image/webp"]
PDF_CONTENT_TYPE = "application/pdf"
include FileAttachment
has_many :related_assets, :dependent => :destroy
has_many :pages, :through => :related_assets
scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) }
scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) }
scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) }
scope :pdfs, -> { where(:upload_content_type => PDF_CONTENT_TYPE) }
scope :headline_eligible, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES + [PDF_CONTENT_TYPE]) }
validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true
def image?
IMAGE_CONTENT_TYPES.include?(upload_content_type)
end
def pdf?
upload_content_type == PDF_CONTENT_TYPE
end
def has_credit?
creator.present? || source_url.present? || license_key.present?
end
def show_credit?
image? && has_credit?
end
# Nodes whose current lifecycle rows (head, draft or autosave) carry
# this asset. Historical revisions also hold RelatedAsset rows; they
# are excluded by construction here, since nothing points at them.
def attached_nodes
page_ids = related_assets.select(:page_id)
Node.where("head_id IN (:ids) OR draft_id IN (:ids) OR autosave_id IN (:ids)",
:ids => page_ids).distinct
end
# An asset's reach is the reach of the pages carrying it: destroying one
# removes it from every live page at once, so a single restricted
# attachment makes the destruction a restricted act.
def restricted?
attached_nodes.any?(&:restricted?)
end
# Witnessed destruction. Destroying an asset is a public-facing act
# even when unattached. The original and its variants are publicly
# reachable under /system/uploads, so an entry is always written,
# before the row and its files die. Every currently-attached node
# participates so its zoomed history shows the loss; the asset itself
# participates as the first non-Node subject (its participant row
# dangles after destroy, by design, the name lives on in metadata).
def destroy_witnessed! user:
if user && !user.may_change_live?(self)
errors.add(:base, :not_permitted)
raise ActiveRecord::RecordInvalid.new(self)
end
ActiveRecord::Base.transaction do
affected = attached_nodes.to_a
headline_losses = affected.select do |node|
[node.head, node.draft, node.autosave].compact.any? do |row|
row.related_assets.exists?(:asset_id => id, :headline => true)
end
end
metadata = {
:asset_name => name,
:content_type => upload_content_type,
:path => upload.url.sub(/\?\d+$/, ""),
}
metadata[:detached_from] = affected.map(&:unique_name) if affected.any?
metadata[:headline_removed_from] = headline_losses.map(&:unique_name) if headline_losses.any?
NodeAction.record!(:participants => [self] + affected, :user => user,
:action => "asset_destroy", **metadata)
destroy!
end
end
def self.editor_search(term)
words = term.to_s.split(/\s+/).reject(&:blank?)
return none if words.empty?
words.inject(all) do |scope, word|
like = "%#{sanitize_sql_like(word)}%"
scope.where(
"assets.name ILIKE :t OR assets.creator ILIKE :t OR " \
"assets.upload_file_name ILIKE :t OR assets.source_url ILIKE :t OR " \
"assets.upload_content_type ILIKE :t", :t => like
)
end
end
end
|