activeadmin_annotations 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/CODE_OF_CONDUCT.md +31 -0
- data/CONTRIBUTING.md +29 -0
- data/GOVERNANCE.md +29 -0
- data/LICENSE.md +21 -0
- data/README.md +321 -0
- data/SECURITY.md +27 -0
- data/activeadmin_annotations.gemspec +68 -0
- data/admin/annotation_reviews.rb +102 -0
- data/admin/annotations.rb +146 -0
- data/app/assets/controllers/activeadmin_annotations/annotator_controller.js +483 -0
- data/app/assets/controllers/activeadmin_annotations/clipboard_controller.js +73 -0
- data/app/assets/stylesheets/activeadmin_annotations.css +362 -0
- data/app/helpers/active_admin/annotations/panel_helper.rb +95 -0
- data/app/models/active_admin/annotations/annotation.rb +37 -0
- data/app/models/active_admin/annotations/application_record.rb +13 -0
- data/app/models/active_admin/annotations/review.rb +54 -0
- data/app/services/active_admin/annotations/review_service.rb +133 -0
- data/app/views/active_admin/annotations/_copy_details_action.html.erb +21 -0
- data/app/views/active_admin/annotations/_panel.html.erb +98 -0
- data/app/views/active_admin/annotations/_read_only_content.html.erb +3 -0
- data/config/polyrun_coverage.yml +8 -0
- data/db/migrate/20260617120000_create_activeadmin_annotations_tables.rb +41 -0
- data/db/migrate/20260706150000_add_metadata_json_gin_index_to_annotation_reviews.rb +9 -0
- data/db/migrate/20260710120000_add_panel_query_index_to_annotation_spans.rb +10 -0
- data/lib/activeadmin/annotations/annotatable.rb +15 -0
- data/lib/activeadmin/annotations/categories.rb +13 -0
- data/lib/activeadmin/annotations/content_revision.rb +101 -0
- data/lib/activeadmin/annotations/context.rb +11 -0
- data/lib/activeadmin/annotations/copy_text.rb +98 -0
- data/lib/activeadmin/annotations/engine.rb +49 -0
- data/lib/activeadmin/annotations/exporter.rb +99 -0
- data/lib/activeadmin/annotations/review_access.rb +29 -0
- data/lib/activeadmin/annotations/review_panel.rb +145 -0
- data/lib/activeadmin/annotations/version.rb +7 -0
- data/lib/activeadmin/annotations.rb +37 -0
- data/lib/activeadmin_annotations.rb +13 -0
- data/sig/activeadmin/annotations.rbs +5 -0
- metadata +393 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<% annotations_payload = annotations.map do |annotation|
|
|
2
|
+
{
|
|
3
|
+
id: annotation.id,
|
|
4
|
+
field_name: annotation.field_name,
|
|
5
|
+
selected_text: annotation.selected_text,
|
|
6
|
+
start_offset: annotation.start_offset,
|
|
7
|
+
end_offset: annotation.end_offset,
|
|
8
|
+
comment: annotation.comment,
|
|
9
|
+
category: annotation.category
|
|
10
|
+
}
|
|
11
|
+
end %>
|
|
12
|
+
<% category_options = categories.to_h %>
|
|
13
|
+
<% panel_id = "aa-annotation-#{review.id}" %>
|
|
14
|
+
|
|
15
|
+
<%= stylesheet_link_tag "activeadmin_annotations", "data-turbo-track": "reload" %>
|
|
16
|
+
|
|
17
|
+
<div class="aa-annotations-panel">
|
|
18
|
+
<% if content_stale && stale_message.present? %>
|
|
19
|
+
<div class="aa-annotations-stale-banner" role="status">
|
|
20
|
+
<p><%= stale_message %></p>
|
|
21
|
+
<% if advance_review_url.present? %>
|
|
22
|
+
<%= button_to "Review latest version",
|
|
23
|
+
advance_review_url,
|
|
24
|
+
method: :post,
|
|
25
|
+
class: "action-item-button" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
class="aa-annotations-layout"
|
|
32
|
+
data-controller="activeadmin-annotations--annotator"
|
|
33
|
+
data-activeadmin-annotations--annotator-review-id-value="<%= review.id %>"
|
|
34
|
+
data-activeadmin-annotations--annotator-field-value="<%= field %>"
|
|
35
|
+
data-activeadmin-annotations--annotator-category-label-value="<%= category_label %>"
|
|
36
|
+
data-activeadmin-annotations--annotator-categories-value="<%= ERB::Util.html_escape(category_options.to_json) %>"
|
|
37
|
+
data-activeadmin-annotations--annotator-create-url-value="<%= admin_annotation_spans_path(format: :json) %>"
|
|
38
|
+
data-activeadmin-annotations--annotator-update-url-template-value="<%= admin_annotation_span_path(":id", format: :json) %>"
|
|
39
|
+
data-activeadmin-annotations--annotator-annotations-value="<%= ERB::Util.html_escape(annotations_payload.to_json) %>">
|
|
40
|
+
<div class="aa-annotations-main">
|
|
41
|
+
<div class="aa-annotations-output">
|
|
42
|
+
<div
|
|
43
|
+
class="aa-annotations-annotatable prose"
|
|
44
|
+
data-activeadmin-annotations--annotator-target="annotatable">
|
|
45
|
+
<%= content %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<% if context_panels.any? %>
|
|
50
|
+
<div class="aa-annotations-context">
|
|
51
|
+
<% context_panels.each do |panel| %>
|
|
52
|
+
<details class="aa-annotations-context-panel">
|
|
53
|
+
<summary><%= panel.fetch(:title) %></summary>
|
|
54
|
+
<pre><%= panel.fetch(:content) %></pre>
|
|
55
|
+
</details>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<aside class="aa-annotations-sidebar" data-activeadmin-annotations--annotator-target="sidebar">
|
|
62
|
+
<h4>Annotations</h4>
|
|
63
|
+
<p class="aa-annotations-hint">Select text in the block, then add a comment.</p>
|
|
64
|
+
<% if content_revision_enabled && content_revision_label.present? %>
|
|
65
|
+
<p class="aa-annotations-version"><%= content_revision_label %></p>
|
|
66
|
+
<% end %>
|
|
67
|
+
|
|
68
|
+
<div class="aa-annotations-composer aa-annotations-composer-hidden" data-activeadmin-annotations--annotator-target="composer">
|
|
69
|
+
<h4 data-activeadmin-annotations--annotator-target="composerHeading">New annotation</h4>
|
|
70
|
+
<p class="aa-annotations-selected-preview" data-activeadmin-annotations--annotator-target="selectedPreview"></p>
|
|
71
|
+
<label for="<%= panel_id %>-comment">Comment</label>
|
|
72
|
+
<textarea
|
|
73
|
+
id="<%= panel_id %>-comment"
|
|
74
|
+
rows="4"
|
|
75
|
+
data-activeadmin-annotations--annotator-target="commentInput"
|
|
76
|
+
data-action="keydown.meta+enter->activeadmin-annotations--annotator#submitComposer keydown.ctrl+enter->activeadmin-annotations--annotator#submitComposer"></textarea>
|
|
77
|
+
<% if category_options.any? %>
|
|
78
|
+
<label for="<%= panel_id %>-category"><%= category_label %></label>
|
|
79
|
+
<select id="<%= panel_id %>-category" data-activeadmin-annotations--annotator-target="categoryInput">
|
|
80
|
+
<option value="">None</option>
|
|
81
|
+
<% category_options.each do |value, label| %>
|
|
82
|
+
<option value="<%= value %>"><%= label %></option>
|
|
83
|
+
<% end %>
|
|
84
|
+
</select>
|
|
85
|
+
<% else %>
|
|
86
|
+
<input type="hidden" value="" data-activeadmin-annotations--annotator-target="categoryInput">
|
|
87
|
+
<% end %>
|
|
88
|
+
<div class="aa-annotations-composer-actions">
|
|
89
|
+
<button type="button" class="action-item-button aa-annotations-save-button" data-action="activeadmin-annotations--annotator#submitComposer">Save annotation</button>
|
|
90
|
+
<button type="button" class="action-item-button" data-action="activeadmin-annotations--annotator#cancelComposer">Cancel</button>
|
|
91
|
+
</div>
|
|
92
|
+
<p class="aa-annotations-error aa-annotations-hidden" data-activeadmin-annotations--annotator-target="errorMessage"></p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="aa-annotations-list" data-activeadmin-annotations--annotator-target="annotationList"></div>
|
|
96
|
+
</aside>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateActiveadminAnnotationsTables < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :annotation_reviews, id: :uuid do |t|
|
|
6
|
+
t.references :subject, polymorphic: true, null: false, type: :uuid, index: true
|
|
7
|
+
t.uuid :reviewer_id, null: false
|
|
8
|
+
t.string :review_status, null: false, default: "pending"
|
|
9
|
+
t.text :notes
|
|
10
|
+
t.jsonb :context_json, null: false, default: {}
|
|
11
|
+
t.string :context_digest
|
|
12
|
+
t.integer :content_revision_version, null: false, default: 0
|
|
13
|
+
t.jsonb :metadata_json, null: false, default: {}
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :annotation_reviews, %i[subject_type subject_id reviewer_id], unique: true, name: "index_annotation_reviews_on_subject_and_reviewer"
|
|
19
|
+
add_index :annotation_reviews, :review_status
|
|
20
|
+
add_index :annotation_reviews, :context_digest
|
|
21
|
+
add_index :annotation_reviews, :reviewer_id
|
|
22
|
+
|
|
23
|
+
create_table :annotation_spans, id: :uuid do |t|
|
|
24
|
+
t.references :annotation_review, null: false, type: :uuid, foreign_key: {to_table: :annotation_reviews}, index: true
|
|
25
|
+
t.string :field_name, null: false
|
|
26
|
+
t.text :selected_text, null: false
|
|
27
|
+
t.integer :start_offset, null: false
|
|
28
|
+
t.integer :end_offset, null: false
|
|
29
|
+
t.text :comment, null: false
|
|
30
|
+
t.string :category
|
|
31
|
+
t.integer :content_revision_version, null: false, default: 0
|
|
32
|
+
t.jsonb :context_paths_json, null: false, default: []
|
|
33
|
+
t.jsonb :metadata_json, null: false, default: {}
|
|
34
|
+
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
add_index :annotation_spans, :category
|
|
39
|
+
add_index :annotation_spans, :field_name
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMetadataJsonGinIndexToAnnotationReviews < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
return unless connection.adapter_name.match?(/postgres/i)
|
|
6
|
+
|
|
7
|
+
add_index :annotation_reviews, :metadata_json, using: :gin, if_not_exists: true
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddPanelQueryIndexToAnnotationSpans < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
add_index :annotation_spans,
|
|
6
|
+
%i[annotation_review_id field_name content_revision_version],
|
|
7
|
+
name: "index_annotation_spans_on_review_field_and_revision",
|
|
8
|
+
if_not_exists: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin::Annotations
|
|
4
|
+
module Annotatable
|
|
5
|
+
extend ::ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
has_many :annotation_reviews,
|
|
9
|
+
as: :subject,
|
|
10
|
+
class_name: "ActiveAdmin::Annotations::Review",
|
|
11
|
+
dependent: :destroy,
|
|
12
|
+
inverse_of: :subject
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin::Annotations
|
|
4
|
+
module ContentRevision
|
|
5
|
+
class << self
|
|
6
|
+
def enabled_for?(subject)
|
|
7
|
+
case ActiveAdmin::Annotations.content_revision_strategy
|
|
8
|
+
when false, :none
|
|
9
|
+
false
|
|
10
|
+
when true, :active_version
|
|
11
|
+
active_version_subject?(subject) || custom_version_resolver?
|
|
12
|
+
else
|
|
13
|
+
custom_version_resolver? || active_version_subject?(subject)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def current_version_for(subject)
|
|
18
|
+
if custom_current_version
|
|
19
|
+
return custom_current_version.call(subject).to_i
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
return subject.current_version.to_i if active_version_subject?(subject)
|
|
23
|
+
|
|
24
|
+
0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def content_subject_for(subject, version)
|
|
28
|
+
if custom_content_subject
|
|
29
|
+
return custom_content_subject.call(subject, version.to_i)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if active_version_subject?(subject)
|
|
33
|
+
return active_version_content_subject(subject, version.to_i)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
subject
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stale_review_message(review:, pinned_version:, latest_version:)
|
|
40
|
+
message = ActiveAdmin::Annotations.stale_content_review_message
|
|
41
|
+
return if message.blank?
|
|
42
|
+
|
|
43
|
+
if message.respond_to?(:call)
|
|
44
|
+
return message.call(review, pinned_version:, latest_version:)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
format(message.to_s, pinned_version:, latest_version:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stale_context_message(review:)
|
|
51
|
+
message = ActiveAdmin::Annotations.stale_context_message
|
|
52
|
+
return if message.blank?
|
|
53
|
+
|
|
54
|
+
return message.call(review) if message.respond_to?(:call)
|
|
55
|
+
|
|
56
|
+
message.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def version_label(version)
|
|
60
|
+
label = ActiveAdmin::Annotations.content_revision_label
|
|
61
|
+
return if label == false
|
|
62
|
+
|
|
63
|
+
if label.respond_to?(:call)
|
|
64
|
+
return label.call(version.to_i)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
format((label || "Content version %{version}").to_s, version: version.to_i)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def custom_version_resolver?
|
|
73
|
+
custom_current_version.present? || custom_content_subject.present?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def custom_current_version
|
|
77
|
+
ActiveAdmin::Annotations.current_content_revision_version
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def custom_content_subject
|
|
81
|
+
ActiveAdmin::Annotations.content_subject_for_revision
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def active_version_subject?(subject)
|
|
85
|
+
subject.class.respond_to?(:has_revisions?) &&
|
|
86
|
+
subject.class.has_revisions? &&
|
|
87
|
+
subject.respond_to?(:current_version)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def active_version_content_subject(subject, requested_version)
|
|
91
|
+
head_version = subject.current_version.to_i
|
|
92
|
+
return subject if requested_version == head_version
|
|
93
|
+
|
|
94
|
+
snapshot_version = requested_version + 1
|
|
95
|
+
return subject.at_version(snapshot_version) if subject.respond_to?(:at_version) && subject.at_version(snapshot_version)
|
|
96
|
+
|
|
97
|
+
subject
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin::Annotations
|
|
6
|
+
class CopyText
|
|
7
|
+
def self.for(annotation)
|
|
8
|
+
new(annotation).to_s
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(annotation)
|
|
12
|
+
@annotation = annotation
|
|
13
|
+
@review = annotation.annotation_review
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_s
|
|
17
|
+
sections = []
|
|
18
|
+
instructions = copy_instructions
|
|
19
|
+
sections << instructions if instructions.present?
|
|
20
|
+
sections << review_section
|
|
21
|
+
sections << context_section
|
|
22
|
+
sections << annotation_section
|
|
23
|
+
sections.join("\n\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :annotation, :review
|
|
29
|
+
|
|
30
|
+
def copy_instructions
|
|
31
|
+
instructions = ActiveAdmin::Annotations.copy_instructions
|
|
32
|
+
return if instructions.blank?
|
|
33
|
+
return instructions.call(annotation, review) if instructions.respond_to?(:call)
|
|
34
|
+
|
|
35
|
+
instructions.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def review_section
|
|
39
|
+
lines = [
|
|
40
|
+
"Review",
|
|
41
|
+
"Subject: #{review.subject_type} ##{review.subject_id}",
|
|
42
|
+
"Status: #{review.review_status}",
|
|
43
|
+
"Reviewer ID: #{review.reviewer_id}",
|
|
44
|
+
"Review ID: #{review.id}"
|
|
45
|
+
]
|
|
46
|
+
if content_revision_enabled?
|
|
47
|
+
lines.insert(2, "Content revision version: #{review.content_revision_version}")
|
|
48
|
+
end
|
|
49
|
+
lines << "Notes: #{review.notes}" if review.notes.present?
|
|
50
|
+
lines << "Context digest: #{review.context_digest}" if review.context_digest.present?
|
|
51
|
+
lines.join("\n")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def context_section
|
|
55
|
+
"Context:\n#{JSON.pretty_generate(review.context_json.to_h)}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def annotation_section
|
|
59
|
+
category_label = ActiveAdmin::Annotations.category_label
|
|
60
|
+
category_value = category_label_for(annotation.category)
|
|
61
|
+
context_paths = Array(annotation.context_paths_json)
|
|
62
|
+
|
|
63
|
+
lines = [
|
|
64
|
+
"Annotation",
|
|
65
|
+
"Annotation ID: #{annotation.id}",
|
|
66
|
+
"Field: #{annotation.field_name}",
|
|
67
|
+
"Selected text:\n#{annotation.selected_text}",
|
|
68
|
+
"Comment:\n#{annotation.comment}",
|
|
69
|
+
"#{category_label}: #{category_value}",
|
|
70
|
+
"Span: #{annotation.start_offset}-#{annotation.end_offset}"
|
|
71
|
+
]
|
|
72
|
+
lines << if context_paths.any?
|
|
73
|
+
"Context paths: #{context_paths.join(", ")}"
|
|
74
|
+
else
|
|
75
|
+
"Context paths: (none)"
|
|
76
|
+
end
|
|
77
|
+
lines << "Created: #{annotation.created_at&.iso8601}" if annotation.created_at
|
|
78
|
+
lines << "Updated: #{annotation.updated_at&.iso8601}" if annotation.updated_at
|
|
79
|
+
if content_revision_enabled?
|
|
80
|
+
lines.insert(2, "Content revision version: #{annotation.content_revision_version}")
|
|
81
|
+
end
|
|
82
|
+
lines.join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def content_revision_enabled?
|
|
86
|
+
subject = review.subject
|
|
87
|
+
return false if subject.blank?
|
|
88
|
+
|
|
89
|
+
ContentRevision.enabled_for?(subject)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def category_label_for(value)
|
|
93
|
+
return "(none)" if value.blank?
|
|
94
|
+
|
|
95
|
+
ActiveAdmin::Annotations.categories[value] || value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module Annotations
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
engine_name "activeadmin_annotations"
|
|
7
|
+
|
|
8
|
+
isolate_namespace ActiveAdmin::Annotations
|
|
9
|
+
|
|
10
|
+
config.generators do |generator|
|
|
11
|
+
generator.test_framework :rspec
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer "activeadmin_annotations.active_admin", after: :load_config_initializers do
|
|
15
|
+
next unless defined?(ActiveAdmin) && ActiveAdmin.respond_to?(:application)
|
|
16
|
+
|
|
17
|
+
ActiveAdmin.application.load_paths << ActiveAdmin::Annotations::Engine.root.join("admin").to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
initializer "activeadmin_annotations.importmap", after: :load_config_initializers do
|
|
21
|
+
next unless Rails.application.respond_to?(:importmap)
|
|
22
|
+
|
|
23
|
+
pin_controller = proc do |importmap|
|
|
24
|
+
importmap.pin "controllers/activeadmin_annotations/annotator_controller",
|
|
25
|
+
to: "activeadmin_annotations/annotator_controller.js"
|
|
26
|
+
importmap.pin "controllers/activeadmin_annotations/clipboard_controller",
|
|
27
|
+
to: "activeadmin_annotations/clipboard_controller.js"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Rails.application.importmap.draw(&pin_controller)
|
|
31
|
+
ActiveAdmin.importmap.draw(&pin_controller) if defined?(ActiveAdmin)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
initializer "activeadmin_annotations.assets" do |app|
|
|
35
|
+
assets_path = root.join("app/assets")
|
|
36
|
+
app.config.importmap.cache_sweepers << assets_path.join("controllers") if app.config.respond_to?(:importmap)
|
|
37
|
+
|
|
38
|
+
assets = app.config.assets if app.config.respond_to?(:assets)
|
|
39
|
+
assets.precompile << "activeadmin_annotations.css" if assets&.respond_to?(:precompile)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
initializer "activeadmin_annotations.view_helpers" do
|
|
43
|
+
ActiveSupport.on_load(:action_view) do
|
|
44
|
+
include ActiveAdmin::Annotations::PanelHelper
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin::Annotations
|
|
6
|
+
class Exporter
|
|
7
|
+
BATCH_SIZE = 100
|
|
8
|
+
|
|
9
|
+
def initialize(reviews:)
|
|
10
|
+
@reviews = reviews
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_jsonl
|
|
14
|
+
lines = []
|
|
15
|
+
each_jsonl_row { |row| lines << JSON.generate(row) }
|
|
16
|
+
return "" if lines.empty?
|
|
17
|
+
|
|
18
|
+
"#{lines.join("\n")}\n"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def each_jsonl_row
|
|
22
|
+
return enum_for(:each_jsonl_row) unless block_given?
|
|
23
|
+
|
|
24
|
+
if reviews.is_a?(ActiveRecord::Relation)
|
|
25
|
+
export_relation_in_batches { |row| yield row }
|
|
26
|
+
else
|
|
27
|
+
Array(reviews).each do |review|
|
|
28
|
+
rows_for(review).each { |row| yield row }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :reviews
|
|
36
|
+
|
|
37
|
+
def export_relation_in_batches
|
|
38
|
+
reviews.find_in_batches(batch_size: BATCH_SIZE) do |batch|
|
|
39
|
+
ActiveRecord::Associations::Preloader.new(records: batch, associations: :annotations).call
|
|
40
|
+
batch.each do |review|
|
|
41
|
+
rows_for(review).each { |row| yield row }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def rows_for(review)
|
|
47
|
+
context = review.context_json.to_h
|
|
48
|
+
revision_enabled = ContentRevision.enabled_for?(review.subject)
|
|
49
|
+
annotations_for(review).map do |annotation|
|
|
50
|
+
{
|
|
51
|
+
review_id: review.id,
|
|
52
|
+
annotation_id: annotation.id,
|
|
53
|
+
subject: {
|
|
54
|
+
type: review.subject_type,
|
|
55
|
+
id: review.subject_id
|
|
56
|
+
},
|
|
57
|
+
field: annotation.field_name,
|
|
58
|
+
context: context,
|
|
59
|
+
annotation: annotation_payload(annotation, revision_enabled: revision_enabled),
|
|
60
|
+
review: review_payload(review, revision_enabled: revision_enabled)
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def annotation_payload(annotation, revision_enabled:)
|
|
66
|
+
payload = {
|
|
67
|
+
selected_text: annotation.selected_text,
|
|
68
|
+
comment: annotation.comment,
|
|
69
|
+
category: annotation.category,
|
|
70
|
+
start_offset: annotation.start_offset,
|
|
71
|
+
end_offset: annotation.end_offset,
|
|
72
|
+
context_paths: Array(annotation.context_paths_json)
|
|
73
|
+
}
|
|
74
|
+
payload[:content_revision_version] = annotation.content_revision_version if revision_enabled
|
|
75
|
+
payload
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def annotations_for(review)
|
|
79
|
+
association = review.association(:annotations)
|
|
80
|
+
if association.loaded?
|
|
81
|
+
association.target.sort_by(&:created_at)
|
|
82
|
+
else
|
|
83
|
+
review.annotations.order(:created_at).to_a
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def review_payload(review, revision_enabled:)
|
|
88
|
+
payload = {
|
|
89
|
+
reviewer_id: review.reviewer_id,
|
|
90
|
+
review_status: review.review_status,
|
|
91
|
+
notes: review.notes,
|
|
92
|
+
context_digest: review.context_digest,
|
|
93
|
+
reviewed_at: review.updated_at&.iso8601
|
|
94
|
+
}
|
|
95
|
+
payload[:content_revision_version] = review.content_revision_version if revision_enabled
|
|
96
|
+
payload
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin::Annotations
|
|
4
|
+
module ReviewAccess
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
class NotFound < Error; end
|
|
8
|
+
|
|
9
|
+
class Forbidden < Error; end
|
|
10
|
+
|
|
11
|
+
def self.review_for_span_create!(review_id:, reviewer:)
|
|
12
|
+
raise NotFound, "Review not found" if review_id.blank?
|
|
13
|
+
|
|
14
|
+
review = Review.find_by(id: review_id)
|
|
15
|
+
raise NotFound, "Review not found" unless review
|
|
16
|
+
raise Forbidden, "Review not found" unless reviewer.present? && review.reviewer_id == reviewer.id
|
|
17
|
+
|
|
18
|
+
review
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.authorize_annotation!(annotation:, reviewer:)
|
|
22
|
+
raise Forbidden, "Annotation not found" unless annotation
|
|
23
|
+
raise Forbidden, "Annotation not found" unless reviewer.present? &&
|
|
24
|
+
annotation.annotation_review.reviewer_id == reviewer.id
|
|
25
|
+
|
|
26
|
+
annotation
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|