triannon 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 %>