triannon 1.1.0 → 2.0.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.
@@ -4,29 +4,28 @@ module Triannon
4
4
  # Loads an existing Annotation from the LDP server
5
5
  class LdpLoader
6
6
 
7
-
7
+ # @param [String] root_container the LDP parent container for the annotation
8
8
  # @param [String] id the unique id of the annotation. Can include base_uri prefix or omit it.
9
- def self.load id
10
- l = Triannon::LdpLoader.new id
9
+ def self.load(root_container, id)
10
+ l = Triannon::LdpLoader.new(root_container, id)
11
11
  l.load_anno_container
12
12
  l.load_bodies
13
13
  l.load_targets
14
14
 
15
- oa_graph = Triannon::LdpToOaMapper.ldp_to_oa l.ldp_annotation
15
+ oa_graph = Triannon::LdpToOaMapper.ldp_to_oa(l.ldp_annotation, root_container)
16
16
  oa_graph
17
17
  end
18
18
 
19
- # @deprecated was needed by old annotations#index action, which now redirects to search (2015-04)
20
- def self.find_all
21
- l = Triannon::LdpLoader.new
22
- l.find_all
23
- end
24
-
25
19
  attr_accessor :ldp_annotation
26
20
 
27
21
  # @param [String] id the unique id of the annotation. Can include base_uri prefix or omit it.
28
- def initialize id = nil
22
+ # @param [String] root_container the LDP parent container for the annotation
23
+ def initialize(root_container, id = nil)
29
24
  @id = id
25
+ @root_container = root_container
26
+ if @root_container.blank?
27
+ fail Triannon::LDPContainerError, "Annotations must be in a root container."
28
+ end
30
29
  base_url = Triannon.config[:ldp]['url']
31
30
  base_url.chop! if base_url.end_with?('/')
32
31
  container_path = Triannon.config[:ldp]['uber_container']
@@ -37,18 +36,17 @@ module Triannon
37
36
  end
38
37
  @base_uri = "#{base_url}/#{container_path}"
39
38
  @ldp_annotation = Triannon::AnnotationLdp.new
40
-
41
39
  end
42
40
 
43
41
  # load annotation container object into @ldp_annotation's (our Triannon::AnnotationLdp object) graph
44
42
  def load_anno_container
45
- load_object_into_annotation_graph(@id)
43
+ load_object_into_annotation_graph("#{@root_container}/#{@id}")
46
44
  end
47
45
 
48
46
  # load body objects into @ldp_annotation's (our Triannon::AnnotationLdp object) graph
49
47
  def load_bodies
50
48
  @ldp_annotation.body_uris.each { |body_uri|
51
- body_obj_path = body_uri.to_s.split(@base_uri + '/').last
49
+ body_obj_path = body_uri.to_s.split("#{@base_uri}/").last
52
50
  load_object_into_annotation_graph(body_obj_path)
53
51
  }
54
52
  end
@@ -56,30 +54,11 @@ module Triannon
56
54
  # load target objects into @ldp_annotation's (our Triannon::AnnotationLdp object) graph
57
55
  def load_targets
58
56
  @ldp_annotation.target_uris.each { |target_uri|
59
- target_obj_path = target_uri.to_s.split(@base_uri + '/').last
57
+ target_obj_path = target_uri.to_s.split("#{@base_uri}/").last
60
58
  load_object_into_annotation_graph(target_obj_path)
61
59
  }
62
60
  end
63
61
 
64
- # @return [Array<Triannon::Annotation>] an array of Triannon::Annotation objects with just the id set. Enough info to build the index page
65
- # @deprecated was needed by old annotations#index action, which now redirects to search (2015-04).
66
- def find_all
67
- root_ttl = get_ttl
68
- objs = []
69
-
70
- g = RDF::Graph.new
71
- g.from_ttl root_ttl
72
- root_uri = RDF::URI.new @base_uri
73
- results = g.query [root_uri, RDF::Vocab::LDP.contains, nil]
74
- results.each do |stmt|
75
- # FIXME: can't be last with pair trees in fedora urls - leave broke as this method is deprecated
76
- id = stmt.object.to_s.split('/').last
77
- objs << Triannon::Annotation.new(:id => id)
78
- end
79
-
80
- objs
81
- end
82
-
83
62
  protected
84
63
 
