proformaxml 0.10.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/.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
|