dradis-projects 3.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.
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Dradis::Plugins::Projects::Engine.routes.draw do
2
+ resource :package, only: [:show]
3
+ resource :template, only: [:show]
4
+ end
@@ -0,0 +1,30 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+
3
+ require 'dradis/plugins/projects/version'
4
+
5
+ # Describe your gem and declare its dependencies:
6
+ Gem::Specification.new do |spec|
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.name = 'dradis-projects'
9
+ spec.version = Dradis::Plugins::Projects::VERSION::STRING
10
+ spec.summary = 'Project export/upload for the Dradis Framework.'
11
+ spec.description = 'This plugin allows you to dump the contents of the repo into a zip archive and restore the state from one of them.'
12
+
13
+ spec.license = 'GPL-2'
14
+
15
+ spec.authors = ['Daniel Martin']
16
+ spec.email = ['etd@nomejortu.com']
17
+ spec.homepage = 'http://dradisframework.org'
18
+
19
+ spec.files = `git ls-files`.split($\)
20
+ spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.6'
24
+ spec.add_development_dependency 'combustion'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec'
27
+
28
+ spec.add_dependency 'dradis-plugins', '~> 3.5'
29
+ spec.add_dependency 'rubyzip', '~> 1.1.0'
30
+ end
@@ -0,0 +1,5 @@
1
+ require 'zip'
2
+
3
+ require 'dradis-plugins'
4
+
5
+ require 'dradis/plugins/projects'
@@ -0,0 +1,15 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Projects
4
+ module Export; end
5
+ module Upload; end
6
+ end
7
+ end
8
+ end
9
+
10
+ require 'dradis/plugins/projects/engine'
11
+ require 'dradis/plugins/projects/export/package'
12
+ require 'dradis/plugins/projects/export/template'
13
+ require 'dradis/plugins/projects/upload/package'
14
+ require 'dradis/plugins/projects/upload/template'
15
+ require 'dradis/plugins/projects/version'
@@ -0,0 +1,31 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Projects
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Dradis::Plugins::Projects
6
+
7
+ include ::Dradis::Plugins::Base
8
+ description 'Save and restore project information'
9
+ provides :export, :upload
10
+
11
+ initializer 'dradis-projects.mount_engine' do
12
+ Rails.application.routes.append do
13
+ mount Dradis::Plugins::Projects::Engine => '/export/projects'
14
+ end
15
+ end
16
+
17
+ # Because this plugin provides two export modules, we have to overwrite
18
+ # the default .uploaders() method.
19
+ #
20
+ # See:
21
+ # Dradis::Plugins::Upload::Base in dradis-plugins
22
+ def self.uploaders
23
+ [
24
+ Dradis::Plugins::Projects::Upload::Package,
25
+ Dradis::Plugins::Projects::Upload::Template
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ module Dradis::Plugins::Projects::Export
2
+ class Package < Dradis::Plugins::Export::Base
3
+
4
+ # Create a new project export bundle. It will include an XML file with the
5
+ # contents of the repository (see db_only) and all the attachments that
6
+ # have been uploaded into the system.
7
+ def export(params={})
8
+ raise ":filename not provided" unless params.key?(:filename)
9
+
10
+ filename = params[:filename]
11
+ logger = params.fetch(:logger, Rails.logger)
12
+
13
+ File.delete(filename) if File.exists?(filename)
14
+
15
+ logger.debug{ "Creating a new Zip file in #{filename}..." }
16
+ Zip::File.open(filename, Zip::File::CREATE) do |zipfile|
17
+ Node.all.each do |node|
18
+ node_path = Attachment.pwd.join(node.id.to_s)
19
+
20
+ Dir["#{node_path}/**/**"].each do |file|
21
+ logger.debug{ "\tAdding attachment for '#{node.label}': #{file}" }
22
+ zipfile.add(file.sub("#{Attachment.pwd.to_s}/", ''), file)
23
+ end
24
+ end
25
+
26
+ logger.debug{ "\tAdding XML repository dump" }
27
+ template_exporter = Template.new(content_service: content_service)
28
+ template = template_exporter.export(params)
29
+ zipfile.get_output_stream('dradis-repository.xml') { |out|
30
+ out << template
31
+ }
32
+ end
33
+ logger.debug{ 'Done.' }
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,173 @@
1
+ module Dradis::Plugins::Projects::Export
2
+ class Template < Dradis::Plugins::Export::Base
3
+ # This method returns an XML representation of current repository which
4
+ # includes Categories, Nodes and Notes
5
+ def export(params={})
6
+ builder = Builder::XmlMarkup.new
7
+ builder.instruct!
8
+ result = builder.tag!('dradis-template') do |template_builder|
9
+ build_nodes(template_builder)
10
+ build_issues(template_builder)
11
+ build_methodologies(template_builder)
12
+ build_categories(template_builder)
13
+ build_tags(template_builder)
14
+ end
15
+ return result
16
+ end
17
+
18
+ private
19
+
20
+ def build_activities_for(builder, trackable)
21
+ builder.activities do |activities_builder|
22
+ trackable.activities.each do |activity|
23
+ activities_builder.activity do |activity_builder|
24
+ activity_builder.action(activity.action)
25
+ activity_builder.user_email(user_email_for_activity(activity))
26
+ activity_builder.created_at(activity.created_at.to_i)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def build_categories(builder)
33
+ categories = []
34
+ categories << Category.issue if @issues.any?
35
+ categories += @nodes.map do |node|
36
+ node.notes.map { |note| note.category }.uniq
37
+ end.flatten.uniq
38
+
39
+ builder.categories do |categories_builder|
40
+ categories.each do |category|
41
+ categories_builder.category do |category_builder|
42
+ category_builder.id(category.id)
43
+ category_builder.name(category.name)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def build_evidence_for_node(builder, node)
50
+ builder.evidence do |evidences_builder|
51
+ node.evidence.each do |evidence|
52
+ evidences_builder.evidence do |evidence_builder|
53
+ evidence_builder.id(evidence.id)
54
+ evidence_builder.author(evidence.author)
55
+ evidence_builder.tag!('issue-id', evidence.issue_id)
56
+ evidence_builder.content do
57
+ evidence_builder.cdata!(evidence.content)
58
+ end
59
+ build_activities_for(evidence_builder, evidence)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def build_issues(builder)
66
+ @issues = Issue.where(node_id: Node.issue_library).includes(:activities)
67
+
68
+ builder.issues do |issues_builder|
69
+ @issues.each do |issue|
70
+ issues_builder.issue do |issue_builder|
71
+ issue_builder.id(issue.id)
72
+ issue_builder.author(issue.author)
73
+ issue_builder.text do
74
+ issue_builder.cdata!(issue.text)
75
+ end
76
+ build_activities_for(issue_builder, issue)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def build_methodologies(builder)
83
+ methodologies = Node.methodology_library.notes
84
+ builder.methodologies do |methodologies_builder|
85
+ methodologies.each do |methodology|
86
+ methodologies_builder.methodology do |methodology_builder|
87
+ methodology_builder.text do
88
+ methodology_builder.cdata!(methodology.text)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def build_nodes(builder)
96
+ @nodes = Node.includes(:activities, :evidence, :notes, evidence: [:activities], notes: [:activities, :category]).all.reject do |node|
97
+ [Node::Types::METHODOLOGY,
98
+ Node::Types::ISSUELIB].include?(node.type_id)
99
+ end
100
+
101
+ builder.nodes do |nodes_builder|
102
+ @nodes.each do |node|
103
+ nodes_builder.node do |node_builder|
104
+ node_builder.id(node.id)
105
+ node_builder.label(node.label)
106
+ node_builder.tag!('parent-id', node.parent_id)
107
+ node_builder.position(node.position)
108
+ node_builder.properties do
109
+ node_builder.cdata!(node.raw_properties)
110
+ end
111
+ node_builder.tag!('type-id', node.type_id)
112
+ # Notes
113
+ build_notes_for_node(node_builder, node)
114
+ # Evidence
115
+ build_evidence_for_node(node_builder, node)
116
+ build_activities_for(node_builder, node)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def build_notes_for_node(builder, node)
123
+ builder.notes do |notes_builder|
124
+ node.notes.each do |note|
125
+ notes_builder.note do |note_builder|
126
+ note_builder.id(note.id)
127
+ note_builder.author(note.author)
128
+ note_builder.tag!('category-id', note.category_id)
129
+ note_builder.text do
130
+ note_builder.cdata!(note.text)
131
+ end
132
+ build_activities_for(note_builder, note)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def build_tags(builder)
139
+ tags = Tag.all
140
+ builder.tags do |tags_builder|
141
+ tags.each do |tag|
142
+ tags_builder.tag do |tag_builder|
143
+ tag_builder.id(tag.id)
144
+ tag_builder.name(tag.name)
145
+ tag_builder.taggings do |taggings_builder|
146
+ tag.taggings.each do |tagging|
147
+ taggings_builder.tagging do |tagging_builder|
148
+ tagging_builder.tag!('taggable-id', tagging.taggable_id)
149
+ tagging_builder.tag!('taggable-type', tagging.taggable_type)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+
159
+ # Cache user emails so we don't have to make an extra SQL request
160
+ # for every activity
161
+ def user_email_for_activity(activity)
162
+ return activity.user if activity.user.is_a?(String)
163
+
164
+ @user_emails ||= begin
165
+ User.select([:id, :email]).all.each_with_object({}) do |user, hash|
166
+ hash[user.id] = user.email
167
+ end
168
+ end
169
+ @user_emails[activity.user_id]
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,19 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Projects
4
+ # Returns the version of the currently loaded Projects as a <tt>Gem::Version</tt>
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 3
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ module Dradis::Plugins::Projects::Upload
2
+ module Package
3
+ def self.meta
4
+ package = Dradis::Plugins::Projects
5
+ {
6
+ name: package::Engine::plugin_name,
7
+ description: 'Upload Project package file (.zip)',
8
+ version: package.version
9
+ }
10
+ end
11
+
12
+ # In this module you will find the implementation details that enable you to
13
+ # upload a project archive (generated using ProjectExport::Processor::full_project)
14
+ class Importer < Dradis::Plugins::Upload::Importer
15
+
16
+ def import(params={})
17
+ package = params[:file]
18
+ success = false
19
+
20
+ # Unpack the archive in a temporary location
21
+ FileUtils.mkdir Rails.root.join('tmp', 'zip')
22
+
23
+ begin
24
+ logger.info { 'Uncompressing the file' }
25
+ #TODO: this could be improved by only uncompressing the XML, then parsing
26
+ # it to get the node_lookup table and then uncompressing each entry to its
27
+ # final destination
28
+ Zip::File.foreach(package) do |entry|
29
+ path = Rails.root.join('tmp', 'zip', entry.name)
30
+ FileUtils.mkdir_p(File.dirname(path))
31
+ entry.extract(path)
32
+ logger.info { "\t#{entry.name}" }
33
+ end
34
+ logger.info { 'Done.' }
35
+
36
+ logger.info { 'Loading XML state file' }
37
+ importer = Template::Importer.new(
38
+ content_service: content_service,
39
+ logger: logger,
40
+ template_service: template_service
41
+ )
42
+ node_lookup = importer.import(
43
+ file: Rails.root.
44
+ join('tmp', 'zip', 'dradis-repository.xml')
45
+ )
46
+
47
+ logger.info { 'Moving attachments to their final destinations' }
48
+ node_lookup.each do |oldid,newid|
49
+ if File.directory? Rails.root.join('tmp', 'zip', oldid)
50
+ FileUtils.mkdir_p Attachment.pwd.join(newid.to_s)
51
+
52
+ Dir.glob(Rails.root.join('tmp', 'zip', oldid, '*')).each do |attachment|
53
+ FileUtils.mv(attachment, Attachment.pwd.join(newid.to_s))
54
+ end
55
+ end
56
+ end
57
+
58
+ success = true
59
+ rescue Exception => e
60
+ logger.error { e.message }
61
+ success = false
62
+ ensure
63
+ # clean up the temporary files
64
+ FileUtils.rm_rf(Rails.root.join('tmp', 'zip'))
65
+ end
66
+
67
+ return success
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,364 @@
1
+ module Dradis::Plugins::Projects::Upload
2
+ module Template
3
+ def self.meta
4
+ package = Dradis::Plugins::Projects
5
+ {
6
+ name: package::Engine::plugin_name,
7
+ description: 'Upload Project template file (.xml)',
8
+ version: package.version
9
+ }
10
+ end
11
+
12
+ class Importer < Dradis::Plugins::Upload::Importer
13
+
14
+ # The import method is invoked by the framework to process a template file
15
+ # that has just been uploaded using the 'Import from file...' dialog.
16
+ #
17
+ # This module will take the XMl export file created with the ProjectExport
18
+ # module and dump the contents into the current database.
19
+ #
20
+ # Since we cannot ensure that the original node and category IDs as specified
21
+ # in the XML are free in this database, we need to keep a few lookup tables
22
+ # to maintain the original structure of Nodes and the Notes pointing to the
23
+ # right nodes and categories.
24
+ #
25
+ # This method also returns the Node lookup table so callers can understand
26
+ # what changes to the original IDs have been applied. This is mainly for the
27
+ # benefit of the ProjectPackageUpload module that would use the translation
28
+ # table to re-associate the attachments in the project archive with the new
29
+ # node IDs in the current project.
30
+ def import(params={})
31
+
32
+ # load the template
33
+ logger.info { "Loading template file from: #{params[:file]}" }
34
+ template = Nokogiri::XML(File.read(params[:file]))
35
+ logger.info { "Done." }
36
+
37
+ unless template.errors.empty?
38
+ logger.error { "Invalid project template format." }
39
+ return false
40
+ end
41
+
42
+ # we need this to be able to convert from old category_id to the new
43
+ # category_id once the categories are added to the DB (the ID may have
44
+ # changed)
45
+ category_lookup = {}
46
+ # the same applies to Nodes (think parent_id)
47
+ node_lookup = {}
48
+ # and to issues
49
+ issue_lookup = {}
50
+
51
+ # evidence is parsed when nodes are parsed, but cannot be saved until issues
52
+ # have been created. Therefore, parse evidence into arrays until time for
53
+ # creation
54
+ evidence_array = []
55
+
56
+ # likewise we also need to hold on to the XML about evidence activities
57
+ # until after the evidence has been saved
58
+ evidence_activity_xml_array = []
59
+
60
+ # all children nodes, we will need to find the new ID of their parents
61
+ orphan_nodes = []
62
+
63
+ # if the note has an attachment screenshot (i.e. !.../nodes/i/attachments/...!)
64
+ # we will fix the URL to point to the new Node ID.
65
+ #
66
+ # WARNING: we need a lookup table because one note may be referencing a
67
+ # different (yet unprocessed) node's attachments.
68
+ attachment_notes = []
69
+
70
+ # go through the categories, keep a translation table between the old
71
+ # category id and the new ones so we know to which category we should
72
+ # assign our notes
73
+ template.xpath('dradis-template/categories/category').each do |xml_category|
74
+ old_id = xml_category.at_xpath('id').text.strip
75
+ name = xml_category.at_xpath('name').text.strip
76
+ category = nil
77
+
78
+ # Prevent creating duplicate categories
79
+ logger.info { "Looking for category: #{name}" }
80
+ category = Category.find_or_create_by!(name: name)
81
+ category_lookup[old_id] = category.id
82
+ end
83
+
84
+
85
+ # ------------------------------------------------------------------- Nodes
86
+ # Re generate the Node tree structure
87
+ template.xpath('dradis-template/nodes/node').each do |xml_node|
88
+ element = xml_node.at_xpath('type-id')
89
+ type_id = element.text.nil? ? nil : element.text.strip
90
+ label = xml_node.at_xpath('label').text.strip
91
+ element = xml_node.at_xpath('parent-id')
92
+ parent_id = element.text.nil? ? nil : element.text.strip
93
+
94
+ # Node positions
95
+ element = xml_node.at_xpath('position')
96
+ position = (element && !element.text.nil?) ? element.text.strip : nil
97
+
98
+ # Node properties
99
+ element = xml_node.at_xpath('properties')
100
+ properties = (element && !element.text.blank?) ? element.text.strip : nil
101
+
102
+ created_at = xml_node.at_xpath('created-at')
103
+ updated_at = xml_node.at_xpath('updated-at')
104
+
105
+ logger.info { "New node detected: #{label}, parent_id: #{parent_id}, type_id: #{type_id}" }
106
+
107
+ # There is one exception to the rule, the Configuration.uploadsNode node,
108
+ # it does not make sense to have more than one of this nodes, in any
109
+ # given tree
110
+ node = nil
111
+ note = nil
112
+ evidence = nil
113
+ if (label == Configuration.plugin_uploads_node)
114
+ node = Node.create_with(type_id: type_id, parent_id: parent_id).
115
+ find_or_create_by!(label: label)
116
+ else
117
+ node = Node.create!(
118
+ type_id: type_id,
119
+ label: label,
120
+ parent_id: parent_id,
121
+ position: position
122
+ )
123
+ end
124
+
125
+ if properties
126
+ node.raw_properties = properties
127
+ end
128
+
129
+ node.update_attribute(:created_at, created_at.text.strip) if created_at
130
+ node.update_attribute(:updated_at, updated_at.text.strip) if updated_at
131
+
132
+ return false unless validate_and_save(node)
133
+ return false unless create_activities(node, xml_node)
134
+
135
+ xml_node.xpath('notes/note').each do |xml_note|
136
+
137
+ if xml_note.at_xpath('author') != nil
138
+ old_id = xml_note.at_xpath('category-id').text.strip
139
+ new_id = category_lookup[old_id]
140
+
141
+ created_at = xml_note.at_xpath('created-at')
142
+ updated_at = xml_note.at_xpath('updated-at')
143
+
144
+ logger.info { "Note category rewrite, used to be #{old_id}, now is #{new_id}" }
145
+ note = Note.create!(
146
+ author: xml_note.at_xpath('author').text.strip,
147
+ node_id: node.id,
148
+ category_id: new_id,
149
+ text: xml_note.at_xpath('text').text
150
+ )
151
+
152
+ note.update_attribute(:created_at, created_at.text.strip) if created_at
153
+ note.update_attribute(:updated_at, updated_at.text.strip) if updated_at
154
+
155
+ return false unless validate_and_save(note)
156
+
157
+ if note.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
158
+ attachment_notes << note
159
+ end
160
+
161
+ return false unless create_activities(note, xml_note)
162
+
163
+ logger.info { "\tNew note added detected." }
164
+ end
165
+ end
166
+
167
+ # Create array of evidence from xml input. Cannot store in DB until we
168
+ # have a new issue id
169
+ xml_node.xpath('evidence/evidence').each do |xml_evidence|
170
+ if xml_evidence.at_xpath('author') != nil
171
+ created_at = xml_evidence.at_xpath('created-at')
172
+ updated_at = xml_evidence.at_xpath('updated-at')
173
+
174
+ evidence = Evidence.new(
175
+ author: xml_evidence.at_xpath('author').text.strip,
176
+ node_id: node.id,
177
+ content: xml_evidence.at_xpath('content').text,
178
+ issue_id: xml_evidence.at_xpath('issue-id').text.strip
179
+ )
180
+
181
+ evidence.update_attribute(:created_at, created_at.text.strip) if created_at
182
+ evidence.update_attribute(:updated_at, updated_at.text.strip) if updated_at
183
+ evidence_array << evidence
184
+
185
+ evidence_activity_xml_array << xml_evidence.xpath("activities/activity")
186
+
187
+ logger.info { "\tNew evidence added." }
188
+ end
189
+ end
190
+
191
+ # keep track of reassigned ids
192
+ node_lookup[xml_node.at_xpath('id').text.strip] = node.id
193
+
194
+ if node.parent_id != nil
195
+ # keep track of orphaned nodes
196
+ orphan_nodes << node
197
+ end
198
+ end
199
+
200
+
201
+ # ------------------------------------------------------------------- Issues
202
+ issue = nil
203
+ issue_category = Category.issue
204
+ issue_library = Node.issue_library
205
+ # go through the issues, keep a translation table between the old
206
+ # issue id and the new ones. This is important for importing evidence
207
+ # Will need to adjust node ID after generating node structure
208
+ template.xpath('dradis-template/issues/issue').each do |xml_issue|
209
+ old_id = xml_issue.at_xpath('id').text.strip
210
+
211
+ # TODO: Need to find some way of checking for dups
212
+ # May be combination of text, category_id and created_at
213
+ issue = Issue.new
214
+ issue.author = xml_issue.at_xpath('author').text.strip
215
+ issue.text = xml_issue.at_xpath('text').text
216
+ issue.node = issue_library
217
+ issue.category = issue_category
218
+
219
+ return false unless validate_and_save(issue)
220
+
221
+ return false unless create_activities(issue, xml_issue)
222
+
223
+ if issue.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
224
+ attachment_notes << issue
225
+ end
226
+
227
+ issue_lookup[old_id] = issue.id
228
+ logger.info{ "New issue detected: #{issue.title}" }
229
+ end
230
+
231
+ # ----------------------------------------------------------- Methodologies
232
+ methodology_category = Category.default
233
+ methodology_library = Node.methodology_library
234
+ template.xpath('dradis-template/methodologies/methodology').each do |xml_methodology|
235
+ # FIXME: this is wrong in a few levels, we should be able to save a
236
+ # Methodology instance calling .save() but the current implementation
237
+ # of the model would consider this a 'methodology template' and not an
238
+ # instance.
239
+ #
240
+ # Also, methodology notes don't have a valid author, see
241
+ # MethodologiesController#create action (i.e. 'methodology builder' is
242
+ # used).
243
+ Note.create!(
244
+ author: 'methodology importer',
245
+ node_id: methodology_library.id,
246
+ category_id: methodology_category.id,
247
+ text: xml_methodology.at_xpath('text').text
248
+ )
249
+ end
250
+
251
+ # -------------------------------------------------------------------- Tags
252
+ template.xpath('dradis-template/tags/tag').each do |xml_tag|
253
+ name = xml_tag.at_xpath('name').text()
254
+ tag = Tag.find_or_create_by!(name: name)
255
+ @logger.info { "New tag detected: #{name}" }
256
+
257
+ xml_tag.xpath('./taggings/tagging').each do |xml_tagging|
258
+ old_taggable_id = xml_tagging.at_xpath('taggable-id').text()
259
+ taggable_type = xml_tagging.at_xpath('taggable-type').text()
260
+
261
+ new_taggable_id = case taggable_type
262
+ when 'Note'
263
+ issue_lookup[old_taggable_id]
264
+ end
265
+
266
+ Tagging.create! tag: tag, taggable_id: new_taggable_id, taggable_type: taggable_type
267
+ end
268
+ end
269
+
270
+ # ----------------------------------------------------------------- Wrap up
271
+
272
+ logger.info { "Wrapping up..." }
273
+
274
+ # Save the Evidence instance to the DB now that we have populated Issues
275
+ # the original issues
276
+ evidence_array.each_with_index do |evidence, i|
277
+ logger.info { "Setting issue_id for evidence" }
278
+ evidence.issue_id = issue_lookup[evidence.issue_id.to_s]
279
+
280
+ new_content = evidence.content.gsub(%r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}) do |_|
281
+ "!%s/nodes/%d/attachments/%s!" % [$1, node_lookup[$2], $3]
282
+ end
283
+ evidence.content = new_content
284
+
285
+ return false unless validate_and_save(evidence)
286
+
287
+ evidence_activity_xml_array[i].each do |xml_activity|
288
+ return false unless create_activity(evidence, xml_activity)
289
+ end
290
+ end
291
+
292
+ # Fix relationships between nodes to ensure parents and childrens match
293
+ # with the new assigned :ids
294
+ orphan_nodes.each do |node|
295
+ logger.info { "Finding parent for orphaned node: #{node.label}. Former parent was #{node.parent_id}" }
296
+ node.parent_id = node_lookup[node.parent_id.to_s]
297
+ return false unless validate_and_save(node)
298
+ end
299
+
300
+ # Adjust attachment URLs for new Node IDs
301
+ attachment_notes.each do |note|
302
+ @logger.info{ "Adjusting screenshot URLs: Note ##{note.id}" }
303
+ new_text = note.text.gsub(%r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}) do |_|
304
+ "!%s/nodes/%d/attachments/%s!" % [$1, node_lookup[$2], $3]
305
+ end
306
+ note.text = new_text
307
+ return false unless validate_and_save(note)
308
+ end
309
+
310
+ return node_lookup
311
+ end
312
+
313
+ private
314
+
315
+ def create_activities(trackable, xml_trackable)
316
+ xml_trackable.xpath('activities/activity').each do |xml_activity|
317
+ # if 'validate_and_save(activity)' returns false, it needs
318
+ # to bubble up to the 'import' method so we can stop execution
319
+ return false unless create_activity(trackable, xml_activity)
320
+ end
321
+ end
322
+
323
+ def create_activity(trackable, xml_activity)
324
+ activity = trackable.activities.new(
325
+ action: xml_activity.at_xpath("action").text,
326
+ created_at: Time.at(xml_activity.at_xpath("created_at").text.to_i)
327
+ )
328
+
329
+ set_activity_user(activity, xml_activity.at_xpath("user_email").text)
330
+
331
+ validate_and_save(activity)
332
+ end
333
+
334
+ def set_activity_user(activity, email)
335
+ if Activity.column_names.include?('user')
336
+ activity.user = email
337
+ else
338
+ activity.user_id = user_id_for_email(email)
339
+ end
340
+ end
341
+
342
+ # Cache users to cut down on excess SQL requests
343
+ def user_id_for_email(email)
344
+ return -1 if email.blank?
345
+ @users ||= begin
346
+ User.select([:id, :email]).all.each_with_object({}) do |user, hash|
347
+ hash[user.email] = user.id
348
+ end
349
+ end
350
+ @users[email] || -1
351
+ end
352
+
353
+ def validate_and_save(instance)
354
+ if instance.save
355
+ return true
356
+ else
357
+ @logger.info{ "Malformed #{ instance.class.name } detected: #{ instance.errors.full_messages }" }
358
+ return false
359
+ end
360
+ end
361
+
362
+ end
363
+ end
364
+ end