85
64
  # given a path to the back end storage url, retrieve the object from storage and load
@@ -1,9 +1,11 @@
1
1
  module Triannon
2
2
  class LdpToOaMapper
3
3
 
4
- # maps an AnnotationLdp to an OA RDF::Graph
5
- def self.ldp_to_oa ldp_anno
6
- mapper = Triannon::LdpToOaMapper.new ldp_anno
4
+ # maps a Triannon::AnnotationLdp to an OA RDF::Graph
5
+ # @param [Triannon::AnnotationLdp] ldp_anno
6
+ # @param [String] root_container the LDP parent container for the annotation
7
+ def self.ldp_to_oa(ldp_anno, root_container)
8
+ mapper = Triannon::LdpToOaMapper.new(ldp_anno, root_container)
7
9
  mapper.extract_base
8
10
  mapper.extract_bodies
9
11
  mapper.extract_targets
@@ -12,30 +14,32 @@ module Triannon
12
14
 
13
15
  attr_accessor :id, :oa_graph
14
16
 
15
- def initialize ldp_anno
17
+ # @param [Triannon::AnnotationLdp] ldp_anno
18
+ # @param [String] root_container the LDP parent container for the annotation
19
+ def initialize(ldp_anno, root_container)
16
20
  @ldp_anno = ldp_anno
21
+ @root_container = root_container
17
22
  @ldp_anno_graph = ldp_anno.stripped_graph
18
23
  g = RDF::Graph.new
19
24
  @oa_graph = OA::Graph.new g
20
25
  end
21
26
 
27
+ # load statements from base anno container into @oa_graph
22
28
  def extract_base
23
29
  root_subject_solns = @ldp_anno_graph.query OA::Graph.anno_query
24
30
  if root_subject_solns.count == 1
25
31
  stored_url = Triannon.config[:ldp]['url'].strip
26
32
  stored_url.chop! if stored_url.end_with?('/')
27
- container_path = Triannon.config[:ldp]['uber_container']
28
- if container_path
29
- container_path.strip!
30
- container_path = container_path[1..-1] if container_path.start_with?('/')
31
- container_path.chop! if container_path.end_with?('/')
32
- stored_url = "#{stored_url}/#{container_path}"
33
+ uber_container = Triannon.config[:ldp]['uber_container'].strip
34
+ if uber_container
35
+ uber_container = uber_container[1..-1] if uber_container.start_with?('/')
36
+ uber_container.chop! if uber_container.end_with?('/')
37
+ stored_url = "#{stored_url}/#{uber_container}/#{@root_container}"
33
38
  end
34
39
  @id = root_subject_solns[0].s.to_s.split("#{stored_url}/").last
35
- base_url = Triannon.config[:triannon_base_url]
36
- base_url.strip!
40
+ base_url = Triannon.config[:triannon_base_url].strip
37
41
  base_url.chop! if base_url[-1] == '/'
38
- @root_uri = RDF::URI.new(base_url + "/#{@id}")
42
+ @root_uri = RDF::URI.new(base_url + "/#{@root_container}/#{@id}")
39
43
  end
40
44
 
41
45
  @ldp_anno_graph.each_statement do |stmnt|
@@ -49,6 +53,7 @@ module Triannon
49
53
  end
50
54
  end
51
55
 
56
+ # load statements from anno body containers into @oa_graph
52
57
  def extract_bodies
53
58
  @ldp_anno.body_uris.each { |body_uri|
54
59
  if !map_external_ref(body_uri, RDF::Vocab::OA.hasBody) &&
@@ -59,6 +64,7 @@ module Triannon
59
64
  }
60
65
  end
61
66
 
67
+ # load statements from anno target containers into @oa_graph
62
68
  def extract_targets
