This repository has been archived by the owner on Mar 11, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 162
/
annotation.rb
280 lines (230 loc) · 8.27 KB
/
annotation.rb
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
class Annotation < ActiveRecord::Base
include DC::Store::DocumentResource
include DC::Access
belongs_to :document
belongs_to :account # NB: This account is not the owner of the document.
# Rather, it is the author of the annotation.
belongs_to :organization
has_many :project_memberships, :through => :document
attr_accessor :author
validates :title, :page_number, :presence=>true
before_validation :ensure_title
after_create :reset_public_note_count
after_destroy :reset_public_note_count
# Sanitizations:
text_attr :title
html_attr :content, :level=>:super_relaxed
scope :accessible, lambda { |account|
has_shared = account && account.accessible_project_ids.present?
# Notes accessible under the following circumstances:
access = []
joins = []
# A note is public
access << "(annotations.access in (#{PUBLIC_LEVELS.join(",")}))"
if account
# A note belongs to the accessing account
access << "(annotations.access = #{PRIVATE} and annotations.account_id = #{account.id})"
# An draft (EXCLUSIVE) note and the accessing account belong to the same organization
joins << <<-EOS
left outer join memberships on
(annotations.organization_id = memberships.organization_id and
memberships.account_id = #{account.id})
EOS
access << "(annotations.access = #{EXCLUSIVE} and annotations.organization_id = memberships.organization_id)"
end
if has_shared
# A draft (EXCLUSIVE) note is on a document shared with the accessing account
access << "((annotations.access = #{EXCLUSIVE}) and projects.document_id = annotations.document_id)"
joins << <<-EOS
left outer join
(select distinct document_id from project_memberships
where project_id in (#{account.accessible_project_ids.join(',')})) as projects
on projects.document_id = annotations.document_id
EOS
end
where( "(#{access.join(' or ')})" ).joins( joins.join("\n") ).readonly(false)
}
scope :owned_by, lambda { |account|
where( :account_id => account.id )
}
scope :unrestricted, lambda{ where( :access => PUBLIC_LEVELS ) }
# Annotations are not indexed for the time being.
# searchable do
# text :title, :boost => 2.0
# text :content
#
# integer :document_id
# integer :account_id
# integer :organization_id
# integer :access
# time :created_at
# end
def self.counts_for_documents(account, docs)
doc_ids = docs.map {|doc| doc.id }
self.accessible(account).where({:document_id => doc_ids}).group('annotations.document_id').count
end
def self.populate_author_info(notes, current_account=nil)
return if notes.empty?
account_sql = <<-EOS
SELECT DISTINCT accounts.id, accounts.first_name, accounts.last_name,
organizations.name as organization_name
FROM accounts
INNER JOIN annotations ON annotations.account_id = accounts.id
INNER JOIN organizations ON organizations.id = annotations.organization_id
WHERE annotations.id in (#{notes.map(&:id).join(',')})
EOS
rows = Account.connection.select_all(account_sql)
account_map = rows.inject({}) do |memo, acc|
memo[acc['id'].to_i] = acc unless acc.nil?
memo
end
notes.each do |note|
author = account_map[note.account_id]
note.author = {
:full_name => author ? "#{author['first_name']} #{author['last_name']}" : "Unattributed",
:account_id => note.account_id,
:owns_note => current_account && current_account.id == note.account_id,
:organization_name => author ? author['organization_name'] : nil
}
end
end
def self.public_note_counts_by_organization
self.unrestricted
.joins(:document)
.where(["documents.access in (?)", PUBLIC_LEVELS])
.group('annotations.organization_id')
.count
end
# The interface prefills the note title with "Untitled Note", so that gets
# saved to the database as the note's title. In certain interfaces (e.g.,
# Twitter cards) we only want to surface titles that a user actually provided.
def user_provided_title
(title.blank? || title == 'Untitled Note') ? '' : title
end
def page
document.pages.find_by_page_number(page_number)
end
def access_name
ACCESS_NAMES[access]
end
def public?
PUBLIC_LEVELS.include?(access)
end
def private?
access == PRIVATE
end
def draft?
access == EXCLUSIVE
end
def cacheable?
public? && document.cacheable?
end
def coordinates
return nil unless location
coords = location.split(',').map { |loc| loc.to_i }
transform_coordinates_to_legacy({
top: coords[0],
left: coords[3],
right: coords[1],
height: coords[2] - coords[0],
width: coords[1] - coords[3],
})
end
def embed_dimensions
return nil unless coords = coordinates
page_width = Page::IMAGE_SIZES['normal'].gsub(/x$/, '').to_i
{
aspect_ratio: 1.0 * coords[:width] / coords[:height],
inverted_aspect_ratio: 1.0 * coords[:height] / coords[:width],
height_pixel: coords[:height],
width_pixel: coords[:width],
width_percent: 100.0 * page_width / coords[:width],
offset_top_percent: -100.0 * coords[:top] / coords[:height],
offset_left_percent: -100.0 * coords[:left] / coords[:width],
}
end
def embed_classes
classes = []
classes.push 'draft' if draft?
classes.push 'private' if private?
classes.map{ |cls| "DC-note-#{cls}" }.join(' ')
end
# `contextual` means "show this thing in the context of its document parent",
# which right now correlates to its page-anchored version.
def contextual_url
File.join(DC.server_root, contextual_path)
end
def contextual_path
"#{document.canonical_path(:html)}\#document/p#{page_number}/a#{id}"
end
def canonical_url(format = :json, allow_ssl = true)
File.join(DC.server_root(:ssl => allow_ssl), canonical_path(format))
end
def canonical_path(format = :json)
"/documents/#{document.canonical_id}/annotations/#{id}.#{format}"
end
def iframe_embed_src_url(options={})
options.merge!(embed: true)
"#{canonical_url(:html)}?#{options.to_query}"
end
def oembed_url
"#{DC.server_root}/api/oembed.json?url=#{CGI.escape(self.canonical_url(:html))}"
end
def canonical_js_cache_path
canonical_path(:js)
end
# Effective duplicate of `canonical_path()` for explicitness
def canonical_json_cache_path
canonical_path(:json)
end
def cache_paths
[canonical_js_cache_path, canonical_json_cache_path]
end
def anchored_published_url
"#{document.published_url}\#document/p#{page_number}/a#{id}"
end
def page_image_url(size: 'normal')
document.page_image_url_template.gsub('{page}', page_number.to_s).gsub('{size}', size)
end
def canonical(opts={})
data = {'id' => id, 'page' => page_number, 'title' => title, 'content' => content, 'access' => access_name.to_s }
data['location'] = {'image' => location} if location
data['image_url'] = document.page_image_url_template if opts[:include_image_url]
data['published_url'] = document.published_url || document.canonical_url(:html) if opts[:include_document_url]
data['canonical_url'] = canonical_url(:html)
data['resource_url'] = canonical_url(:js)
data['account_id'] = account_id if [PREMODERATED, POSTMODERATED].include? document.access
if author
data.merge!({
'author' => author[:full_name],
'owns_note' => author[:owns_note],
'author_organization' => author[:organization_name]
})
end
data
end
def reset_public_note_count
document.reset_public_note_count
end
def as_json(opts={})
canonical.merge({
'document_id' => document_id,
'account_id' => account_id,
'organization_id' => organization_id
})
end
private
# For unknown reasons, we do this
def transform_coordinates_to_legacy(coords)
{
top: coords[:top] + 1,
left: coords[:left] - 2,
right: coords[:right] ,
height: coords[:height] ,
width: coords[:width] - 8,
}
end
def ensure_title
self.title = "Untitled Annotation" if title.blank?
end
end