proformaxml 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop/layout.yml +39 -0
- data/.rubocop/lint.yml +14 -0
- data/.rubocop/metrics.yml +31 -0
- data/.rubocop/rails.yml +4 -0
- data/.rubocop/rspec.yml +27 -0
- data/.rubocop/style.yml +74 -0
- data/.rubocop.yml +30 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +168 -0
- data/Guardfile +44 -0
- data/LICENSE.txt +21 -0
- data/README.md +212 -0
- data/Rakefile +11 -0
- data/SECURITY.md +14 -0
- data/assets/schemas/proforma-2.0.xsd +1038 -0
- data/assets/schemas/proforma-2.1.xsd +1084 -0
- data/assets/schemas/proforma-java-checkstyle.xsd +11 -0
- data/assets/schemas/proforma-regexptest.xsd +50 -0
- data/assets/schemas/proforma-unittest.xsd +12 -0
- data/lib/proformaxml/errors.rb +9 -0
- data/lib/proformaxml/helpers/export_helpers.rb +79 -0
- data/lib/proformaxml/helpers/import_helpers.rb +114 -0
- data/lib/proformaxml/models/base.rb +11 -0
- data/lib/proformaxml/models/model_solution.rb +12 -0
- data/lib/proformaxml/models/task.rb +29 -0
- data/lib/proformaxml/models/task_file.rb +11 -0
- data/lib/proformaxml/models/test.rb +12 -0
- data/lib/proformaxml/services/exporter.rb +140 -0
- data/lib/proformaxml/services/importer.rb +135 -0
- data/lib/proformaxml/services/validator.rb +52 -0
- data/lib/proformaxml/version.rb +5 -0
- data/lib/proformaxml.rb +24 -0
- data/proformaxml.gemspec +37 -0
- metadata +179 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns1="urn:proforma:tests:java-checkstyle:v1.1" targetNamespace="urn:proforma:tests:java-checkstyle:v1.1" elementFormDefault="qualified" attributeFormDefault="unqualified">
|
3
|
+
<xs:element name="java-checkstyle">
|
4
|
+
<xs:complexType>
|
5
|
+
<xs:sequence>
|
6
|
+
<xs:element name="max-checkstyle-warnings" type="xs:string"/>
|
7
|
+
</xs:sequence>
|
8
|
+
<xs:attribute name="version" type="xs:string" use="required"/>
|
9
|
+
</xs:complexType>
|
10
|
+
</xs:element>
|
11
|
+
</xs:schema>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!-- This is meant for blackbox tests of the output of a program. For regexp tests of
|
3
|
+
sourcecode something like checkstyle can be used.-->
|
4
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns1="urn:proforma:tests:regexptest:v0.9"
|
5
|
+
targetNamespace="urn:proforma:tests:regexptest:v0.9" elementFormDefault="qualified"
|
6
|
+
attributeFormDefault="unqualified">
|
7
|
+
<xs:element name="regexptest">
|
8
|
+
<xs:complexType>
|
9
|
+
<xs:sequence>
|
10
|
+
<!-- entry-point is a file or a main-class needed to start a program -->
|
11
|
+
<xs:element name="entry-point" type="xs:string"/>
|
12
|
+
<!-- parameter: optional list of command line arguments -->
|
13
|
+
<xs:element name="parameter" minOccurs="0">
|
14
|
+
<xs:simpleType>
|
15
|
+
<xs:list itemType="xs:string"/>
|
16
|
+
</xs:simpleType>
|
17
|
+
</xs:element>
|
18
|
+
<xs:element name="regular-expressions">
|
19
|
+
<xs:complexType>
|
20
|
+
<!-- choice means: regexp-allow OR regexp-disallow -->
|
21
|
+
<xs:choice>
|
22
|
+
<xs:sequence>
|
23
|
+
<!-- regular expression for allowed program outputs -->
|
24
|
+
<xs:element name="regexp-allow" type="ns1:regexpType" maxOccurs="unbounded"/>
|
25
|
+
<!-- regular expression for not allowed program outputs -->
|
26
|
+
<xs:element name="regexp-disallow" type="ns1:regexpType" minOccurs="0" maxOccurs="unbounded"/>
|
27
|
+
</xs:sequence>
|
28
|
+
<xs:element name="regexp-disallow" type="ns1:regexpType" maxOccurs="unbounded"/>
|
29
|
+
</xs:choice>
|
30
|
+
</xs:complexType>
|
31
|
+
</xs:element>
|
32
|
+
</xs:sequence>
|
33
|
+
</xs:complexType>
|
34
|
+
</xs:element>
|
35
|
+
|
36
|
+
<xs:complexType name="regexpType">
|
37
|
+
<xs:simpleContent>
|
38
|
+
<xs:extension base="xs:string">
|
39
|
+
<xs:attributeGroup ref="ns1:regexp-flags"/>
|
40
|
+
</xs:extension>
|
41
|
+
</xs:simpleContent>
|
42
|
+
</xs:complexType>
|
43
|
+
<!-- attributes to define, which regexp-flags are applied to a specified regular expression -->
|
44
|
+
<xs:attributeGroup name="regexp-flags">
|
45
|
+
<xs:attribute name="case-insensitive" type="xs:boolean" default="false"/>
|
46
|
+
<xs:attribute name="dotall" type="xs:boolean" default="false"/>
|
47
|
+
<xs:attribute name="multiline" type="xs:boolean" default="false"/>
|
48
|
+
<xs:attribute name="free-spacing" type="xs:boolean" default="false"/>
|
49
|
+
</xs:attributeGroup>
|
50
|
+
</xs:schema>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns1="urn:proforma:tests:unittest:v1.1" targetNamespace="urn:proforma:tests:unittest:v1.1" elementFormDefault="qualified" attributeFormDefault="unqualified">
|
3
|
+
<xs:element name="unittest">
|
4
|
+
<xs:complexType>
|
5
|
+
<xs:sequence>
|
6
|
+
<xs:element name="entry-point" type="xs:string" maxOccurs="unbounded"/>
|
7
|
+
</xs:sequence>
|
8
|
+
<xs:attribute name="framework" type="xs:string" use="required"/>
|
9
|
+
<xs:attribute name="version" type="xs:string" use="required"/>
|
10
|
+
</xs:complexType>
|
11
|
+
</xs:element>
|
12
|
+
</xs:schema>
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProformaXML
|
4
|
+
module Helpers
|
5
|
+
module ExportHelpers
|
6
|
+
def attach_file(xml, file)
|
7
|
+
if file.embed?
|
8
|
+
embed_file(file, xml)
|
9
|
+
else
|
10
|
+
xml.send "attached-#{file.binary ? 'bin' : 'txt'}-file", file.filename
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def embed_file(file, xml)
|
15
|
+
if file.binary
|
16
|
+
xml.send :'embedded-bin-file', {filename: file.filename}, Base64.encode64(file.content)
|
17
|
+
else
|
18
|
+
xml.send :'embedded-txt-file', {filename: file.filename}, file.content
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_description_to_xml(xml, description)
|
23
|
+
xml.send(:description, description) if description.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_test_configuration(xml, test)
|
27
|
+
xml.send(:'test-configuration') do
|
28
|
+
add_filerefs(xml, test) if test.files
|
29
|
+
add_configuration(xml, test.configuration) unless test.configuration.nil?
|
30
|
+
if test.meta_data
|
31
|
+
xml.send(:'test-meta-data') do
|
32
|
+
meta_data(xml, test.meta_data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def inner_meta_data(xml, namespace, data)
|
39
|
+
data.each do |key, value|
|
40
|
+
case value.class.name
|
41
|
+
when 'Hash'
|
42
|
+
# underscore is used to disambiguate tag names from ruby methods
|
43
|
+
xml[namespace].send("#{key}_") do |meta_data_xml|
|
44
|
+
inner_meta_data(meta_data_xml, namespace, value)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
xml[namespace].send("#{key}_", value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def meta_data(xml, meta_data)
|
53
|
+
meta_data.each do |namespace, data|
|
54
|
+
inner_meta_data(xml, namespace, data)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_configuration(xml, configuration)
|
59
|
+
xml_snippet = Dachsfisch::JSON2XMLConverter.perform(json: configuration.to_json)
|
60
|
+
configuration.flat_map {|_, val| val['@xmlns'].to_a }.uniq.each do |namespace|
|
61
|
+
xml.doc.root.add_namespace(namespace[0], namespace[1])
|
62
|
+
end
|
63
|
+
|
64
|
+
xml << xml_snippet
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_namespaces_to_header(header, custom_namespaces)
|
68
|
+
custom_namespaces.each do |namespace|
|
69
|
+
header["xmlns:#{namespace[:prefix]}"] = namespace[:uri]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_parent_uuid_and_lang_to_header(header)
|
74
|
+
header['lang'] = @task.language if @task.language.present?
|
75
|
+
header['parent-uuid'] = @task.parent_uuid if @task.parent_uuid.present?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProformaXML
|
4
|
+
module Helpers
|
5
|
+
module ImportHelpers
|
6
|
+
CONFIGURATION_NODES = %w[filerefs timeout externalresourcerefs test-meta-data].freeze
|
7
|
+
|
8
|
+
def set_hash_value_if_present(hash:, name:, attributes: nil, value_overwrite: nil)
|
9
|
+
raise unless attributes || value_overwrite
|
10
|
+
|
11
|
+
value = value_overwrite || attributes[name.to_s]&.value
|
12
|
+
hash[name.underscore.to_sym] = value if value.present?
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_value_from_xml(object:, node:, name:, attribute: false, check_presence: true)
|
16
|
+
value = value_from_node(name, node, attribute)
|
17
|
+
return if check_presence && value.blank?
|
18
|
+
|
19
|
+
set_value(object:, name: (name.is_a?(Array) ? name[1] : name).underscore, value:)
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_value(object:, name:, value:)
|
23
|
+
if object.is_a? Hash
|
24
|
+
object[name] = value
|
25
|
+
else
|
26
|
+
object.send("#{name}=", value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def embedded_file_attributes(attributes, file_tag)
|
31
|
+
shared = shared_file_attributes(attributes, file_tag)
|
32
|
+
shared.merge(
|
33
|
+
content: content_from_file_tag(file_tag, shared[:binary])
|
34
|
+
).tap do |hash|
|
35
|
+
hash[:filename] = file_tag.attributes['filename']&.value if file_tag.attributes['filename']&.value.present?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def attached_file_attributes(attributes, file_tag)
|
40
|
+
filename = file_tag.text
|
41
|
+
shared_file_attributes(attributes, file_tag).merge(filename:,
|
42
|
+
content: filestring_from_zip(filename))
|
43
|
+
end
|
44
|
+
|
45
|
+
def shared_file_attributes(attributes, file_tag)
|
46
|
+
{
|
47
|
+
id: attributes['id']&.value,
|
48
|
+
used_by_grader: attributes['used-by-grader']&.value == 'true',
|
49
|
+
visible: attributes['visible']&.value,
|
50
|
+
binary: file_tag.name.include?('-bin-file'),
|
51
|
+
}.tap do |hash|
|
52
|
+
set_hash_value_if_present(hash:, name: 'usage-by-lms', attributes:)
|
53
|
+
set_value_from_xml(object: hash, node: file_tag.parent, name: 'internal-description')
|
54
|
+
set_hash_value_if_present(hash:, name: 'mimetype', attributes:)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_test_configuration(test, test_node)
|
59
|
+
test_configuration_node = test_node.xpath('xmlns:test-configuration')
|
60
|
+
test.files = test_files_from_test_configuration(test_configuration_node)
|
61
|
+
test.configuration = extra_configuration_from_test_configuration(test_configuration_node)
|
62
|
+
meta_data_node = test_node.xpath('xmlns:test-configuration').xpath('xmlns:test-meta-data')
|
63
|
+
test.meta_data = meta_data(meta_data_node, use_namespace: true) if meta_data_node.present?
|
64
|
+
end
|
65
|
+
|
66
|
+
def extra_configuration_from_test_configuration(test_configuration_node)
|
67
|
+
configuration_any_nodes = test_configuration_node.children.reject {|c| CONFIGURATION_NODES.include? c.name }
|
68
|
+
return nil if configuration_any_nodes.empty?
|
69
|
+
|
70
|
+
{}.tap do |config_hash|
|
71
|
+
configuration_any_nodes.each do |config_node|
|
72
|
+
config_hash.merge! convert_xml_node_to_json(config_node)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_files_from_test_configuration(test_configuration_node)
|
78
|
+
files_from_filerefs(test_configuration_node.search('filerefs'))
|
79
|
+
end
|
80
|
+
|
81
|
+
def meta_data(any_data_node, use_namespace: false)
|
82
|
+
# use_namespace forces the use of the namespace as hash key - it should only be used at the entry of the recursion
|
83
|
+
{}.tap do |any_data|
|
84
|
+
any_data_node.children.each do |node|
|
85
|
+
key = (use_namespace ? node.namespace.prefix : any_data_node.name).to_sym
|
86
|
+
any_data[key] = if node.node_type == Nokogiri::XML::Node::TEXT_NODE
|
87
|
+
node.text
|
88
|
+
else
|
89
|
+
# preserve any existing data in the nested hash
|
90
|
+
(any_data[key] || {}).merge meta_data(node)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def convert_xml_node_to_json(any_node)
|
99
|
+
xml_snippet = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new, any_node)
|
100
|
+
xml_snippet.children.first.add_namespace_definition(any_node.namespace.prefix, any_node.namespace.href)
|
101
|
+
JSON.parse(Dachsfisch::XML2JSONConverter.perform(xml: xml_snippet.to_xml))
|
102
|
+
end
|
103
|
+
|
104
|
+
def value_from_node(name, node, attribute)
|
105
|
+
xml_name = name.is_a?(Array) ? name[0] : name
|
106
|
+
attribute ? node.attribute(xml_name)&.value : node.xpath("xmlns:#{xml_name}").text
|
107
|
+
end
|
108
|
+
|
109
|
+
def content_from_file_tag(file_tag, binary)
|
110
|
+
binary ? Base64.decode64(file_tag.text) : file_tag.text
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'proformaxml/models/base'
|
4
|
+
require 'proformaxml/models/task_file'
|
5
|
+
require 'proformaxml/models/test'
|
6
|
+
require 'proformaxml/models/model_solution'
|
7
|
+
require 'proformaxml/errors'
|
8
|
+
|
9
|
+
module ProformaXML
|
10
|
+
class Task < Base
|
11
|
+
attr_accessor :title, :description, :internal_description, :proglang, :uuid, :parent_uuid,
|
12
|
+
:language, :model_solutions, :files, :tests, :meta_data
|
13
|
+
|
14
|
+
def initialize(attributes = {})
|
15
|
+
super
|
16
|
+
self.files = [] if files.nil?
|
17
|
+
self.tests = [] if tests.nil?
|
18
|
+
self.model_solutions = [] if model_solutions.nil?
|
19
|
+
self.meta_data = {} if meta_data.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_files
|
23
|
+
task_files = files
|
24
|
+
model_solution_files = model_solutions.map(&:files).filter(&:present?)
|
25
|
+
test_files = tests.map(&:files).filter(&:present?)
|
26
|
+
(task_files + model_solution_files + test_files).flatten.uniq
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProformaXML
|
4
|
+
class TaskFile < Base
|
5
|
+
attr_accessor :id, :content, :filename, :used_by_grader, :visible, :usage_by_lms, :binary, :internal_description, :mimetype
|
6
|
+
|
7
|
+
def embed?
|
8
|
+
(content&.length || 0) < MAX_EMBEDDED_FILE_SIZE_KB * (2**10)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProformaXML
|
4
|
+
class Test < Base
|
5
|
+
attr_accessor :id, :title, :description, :internal_description, :test_type, :files, :configuration, :meta_data
|
6
|
+
|
7
|
+
def initialize(attributes = {})
|
8
|
+
super
|
9
|
+
self.files = [] if files.nil?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'proformaxml/helpers/export_helpers'
|
4
|
+
|
5
|
+
module ProformaXML
|
6
|
+
class Exporter
|
7
|
+
include ProformaXML::Helpers::ExportHelpers
|
8
|
+
|
9
|
+
def initialize(task:, custom_namespaces: [], version: nil)
|
10
|
+
@files = {}
|
11
|
+
@task = task
|
12
|
+
@custom_namespaces = custom_namespaces
|
13
|
+
@version = version || SCHEMA_VERSIONS.first
|
14
|
+
add_placeholders if @version == '2.0'
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform
|
18
|
+
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
19
|
+
xml_task(xml)
|
20
|
+
end
|
21
|
+
xmldoc = builder.to_xml
|
22
|
+
doc = Nokogiri::XML(xmldoc)
|
23
|
+
errors = validate(doc)
|
24
|
+
|
25
|
+
raise PostGenerateValidationError.new(errors) if errors.any?
|
26
|
+
|
27
|
+
# File.open('../testfile.zip', 'wb') { |file| file.write(write_to_zip(xmldoc).string) }
|
28
|
+
write_to_zip(xmldoc)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def xml_task(xml)
|
34
|
+
xml.task(headers) do
|
35
|
+
xml.title @task.title
|
36
|
+
xml.description @task.description
|
37
|
+
add_internal_description_to_xml(xml, @task.internal_description)
|
38
|
+
xml.proglang({version: @task.proglang&.dig(:version)}, @task.proglang&.dig(:name))
|
39
|
+
|
40
|
+
add_objects_to_xml(xml)
|
41
|
+
add_meta_data(xml)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_internal_description_to_xml(xml, internal_description)
|
46
|
+
xml.send(:'internal-description', internal_description) if internal_description.present?
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_meta_data(xml)
|
50
|
+
xml.send(:'meta-data') { meta_data(xml, @task.meta_data) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_objects_to_xml(xml)
|
54
|
+
xml.files { files(xml) }
|
55
|
+
xml.send(:'model-solutions') { model_solutions(xml) } if @task.model_solutions.any? || @version == '2.0'
|
56
|
+
xml.tests { tests(xml) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def files(xml)
|
60
|
+
@task.all_files.each do |file|
|
61
|
+
xml.file({
|
62
|
+
id: file.id, 'used-by-grader' => file.used_by_grader, visible: file.visible,
|
63
|
+
'usage-by-lms' => file.usage_by_lms, mimetype: file.mimetype
|
64
|
+
}.compact) do
|
65
|
+
attach_file(xml, file)
|
66
|
+
add_internal_description_to_xml(xml, file.internal_description)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def model_solutions(xml)
|
72
|
+
@task.model_solutions&.each do |model_solution|
|
73
|
+
xml.send(:'model-solution', id: model_solution.id) do
|
74
|
+
add_filerefs(xml, model_solution)
|
75
|
+
add_description_to_xml(xml, model_solution.description)
|
76
|
+
add_internal_description_to_xml(xml, model_solution.internal_description)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_filerefs(xml, object)
|
82
|
+
return unless object.files.any?
|
83
|
+
|
84
|
+
xml.filerefs do
|
85
|
+
object.files.each do |file|
|
86
|
+
xml.fileref(refid: file.id) {}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def tests(xml)
|
92
|
+
@task.tests&.each do |test|
|
93
|
+
xml.test(id: test.id) do
|
94
|
+
xml.title test.title
|
95
|
+
add_description_to_xml(xml, test.description)
|
96
|
+
add_internal_description_to_xml(xml, test.internal_description)
|
97
|
+
xml.send(:'test-type', test.test_type)
|
98
|
+
add_test_configuration(xml, test)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# ms-placeholder only necessary for version 2.0 where model-solutions were mandatory
|
104
|
+
def add_placeholders
|
105
|
+
return if @task.model_solutions&.any?
|
106
|
+
|
107
|
+
file = TaskFile.new(content: '', id: 'ms-placeholder-file', used_by_grader: false, visible: 'no', binary: false)
|
108
|
+
model_solution = ModelSolution.new(id: 'ms-placeholder', files: [file])
|
109
|
+
@task.model_solutions = [model_solution]
|
110
|
+
end
|
111
|
+
|
112
|
+
def headers
|
113
|
+
{
|
114
|
+
'xmlns' => "urn:proforma:v#{@version}",
|
115
|
+
'uuid' => @task.uuid,
|
116
|
+
}.tap do |header|
|
117
|
+
add_namespaces_to_header(header, @custom_namespaces)
|
118
|
+
add_parent_uuid_and_lang_to_header(header)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def validate(doc)
|
123
|
+
validator = ProformaXML::Validator.new doc, @version
|
124
|
+
validator.perform
|
125
|
+
end
|
126
|
+
|
127
|
+
def write_to_zip(xmldoc)
|
128
|
+
Zip::OutputStream.write_buffer do |zio|
|
129
|
+
zio.put_next_entry('task.xml')
|
130
|
+
zio.write xmldoc
|
131
|
+
@task.all_files.each do |file|
|
132
|
+
next if file.embed?
|
133
|
+
|
134
|
+
zio.put_next_entry(file.filename)
|
135
|
+
zio.write file.content
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
require 'proformaxml/helpers/import_helpers'
|
5
|
+
|
6
|
+
module ProformaXML
|
7
|
+
class Importer
|
8
|
+
include ProformaXML::Helpers::ImportHelpers
|
9
|
+
|
10
|
+
def initialize(zip:, expected_version: nil)
|
11
|
+
@zip = zip
|
12
|
+
@expected_version = expected_version
|
13
|
+
|
14
|
+
xml = filestring_from_zip('task.xml')
|
15
|
+
raise PreImportValidationError if xml.nil?
|
16
|
+
|
17
|
+
@doc = Nokogiri::XML(xml, &:noblanks)
|
18
|
+
@task = Task.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def perform
|
22
|
+
errors = validate
|
23
|
+
|
24
|
+
raise PreImportValidationError.new(errors) if errors.any?
|
25
|
+
|
26
|
+
@task_node = @doc.xpath('/xmlns:task')
|
27
|
+
|
28
|
+
set_data
|
29
|
+
{task: @task, custom_namespaces: @custom_namespaces}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def filestring_from_zip(filename)
|
35
|
+
Zip::File.open(@zip.path) do |zip_file|
|
36
|
+
return zip_file.glob(filename).first&.get_input_stream&.read
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_data
|
41
|
+
set_namespaces
|
42
|
+
set_base_data
|
43
|
+
set_files
|
44
|
+
set_model_solutions
|
45
|
+
set_tests
|
46
|
+
set_meta_data
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_namespaces
|
50
|
+
@custom_namespaces = @doc.namespaces.except('xmlns').map {|k, v| {prefix: k[6..], uri: v} }
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_base_data
|
54
|
+
set_value_from_xml(object: @task, node: @task_node, name: 'title')
|
55
|
+
set_value_from_xml(object: @task, node: @task_node, name: 'description')
|
56
|
+
set_value_from_xml(object: @task, node: @task_node, name: 'internal-description')
|
57
|
+
set_proglang
|
58
|
+
set_value_from_xml(object: @task, node: @task_node, name: %w[lang language], attribute: true)
|
59
|
+
set_value_from_xml(object: @task, node: @task_node, name: 'parent-uuid', attribute: true)
|
60
|
+
set_value_from_xml(object: @task, node: @task_node, name: 'uuid', attribute: true)
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_proglang
|
64
|
+
return if @task_node.xpath('xmlns:proglang').text.blank?
|
65
|
+
|
66
|
+
@task.proglang = {name: @task_node.xpath('xmlns:proglang').text,
|
67
|
+
version: @task_node.xpath('xmlns:proglang').attribute('version').value.presence}.compact
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_files
|
71
|
+
@task_node.search('files//file').each {|file_node| add_file file_node }
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_tests
|
75
|
+
@task_node.search('tests//test').each {|test_node| add_test test_node }
|
76
|
+
end
|
77
|
+
|
78
|
+
def set_model_solutions
|
79
|
+
@task_node.search('model-solutions//model-solution').each do |model_solution_node|
|
80
|
+
add_model_solution model_solution_node
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_meta_data
|
85
|
+
meta_data_node = @task_node.xpath('xmlns:meta-data')
|
86
|
+
@task.meta_data = meta_data(meta_data_node, use_namespace: true) if meta_data_node.text.present?
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_model_solution(model_solution_node)
|
90
|
+
model_solution = ModelSolution.new
|
91
|
+
model_solution.id = model_solution_node.attributes['id'].value
|
92
|
+
model_solution.files = files_from_filerefs(model_solution_node.search('filerefs'))
|
93
|
+
set_value_from_xml(object: model_solution, node: model_solution_node, name: 'description')
|
94
|
+
set_value_from_xml(object: model_solution, node: model_solution_node, name: 'internal-description')
|
95
|
+
@task.model_solutions << model_solution unless model_solution.files.first&.id == 'ms-placeholder-file'
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_file(file_node)
|
99
|
+
file_tag = file_node.children.first
|
100
|
+
file = nil
|
101
|
+
case file_tag.name
|
102
|
+
when /embedded-(bin|txt)-file/
|
103
|
+
file = TaskFile.new(embedded_file_attributes(file_node.attributes, file_tag))
|
104
|
+
when /attached-(bin|txt)-file/
|
105
|
+
file = TaskFile.new(attached_file_attributes(file_node.attributes, file_tag))
|
106
|
+
end
|
107
|
+
@task.files << file
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_test(test_node)
|
111
|
+
test = Test.new
|
112
|
+
set_value_from_xml(object: test, node: test_node, name: 'id', attribute: true, check_presence: false)
|
113
|
+
set_value_from_xml(object: test, node: test_node, name: 'title', check_presence: false)
|
114
|
+
set_value_from_xml(object: test, node: test_node, name: 'description')
|
115
|
+
set_value_from_xml(object: test, node: test_node, name: 'internal-description')
|
116
|
+
set_value_from_xml(object: test, node: test_node, name: 'test-type', check_presence: false)
|
117
|
+
add_test_configuration(test, test_node)
|
118
|
+
@task.tests << test
|
119
|
+
end
|
120
|
+
|
121
|
+
def files_from_filerefs(filerefs_node)
|
122
|
+
[].tap do |files|
|
123
|
+
filerefs_node.search('fileref').each do |fileref_node|
|
124
|
+
fileref = fileref_node.attributes['refid'].value
|
125
|
+
files << @task.files.delete(@task.files.detect {|file| file.id == fileref })
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def validate
|
131
|
+
validator = ProformaXML::Validator.new @doc, @expected_version
|
132
|
+
validator.perform
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|