63
69
  @ldp_anno.target_uris.each { |target_uri|
64
70
  if !map_external_ref(target_uri, RDF::Vocab::OA.hasTarget) &&
@@ -4,13 +4,13 @@ module Triannon
4
4
  class LdpWriter
5
5
 
6
6
  # use LDP protocol to create the OpenAnnotation.Annotation in an RDF store
7
- # @param [Triannon::Annotation] anno a Triannon::Annotation object, from
8
- # which we use the graph
9
- def self.create_anno(anno)
7
+ # @param [Triannon::Annotation] anno a Triannon::Annotation object, from which we use the graph
8
+ # @param [String] root_container the LDP parent container for the annotation
9
+ def self.create_anno(anno, root_container)
10
10
  if anno && anno.graph
11
11
  # TODO: special case if the Annotation object already has an id --
12
12
  # see https://github.com/sul-dlss/triannon/issues/84
13
- ldp_writer = Triannon::LdpWriter.new anno
13
+ ldp_writer = Triannon::LdpWriter.new anno, root_container
14
14
  id = ldp_writer.create_base
15
15
 
16
16
  bodies_solns = anno.graph.query([nil, RDF::Vocab::OA.hasBody, nil])
@@ -30,8 +30,7 @@ module Triannon
30
30
  end
31
31
  end
32
32
 
33
- # deletes the indicated container and all its child containers from the LDP
34
- # store
33
+ # deletes the indicated container and all its child containers from the LDP store
35
34
  # @param [String] id the unique id for the LDP container for an annotation.
36
35
  # May be a compound id, such as uuid1/t/uuid2, in which case the LDP
37
36
  # container object uuid2 and its children are deleted from the LDP
@@ -39,7 +38,7 @@ module Triannon
39
38
  # the LDP store.
40
39
  def self.delete_container id
41
40
  if id && id.size > 0
42
- ldpw = Triannon::LdpWriter.new nil
41
+ ldpw = Triannon::LdpWriter.new nil, nil
43
42
  ldpw.delete_containers id
44
43
  end
45
44
  end
@@ -126,35 +125,39 @@ module Triannon
126
125
 
127
126
 
128
127
  # @param [Triannon::Annotation] anno a Triannon::Annotation object
129
- # @param [String] id the unique id for the LDP container for the passed
130
- # annotation; defaults to nil
131
- def initialize(anno, id = nil)
128
+ # @param [String] root_container the LDP parent container for the annotation
129
+ # @param [String] id the unique id for the LDP container for the passed annotation; defaults to nil
130
+ def initialize(anno, root_container, id = nil)
132
131
  @anno = anno
132
+ @root_container = root_container
133
133
  @id = id
134
134
  base_url = Triannon.config[:ldp]['url'].strip
135
135
  base_url.chop! if base_url.end_with?('/')
136
- container_path = Triannon.config[:ldp]['uber_container']
137
- if container_path
138
- container_path.strip!
139
- container_path = container_path[1..-1] if container_path.start_with?('/')
140
- container_path.chop! if container_path.end_with?('/')
136
+ @uber_container_path = Triannon.config[:ldp]['uber_container'].strip
137
+ if @uber_container_path
138
+ @uber_container_path = @uber_container_path[1..-1] if @uber_container_path.start_with?('/')
139
+ @uber_container_path.chop! if @uber_container_path.end_with?('/')
141
140
  end
142
- @base_uri = "#{base_url}/#{container_path}"
141
+ @base_uri = "#{base_url}/#{@uber_container_path}"
143
142
  end
144
143
 
145
144
  # creates a stored LDP container for this object's Annotation, without its
146
145
  # targets or bodies (as those are put in descendant containers)
147
146
  # SIDE EFFECT: assigns the uuid of the container created to @id
148
- # @return [String] the unique id for the LDP container created for this
149
- # annotation
147
+ # @return [String] the unique id for the LDP container created for this annotation
150
148
  def create_base
151
149
  if @anno.graph.query([nil, RDF::Triannon.externalReference, nil]).count > 0
152
150
  fail Triannon::ExternalReferenceError, "Incoming annotations may not have http://triannon.stanford.edu/ns/externalReference as a predicate."
153
151
  end
154
-
155
152
  if @anno.graph.id_as_url && @anno.graph.id_as_url.size > 0
156
153
  fail Triannon::ExternalReferenceError, "Incoming new annotations may not have an existing id (yet)."
157
154
  end
155
+ if @root_container.blank?
156
+ fail Triannon::LDPContainerError, "Annotations must be created in a root container."
157
+ end
158
+ unless self.class.container_exist? "#{@uber_container_path}/#{@root_container}"
159
+ fail Triannon::MissingLDPContainerError, "Annotation root container #{@root_container} doesn't exist."
160
+ end
158
161
 
159
162
  # TODO: special case if the Annotation object already has an id --
160
163
  # see https://github.com/sul-dlss/triannon/issues/84
@@ -167,8 +170,9 @@ module Triannon
167
170
  g = OA::Graph.new(g)
168
171
  g.remove_non_base_statements
169
172
  g.make_null_relative_uri_out_of_blank_node
173
+ g << [RDF::URI.new, RDF.type, RDF::Vocab::LDP.BasicContainer]
170
174
 
171
- @id = create_resource g.to_ttl
175
+ @id = create_resource g.to_ttl, @root_container
172
176
  end
173
177
 
174
178
  # creates the LDP container for any and all bodies for this annotation
@@ -202,8 +206,9 @@ module Triannon
202
206
  end
203
207
  something_deleted = false
204
208
  ldp_container_uris.each { |uri|
205
- ldp_id = uri.to_s.split(@base_uri + '/').last
206
- resp = conn.delete { |req| req.url ldp_id }
209
+ ldp_id = uri.to_s.split("#{@base_uri}/").last
210
+ ldp_id = "#{@root_container}/#{ldp_id}" unless @root_container.blank? || ldp_id.match(@root_container)
211
+ resp = conn.delete { |req| req.url "#{ldp_id}" }
207
212
  if resp.status != 204
208
213
  fail Triannon::LDPStorageError.new("Unable to delete LDP container #{ldp_id}", resp.status, resp.body)
209
214
  end
@@ -214,18 +219,17 @@ module Triannon
214
219
 
215
220
  protected
216
221
 
217
- # POSTS a ttl representation of a graph to an existing LDP container in the
218
- # LDP store
219
- # @param [String] ttl a turtle representation of RDF data to be put in the
220
- # LDP container
221
- # @param [String] parent_path the path portion of the url for the LDP parent
222
- # container for this resource if no path is supplied, then the resource
223
- # will be created as a child of the root annotation; expected paths would
224
- # also be (anno_id)/t for a target resource (inside the target container
225
- # of anno_id) or (anno_id)/b for a body resource (inside the body
226
- # container of anno_id)
227
- # @return [String] path_id representing the unique path of the newly created LDP
228
- # container
222
+ # POSTS a ttl representation of a graph to an existing LDP container in the LDP store,
223
+ # which will cause the LDP store to create a new container that is a child of
224
+ # the existing container.
225
+ # @param [String] ttl a turtle representation of RDF data to be put in the newly created LDP container
226
+ # @param [String] parent_path the path portion of the url for the LDP parent container for this resource.
227
+ # if no path is supplied, then the resource will be created as a child of the root annotation;
228
+ # expected paths would also be
229
+ # (anno_id)/t for a target resource (new container to be created inside the target container of anno_id)
230
+ # or
231
+ # (anno_id)/b for a body resource (new container to be created inside the body container of anno_id)
232
+ # @return [String] path_id representing the unique path of the newly created LDP container
229
233
  def create_resource ttl, parent_path = nil
230
234
  return if !ttl || ttl.empty?
231
235
 
@@ -243,7 +247,7 @@ module Triannon
243
247
  req.body = ttl
244
248
  end
245
249
  if resp.status != 200 && resp.status != 201
246
- fail Triannon::LDPStorageError.new("Unable to create LDP resource in container #{parent_path}; RDF sent: #{ttl}", resp.status, resp.body)
250
+ fail Triannon::LDPStorageError.new("Unable to create LDP resource in container #{parent_path};\nRDF sent: #{ttl}", resp.status, resp.body)
247
251
  end
248
252
  new_url = resp.headers['Location'] ? resp.headers['Location'] : resp.headers['location']
249
253
  if new_url
@@ -257,37 +261,35 @@ module Triannon
257
261
  # the base container at @id and has the memberRelation per the
258
262
  # oa_vocab_term. The id of the created container will be (base container
259
263
  # id)/b if hasBody or (base container id)/t if hasTarget
260
- # @param [RDF::Vocabulary::Term] oa_vocab_term RDF::Vocab::OA.hasTarget or
261
- # RDF::Vocab::OA.hasBody
264
+ # @param [RDF::Vocabulary::Term] oa_vocab_term RDF::Vocab::OA.hasTarget or RDF::Vocab::OA.hasBody
262
265
  def create_direct_container oa_vocab_term
263
266
  null_rel_uri = RDF::URI.new
264
267
  g = RDF::Graph.new
265
268
  g << [null_rel_uri, RDF.type, RDF::Vocab::LDP.DirectContainer]
266
269
  g << [null_rel_uri, RDF::Vocab::LDP.hasMemberRelation, oa_vocab_term]
267
- g << [null_rel_uri, RDF::Vocab::LDP.membershipResource, RDF::URI.new("#{@base_uri}/#{@id}")]
270
+ g << [null_rel_uri, RDF::Vocab::LDP.membershipResource, RDF::URI.new("#{@base_uri}/#{@root_container}/#{@id}")]
268
271
 
269
272
  resp = conn.post do |req|
270
- req.url "#{@id}"
273
+ req.url "#{@root_container}/#{@id}"
271
274
  req.headers['Content-Type'] = 'application/x-turtle'
272
275
  # OA vocab relationships all of form "hasXXX" so this becomes 't' or 'b'
273
276
  req.headers['Slug'] = oa_vocab_term.fragment.slice(3).downcase
274
277
  req.body = g.to_ttl
275
278
  end
276
279
  if resp.status != 201
277
- fail Triannon::LDPStorageError.new("Unable to create #{oa_vocab_term.fragment.sub('has', '')} LDP container for anno; RDF sent: #{g.to_ttl}", resp.status, resp.body)
280
+ fail Triannon::LDPStorageError.new("Unable to create #{oa_vocab_term.fragment.sub('has', '')} LDP container for anno #{@root_container}/#{@id};\nRDF sent: #{g.to_ttl}", resp.status, resp.body)
278
281
  end
279
282
  resp
280
283
  end
281
284
 
282
- # create the target/body resources inside the (already created) target/body
283
- # container
284
- # @param [RDF::URI] predicate either RDF::Vocab::OA.hasTarget or
285
- # RDF::Vocab::OA.hasBody
285
+ # create the target/body resources inside the (already created) target/body container
286
+ # @param [RDF::URI] predicate either RDF::Vocab::OA.hasTarget or RDF::Vocab::OA.hasBody
286
287
  def create_resources_in_container(predicate)
287
288
  predicate_solns = @anno.graph.query([nil, predicate, nil])
288
289
  resource_ids = []
289
290
  predicate_solns.each { |predicate_stmt |
290
291
  graph_for_resource = RDF::Graph.new
292
+ graph_for_resource << [RDF::URI.new, RDF.type, RDF::Vocab::LDP.BasicContainer]
291
293
  predicate_obj = predicate_stmt.object
292
294
  if predicate_obj.is_a?(RDF::Node)
293
295
  # we need to use the null relative URI representation of blank nodes
@@ -384,10 +386,11 @@ module Triannon
384
386
  graph_for_resource.delete(s)
385
387
  }
386
388
  }
389
+
387
390
  if (predicate == RDF::Vocab::OA.hasTarget)
388
- resource_ids << create_resource(graph_for_resource.to_ttl, "#{@id}/t")
391
+ resource_ids << create_resource(graph_for_resource.to_ttl, "#{@root_container}/#{@id}/t")
389
392
  else
390
- resource_ids << create_resource(graph_for_resource.to_ttl, "#{@id}/b")
393
+ resource_ids << create_resource(graph_for_resource.to_ttl, "#{@root_container}/#{@id}/b")
391
394
  end
392
395
  }
393
396
  resource_ids
@@ -49,6 +49,12 @@ module Triannon
49
49
  when 'bodyexact'
50
50
  # no need to Solr escape value because it's in quotes
51
51
  q_terms_array << "body_chars_exact:\"#{v}\""
52
+ when 'bodykeyword'
53
+ solr_params_hash[:kqf] = 'body_chars_exact^3 body_chars_unstem^2 body_chars_stem'
54
+ solr_params_hash[:kpf] = 'body_chars_exact^15 body_chars_unstem^10 body_chars_stem^5'
55
+ solr_params_hash[:kpf3] = 'body_chars_exact^9 body_chars_unstem^6 body_chars_stem^3'
56
+ solr_params_hash[:kpf2] = 'body_chars_exact^6 body_chars_unstem^4 body_chars_stem^2'
57
+ q_terms_array << '_query_:"{!dismax qf=$kqf pf=$kpf pf3=$kpf3 pf2=$kpf2}' + RSolr.solr_escape(v) + '"'
52
58
  when 'motivatedby'
53
59
  case
54
60
  when v.include?('#')
@@ -60,12 +66,8 @@ module Triannon
60
66
  else
61
67
  fq_terms_array << "motivation:#{RSolr.solr_escape(v)}"
62
68
  end
63
- when 'bodykeyword'
64
- solr_params_hash[:kqf] = 'body_chars_exact^3 body_chars_unstem^2 body_chars_stem'
65
- solr_params_hash[:kpf] = 'body_chars_exact^15 body_chars_unstem^10 body_chars_stem^5'
66
- solr_params_hash[:kpf3] = 'body_chars_exact^9 body_chars_unstem^6 body_chars_stem^3'
67
- solr_params_hash[:kpf2] = 'body_chars_exact^6 body_chars_unstem^4 body_chars_stem^2'
68
- q_terms_array << '_query_:"{!dismax qf=$kqf pf=$kpf pf3=$kpf3 pf2=$kpf2}' + RSolr.solr_escape(v) + '"'
69
+ when 'anno_root'
70
+ fq_terms_array << "root:#{RSolr.solr_escape(v)}"
69
71
 
70
72
  # TODO: add'l params to implement:
71
73
  # targetType - fq
@@ -7,16 +7,20 @@ module Triannon
7
7
  # Convert a OA::Graph object into a Hash suitable for writing to Solr.
8
8
  #
9
9
  # @param [OA::Graph] triannon_graph a populated OA::Graph object for a *stored* anno
10
+ # @param [String] root_container the id of the LDP parent container for the annotation
10
11
  # @return [Hash] a hash to be written to Solr, populated appropriately
11
- def self.solr_hash(triannon_graph)
12
+ def self.solr_hash(triannon_graph, root_container)
12
13
  doc_hash = {}
13
14
  triannon_id = triannon_graph.id_as_url
14
- if triannon_id
15
- # chars in Solr/Lucene query syntax are a big pain in Solr id fields, so we only use
16
- # the uuid portion of the Triannon anno id, not the full url
15
+ if triannon_id && root_container.present?
16
+ # we simplify the URL, removing the triannon base_url. This will only be problematic if
17
+ # two different LDP Stores had the same root-container/pair-tree/uuid format AND they were
18
+ # both writing to the same Solr index.
17
19
  solr_id = triannon_id.sub(Triannon.config[:triannon_base_url], "")
18
20
  doc_hash[:id] = solr_id.sub(/^\/*/, "") # remove first char slash(es) if present
19
21
 
22
+ doc_hash[:root] = root_container
23
+
20
24
  # use short strings for motivation field
21
25
  doc_hash[:motivation] = triannon_graph.motivated_by.map { |m| m.sub(RDF::Vocab::OA.to_s, "") }
22
26
 
@@ -60,8 +64,9 @@ module Triannon
60
64
  # Convert the OA::Graph to a Solr document hash, then call RSolr.add
61
65
  # with the doc hash
62
66
  # @param [OA::Graph] tgraph anno represented as a OA::Graph
63
- def write(tgraph)
64
- doc_hash = self.class.solr_hash(tgraph) if tgraph && !tgraph.id_as_url.empty?
67
+ # @param [String] root the id of the LDP parent container for the annotation
68
+ def write(tgraph, root)
69
+ doc_hash = self.class.solr_hash(tgraph, root) if tgraph && !tgraph.id_as_url.empty? && !root.blank?
65
70
  add(doc_hash) if doc_hash && !doc_hash.empty?
66
71
  end
67
72
 
@@ -1 +1,21 @@
1
- <%= render 'form' %>
1
+ <%= form_for @annotation, url: {action: "create"} do |f| %>
2
+ <% if @annotation.errors.any? %>
3
+ <div id="error_explanation">
4
+ <h2><%= pluralize(@annotation.errors.count, "error") %> prohibited this annotation from being saved:</h2>
5
+
6
+ <ul>
7
+ <% @annotation.errors.full_messages.each do |message| %>
8
+ <li><%= message %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
13
+
14
+ <div class="field">
15
+ <%= f.label 'Annotation (as json-ld or turtle)' %><br>
16
+ <%= f.text_area :data, size: "80x20" %>
17
+ </div>
18
+ <div class="actions">
19
+ <%= f.submit %>
20
+ </div>
21
+ <% end %>