dradis-projects 4.6.0 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/app/controllers/dradis/plugins/projects/packages_controller.rb +6 -1
  4. data/app/controllers/dradis/plugins/projects/templates_controller.rb +6 -1
  5. data/app/views/dradis/plugins/projects/export/_index-content.html.erb +1 -1
  6. data/dradis-projects.gemspec +2 -2
  7. data/lib/dradis/plugins/projects/engine.rb +2 -2
  8. data/lib/dradis/plugins/projects/export/template.rb +1 -3
  9. data/lib/dradis/plugins/projects/export/v1/template.rb +8 -0
  10. data/lib/dradis/plugins/projects/export/v2/template.rb +8 -0
  11. data/lib/dradis/plugins/projects/export/v3/template.rb +8 -0
  12. data/lib/dradis/plugins/projects/export/v4/template.rb +207 -0
  13. data/lib/dradis/plugins/projects/gem_version.rb +1 -1
  14. data/lib/dradis/plugins/projects/upload/template.rb +2 -4
  15. data/lib/dradis/plugins/projects/upload/v1/template.rb +8 -0
  16. data/lib/dradis/plugins/projects/upload/v2/template.rb +8 -0
  17. data/lib/dradis/plugins/projects/upload/v3/template.rb +8 -0
  18. data/lib/dradis/plugins/projects/upload/v4/template.rb +651 -0
  19. data/spec/fixtures/files/with_invalid_states.xml +111 -0
  20. data/spec/fixtures/files/with_states.xml +111 -0
  21. data/spec/lib/dradis/plugins/projects/export/v2/template_spec.rb +9 -1
  22. data/spec/lib/dradis/plugins/projects/export/v4/template_spec.rb +84 -0
  23. data/spec/lib/dradis/plugins/projects/upload/v1/template_spec.rb +8 -0
  24. data/spec/lib/dradis/plugins/projects/upload/v2/template_spec.rb +9 -1
  25. data/spec/lib/dradis/plugins/projects/upload/v4/template_spec.rb +168 -0
  26. metadata +20 -10
@@ -0,0 +1,651 @@
1
+ module Dradis::Plugins::Projects::Upload::V4
2
+ module Template
3
+ class Importer < Dradis::Plugins::Projects::Upload::Template::Importer
4
+
5
+ attr_accessor :attachment_notes, :logger, :pending_changes, :users
6
+
7
+ ATTACHMENT_URL = %r{^!(/[a-z]+)?/(?:projects/\d+/)?nodes/(\d+)/attachments/(.+)!$}
8
+
9
+ def post_initialize(args={})
10
+ @lookup_table = {
11
+ categories: {},
12
+ nodes: {},
13
+ issues: {}
14
+ }
15
+
16
+ @pending_changes = {
17
+ # If the note has an attachment screenshot (i.e. !.../nodes/i/attachments/...!)
18
+ # we will fix the URL to point to the new Node ID.
19
+ #
20
+ # WARNING: we need a lookup table because one note may be referencing a
21
+ # different (yet unprocessed) node's attachments.
22
+ attachment_notes: [],
23
+
24
+ # evidence is parsed when nodes are parsed, but cannot be saved until
25
+ # issues have been created. Therefore, parse evidence into arrays until
26
+ # time for creation
27
+ evidence: [],
28
+
29
+ # likewise we also need to hold on to the XML about evidence activities
30
+ # and comments until after the evidence has been saved
31
+ evidence_activity: [],
32
+ evidence_comments: [],
33
+
34
+ # all children nodes, we will need to find the ID of their new parents.
35
+ orphan_nodes: []
36
+ }
37
+ end
38
+
39
+ private
40
+
41
+ def create_comments(commentable, xml_comments)
42
+ return true if xml_comments.empty?
43
+
44
+ xml_comments.each do |xml_comment|
45
+ author_email = xml_comment.at_xpath('author').text
46
+ comment = Comment.new(
47
+ commentable_id: commentable.id,
48
+ commentable_type: commentable.class.to_s,
49
+ content: xml_comment.at_xpath('content').text,
50
+ created_at: Time.at(xml_comment.at_xpath('created_at').text.to_i),
51
+ user_id: users[author_email]
52
+ )
53
+
54
+ if comment.user.nil?
55
+ comment.content = comment.content +
56
+ "\n\nOriginal author not available in this Dradis instance: "\
57
+ "#{author_email}."
58
+ end
59
+
60
+ unless validate_and_save(comment)
61
+ logger.info { "comment errors: #{comment.inspect}" }
62
+ return false
63
+ end
64
+ end
65
+ end
66
+
67
+ def create_activities(trackable, xml_trackable)
68
+ xml_trackable.xpath('activities/activity').each do |xml_activity|
69
+ # if 'validate_and_save(activity)' returns false, it needs
70
+ # to bubble up to the 'import' method so we can stop execution
71
+ return false unless create_activity(trackable, xml_activity)
72
+ end
73
+ end
74
+
75
+ def create_activity(trackable, xml_activity)
76
+ activity = trackable.activities.new(
77
+ action: xml_activity.at_xpath("action").text,
78
+ created_at: Time.at(xml_activity.at_xpath("created_at").text.to_i)
79
+ )
80
+
81
+ activity.project_id = project.id if activity.respond_to?(:project)
82
+
83
+ set_activity_user(activity, xml_activity.at_xpath("user_email").text)
84
+
85
+ validate_and_save(activity)
86
+ end
87
+
88
+ def create_issue(issue, xml_issue)
89
+ # TODO: Need to find some way of checking for dups
90
+ # May be combination of text, category_id and created_at
91
+ issue.author = xml_issue.at_xpath('author').text.strip
92
+ issue.state = xml_issue.at_xpath('state')&.text || :published
93
+ issue.text = xml_issue.at_xpath('text').text
94
+ issue.node = project.issue_library
95
+ issue.category = Category.issue
96
+
97
+ return false unless validate_and_save(issue)
98
+
99
+ return false unless create_activities(issue, xml_issue)
100
+
101
+ return false unless create_comments(issue, xml_issue.xpath('comments/comment'))
102
+
103
+ true
104
+ end
105
+
106
+ def finalize(template)
107
+ logger.info { 'Wrapping up...' }
108
+
109
+ finalize_nodes()
110
+ finalize_evidence()
111
+ finalize_attachments()
112
+
113
+ logger.info { 'Done.' }
114
+ end
115
+
116
+ def finalize_attachments
117
+ # Adjust attachment URLs for new Node IDs
118
+ pending_changes[:attachment_notes].each do |item|
119
+ text_attr =
120
+ if defined?(ContentBlock) && item.is_a?(ContentBlock)
121
+ :content
122
+ else
123
+ :text
124
+ end
125
+
126
+ logger.info { "Adjusting screenshot URLs: #{item.class.name} ##{item.id}" }
127
+
128
+ new_text = update_attachment_references(item.send(text_attr))
129
+ item.send(text_attr.to_s + "=", new_text)
130
+
131
+ raise "Couldn't save note attachment URL for #{item.class.name} ##{item.id}" unless validate_and_save(item)
132
+ end
133
+ end
134
+
135
+ # Save the Evidence instance to the DB now that we have populated the
136
+ # original issues.
137
+ def finalize_evidence
138
+ pending_changes[:evidence].each_with_index do |evidence, i|
139
+ logger.info { "Setting issue_id for evidence" }
140
+ evidence.issue_id = lookup_table[:issues][evidence.issue_id]
141
+
142
+ evidence.content = update_attachment_references(evidence.content)
143
+
144
+ raise "Couldn't save Evidence :issue_id / attachment URL Evidence ##{evidence.id}" unless validate_and_save(evidence)
145
+
146
+ pending_changes[:evidence_activity][i].each do |xml_activity|
147
+ raise "Couldn't create activity for Evidence ##{evidence.id}" unless create_activity(evidence, xml_activity)
148
+ end
149
+
150
+ xml_comments = pending_changes[:evidence_comments][i]
151
+ raise "Couldn't create comments for Evidence ##{evidence.id}" unless create_comments(evidence, xml_comments)
152
+ end
153
+ end
154
+
155
+ # Fix relationships between nodes to ensure parents and childrens match
156
+ # with the new assigned :ids
157
+ def finalize_nodes
158
+ pending_changes[:orphan_nodes].each do |node|
159
+ logger.info { "Finding parent for orphaned node: #{node.label}. Former parent was #{node.parent_id}" }
160
+ node.parent_id = lookup_table[:nodes][node.parent_id]
161
+ raise "Couldn't save node parent for Node ##{node.id}" unless validate_and_save(node)
162
+ end
163
+ end
164
+
165
+ # Go through the categories, keep a translation table between the old
166
+ # category id and the new ones so we know to which category we should
167
+ # assign our notes
168
+ def parse_categories(template)
169
+ logger.info { 'Processing Categories...' }
170
+
171
+ template.xpath('dradis-template/categories/category').each do |xml_category|
172
+ old_id = Integer(xml_category.at_xpath('id').text.strip)
173
+ name = xml_category.at_xpath('name').text.strip
174
+ category = nil
175
+
176
+ # Prevent creating duplicate categories
177
+ logger.info { "Looking for category: #{name}" }
178
+ category = Category.find_or_create_by!(name: name)
179
+ lookup_table[:categories][old_id] = category.id
180
+ end
181
+
182
+ logger.info { 'Done.' }
183
+ end
184
+
185
+ # Go through the issues, keep a translation table between the old
186
+ # issue id and the new ones. This is important for importing evidence
187
+ # Will need to adjust node ID after generating node structure
188
+ def parse_issues(template)
189
+ issue = nil
190
+
191
+ logger.info { 'Processing Issues...' }
192
+
193
+ template.xpath('dradis-template/issues/issue').each do |xml_issue|
194
+ issue = Issue.new
195
+
196
+ return false unless create_issue(issue, xml_issue)
197
+
198
+ if issue.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
199
+ pending_changes[:attachment_notes] << issue
200
+ end
201
+
202
+ old_id = Integer(xml_issue.at_xpath('id').text.strip)
203
+ lookup_table[:issues][old_id] = issue.id
204
+ logger.info{ "New issue detected: #{issue.title}" }
205
+ end
206
+
207
+ logger.info { 'Done.' }
208
+
209
+ end
210
+
211
+ def parse_methodologies(template)
212
+ methodology_category = Category.default
213
+ methodology_library = project.methodology_library
214
+
215
+ logger.info { 'Processing Methodologies...' }
216
+
217
+ template.xpath('dradis-template/methodologies/methodology').each do |xml_methodology|
218
+ # FIXME: this is wrong in a few levels, we should be able to save a
219
+ # Methodology instance calling .save() but the current implementation
220
+ # of the model would consider this a 'methodology template' and not an
221
+ # instance.
222
+ #
223
+ # Also, methodology notes don't have a valid author, see
224
+ # MethodologiesController#create action (i.e. 'methodology builder' is
225
+ # used).
226
+ Note.create!(
227
+ author: 'methodology importer',
228
+ node_id: methodology_library.id,
229
+ category_id: methodology_category.id,
230
+ text: xml_methodology.at_xpath('text').text
231
+ )
232
+ end
233
+
234
+ logger.info { 'Done.' }
235
+ end
236
+
237
+ def parse_node(xml_node)
238
+ element = xml_node.at_xpath('type-id')
239
+ type_id = element.text.nil? ? nil : element.text.strip
240
+ label = xml_node.at_xpath('label').text.strip
241
+ element = xml_node.at_xpath('parent-id')
242
+ parent_id = element.text.blank? ? nil : element.text.strip
243
+
244
+ # Node positions
245
+ element = xml_node.at_xpath('position')
246
+ position = (element && !element.text.nil?) ? element.text.strip : nil
247
+
248
+ # Node properties
249
+ element = xml_node.at_xpath('properties')
250
+ properties = (element && !element.text.blank?) ? element.text.strip : nil
251
+
252
+ created_at = xml_node.at_xpath('created-at')
253
+ updated_at = xml_node.at_xpath('updated-at')
254
+
255
+ logger.info { "New node detected: #{label}, parent_id: #{parent_id}, type_id: #{type_id}" }
256
+
257
+ # There are exceptions to the rule, when it does not make sense to have
258
+ # more than one of this nodes, in any given tree:
259
+ # - the Configuration.uploadsNode node (detected by its label)
260
+ # - any nodes with type different from DEFAULT or HOST
261
+ if label == Configuration.plugin_uploads_node
262
+ node = project.nodes.create_with(type_id: type_id, parent_id: parent_id)
263
+ .find_or_create_by!(label: label)
264
+ elsif Node::Types::USER_TYPES.exclude?(type_id.to_i)
265
+ node = project.nodes.create_with(label: label)
266
+ .find_or_create_by!(type_id: type_id)
267
+ else
268
+ # We don't want to validate child nodes here yet since they always
269
+ # have invalid parent id's. They'll eventually be validated in the
270
+ # finalize_nodes method.
271
+ has_nil_parent = !parent_id
272
+ node =
273
+ project.nodes.new(
274
+ type_id: type_id,
275
+ label: label,
276
+ parent_id: parent_id,
277
+ position: position
278
+ )
279
+ node.save!(validate: has_nil_parent)
280
+ pending_changes[:orphan_nodes] << node if parent_id
281
+ end
282
+
283
+ if properties
284
+ node.raw_properties = properties
285
+ node.save!(validate: has_nil_parent)
286
+ end
287
+
288
+ node.update_attribute(:created_at, created_at.text.strip) if created_at
289
+ node.update_attribute(:updated_at, updated_at.text.strip) if updated_at
290
+
291
+ raise "Couldn't create activities for Node ##{node.id}" unless create_activities(node, xml_node)
292
+
293
+ parse_node_notes(node, xml_node)
294
+ parse_node_evidence(node, xml_node)
295
+
296
+ node
297
+ end
298
+
299
+ def parse_nodes(template)
300
+ logger.info { 'Processing Nodes...' }
301
+
302
+ # Re generate the Node tree structure
303
+ template.xpath('dradis-template/nodes/node').each do |xml_node|
304
+
305
+ node = parse_node(xml_node)
306
+
307
+ # keep track of reassigned ids
308
+ # Convert the id to an integer as it has no place being a string, or
309
+ # directory path. We later use this ID to build a directory structure
310
+ # to place attachments and without validation opens the potential for
311
+ # path traversal.
312
+ node_original_id = Integer(xml_node.at_xpath('id').text.strip)
313
+ lookup_table[:nodes][node_original_id] = node.id
314
+ end
315
+
316
+ logger.info { 'Done.' }
317
+ end
318
+
319
+ # Create array of evidence from xml input. Cannot store in DB until we
320
+ # have a new issue id
321
+ def parse_node_evidence(node, xml_node)
322
+ xml_node.xpath('evidence/evidence').each do |xml_evidence|
323
+ if xml_evidence.at_xpath('author') != nil
324
+ created_at = xml_evidence.at_xpath('created-at')
325
+ updated_at = xml_evidence.at_xpath('updated-at')
326
+
327
+ evidence = Evidence.new(
328
+ author: xml_evidence.at_xpath('author').text.strip,
329
+ node_id: node.id,
330
+ content: xml_evidence.at_xpath('content').text,
331
+ issue_id: xml_evidence.at_xpath('issue-id').text.strip
332
+ )
333
+
334
+ evidence.update_attribute(:created_at, created_at.text.strip) if created_at
335
+ evidence.update_attribute(:updated_at, updated_at.text.strip) if updated_at
336
+
337
+ pending_changes[:evidence] << evidence
338
+ pending_changes[:evidence_activity] << xml_evidence.xpath('activities/activity')
339
+ pending_changes[:evidence_comments] << xml_evidence.xpath('comments/comment')
340
+
341
+ logger.info { "\tNew evidence added." }
342
+ end
343
+ end
344
+ end
345
+
346
+ def parse_node_notes(node, xml_node)
347
+ xml_node.xpath('notes/note').each do |xml_note|
348
+
349
+ if xml_note.at_xpath('author') != nil
350
+ old_id = Integer(xml_note.at_xpath('category-id').text.strip)
351
+ new_id = lookup_table[:categories][old_id]
352
+
353
+ created_at = xml_note.at_xpath('created-at')
354
+ updated_at = xml_note.at_xpath('updated-at')
355
+
356
+ logger.info { "Note category rewrite, used to be #{old_id}, now is #{new_id}" }
357
+ note = Note.create!(
358
+ author: xml_note.at_xpath('author').text.strip,
359
+ node_id: node.id,
360
+ category_id: new_id,
361
+ text: xml_note.at_xpath('text').text
362
+ )
363
+
364
+ note.update_attribute(:created_at, created_at.text.strip) if created_at
365
+ note.update_attribute(:updated_at, updated_at.text.strip) if updated_at
366
+
367
+ raise "Couldn't save Note" unless validate_and_save(note)
368
+
369
+ if note.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
370
+ pending_changes[:attachment_notes] << note
371
+ end
372
+
373
+ raise "Couldn't create activities for Note ##{note.id}" unless create_activities(note, xml_note)
374
+ raise "Couldn't create comments for Note ##{note.id}" unless create_comments(note, xml_note.xpath('comments/comment'))
375
+
376
+ logger.info { "\tNew note added." }
377
+ end
378
+ end
379
+ end
380
+
381
+ def parse_report_content(template); end
382
+
383
+ def parse_tags(template)
384
+ logger.info { 'Processing Tags...' }
385
+
386
+ template.xpath('dradis-template/tags/tag').each do |xml_tag|
387
+ name = xml_tag.at_xpath('name').text()
388
+ tag_params = { name: name }
389
+ tag_params[:project_id] = project.id if Tag.has_attribute?(:project_id)
390
+ tag = Tag.where(tag_params).first_or_create
391
+ logger.info { "New tag detected: #{name}" }
392
+
393
+ xml_tag.xpath('./taggings/tagging').each do |xml_tagging|
394
+ old_taggable_id = Integer(xml_tagging.at_xpath('taggable-id').text())
395
+ taggable_type = xml_tagging.at_xpath('taggable-type').text()
396
+
397
+ new_taggable_id = case taggable_type
398
+ when 'Note'
399
+ lookup_table[:issues][old_taggable_id]
400
+ end
401
+
402
+ Tagging.create! tag: tag,
403
+ taggable_id: new_taggable_id, taggable_type: taggable_type
404
+ end
405
+ end
406
+
407
+ logger.info { 'Done.' }
408
+ end
409
+
410
+ def set_activity_user(activity, email)
411
+ if Activity.column_names.include?('user')
412
+ activity.user = email
413
+ else
414
+ activity.user_id = user_id_for_email(email)
415
+ end
416
+ end
417
+
418
+ def update_attachment_references(string)
419
+ string.gsub(ATTACHMENT_URL) do |attachment|
420
+ node_id = lookup_table[:nodes][$2.to_i]
421
+ if node_id
422
+ "!%s/projects/%d/nodes/%d/attachments/%s!" % [$1, project.id, node_id, $3]
423
+ else
424
+ logger.error { "The attachment wasn't included in the package: #{attachment}" }
425
+ attachment
426
+ end
427
+ end
428
+ end
429
+
430
+ def user_id_for_email(email)
431
+ users[email] || @default_user_id
432
+ end
433
+
434
+ # Cache users to cut down on excess SQL requests
435
+ def users
436
+ @users ||= begin
437
+ User.select([:id, :email]).all.each_with_object({}) do |user, hash|
438
+ hash[user.email] = user.id
439
+ end
440
+ end
441
+ end
442
+
443
+ def validate_and_save(instance)
444
+ if instance.save
445
+ return true
446
+ else
447
+ @logger.info{ "Malformed #{ instance.class.name } detected: #{ instance.errors.full_messages }" }
448
+ return false
449
+ end
450
+ end
451
+
452
+ # Private: Given a XML node contianing assignee information this method
453
+ # tries to recreate the assignment in the new project.
454
+ #
455
+ # * If the user exists in this instance: assign the card to that user
456
+ # (no matter if the user is not a project author).
457
+ # * If the user doesn't exist, don't creat an assiment and add a note
458
+ # inside the card's description.
459
+ #
460
+ # card - the Card object we're creating assignments for.
461
+ # xml_assignee - the Nokogiri::XML::Node that contains node assignment
462
+ # information.
463
+ #
464
+ # Returns nothing, but creates a new Assignee for this card.
465
+ def create_assignee(card, xml_assignee)
466
+ email = xml_assignee.text()
467
+ user_id = user_id_for_email(email)
468
+
469
+ if user_id == -1
470
+ old_assignee_field = card.fields['FormerAssignees'] || ''
471
+ card.set_field 'FormerAssignees', old_assignee_field << "* #{email}\n"
472
+ else
473
+ old_assignee_ids = card.assignee_ids
474
+ card.assignee_ids = old_assignee_ids + [user_id]
475
+ end
476
+ end
477
+
478
+ # Private: Reassign cross-references once all the objects in the project
479
+ # have been recreated.
480
+ #
481
+ # No arguments received, but the methods relies on :lookup_table and
482
+ # :pending_changes provided by dradis-projects.
483
+ #
484
+ # Returns nothing.
485
+ def finalize_cards
486
+ logger.info { 'Reassigning card positions...' }
487
+
488
+ # Fix the :previous_id with the new card IDs
489
+ pending_changes[:cards].each do |card|
490
+ card.previous_id = lookup_table[:cards][card.previous_id]
491
+ raise "Couldn't save card's position" unless validate_and_save(card)
492
+ end
493
+
494
+ logger.info { 'Done.' }
495
+ end
496
+
497
+ # Private: Reassign the List's :previous_id now that we know what are the
498
+ # new IDs that correspond to all List objects in the import.
499
+ #
500
+ # No arguments received, but the method relies on :lookup_table and
501
+ # :pending_changes provided by dradis-projects.
502
+ #
503
+ # Returns nothing.
504
+ def finalize_lists
505
+ logger.info { 'Reassigning list positions...' }
506
+
507
+ # Fix the :previous_id with the new card IDs
508
+ pending_changes[:lists].each do |list|
509
+ list.previous_id = lookup_table[:lists][list.previous_id]
510
+ raise "Couldn't save list's position" unless validate_and_save(list)
511
+ end
512
+
513
+ logger.info { 'Done.' }
514
+ end
515
+
516
+ # Private: Restore Board, List and Card information from the project
517
+ # template.
518
+ def parse_methodologies(template)
519
+ if template_version == 1
520
+ # Restore Board from old xml methodology format
521
+ process_v1_methodologies(template)
522
+ else
523
+ process_v2_methodologies(template)
524
+ end
525
+ end
526
+
527
+ # Private: For each XML card block, we're creating a new Card instance,
528
+ # restoring the card's Activities and Assignments.
529
+ #
530
+ # list - the List instance that will hold this Card.
531
+ # xml_card - the Nokogiri::XML node containing the card's data.
532
+ #
533
+ # Returns nothing, but makes use of the :lookup_table and :pending_changes
534
+ # variables to store information that will be used during the
535
+ # :finalize_cards method.
536
+ def process_card(list, xml_card)
537
+ due_date = xml_card.at_xpath('due_date').text
538
+ due_date = Date.iso8601(due_date) unless due_date.empty?
539
+
540
+ card = list.cards.create name: xml_card.at_xpath('name').text,
541
+ description: xml_card.at_xpath('description').text,
542
+ due_date: due_date,
543
+ previous_id: xml_card.at_xpath('previous_id').text&.to_i
544
+
545
+ xml_card.xpath('activities/activity').each do |xml_activity|
546
+ raise "Couldn't create activity for Card ##{card.id}" unless create_activity(card, xml_activity)
547
+ end
548
+
549
+ xml_card.xpath('assignees/assignee').each do |xml_assignee|
550
+ raise "Couldn't create assignment for Card ##{card.id}" unless create_assignee(card, xml_assignee)
551
+ end
552
+
553
+ raise "Couldn't create comments for Card ##{card.id}" unless create_comments(card, xml_card.xpath('comments/comment'))
554
+
555
+ xml_id = Integer(xml_card.at_xpath('id').text)
556
+ lookup_table[:cards][xml_id] = card.id
557
+ pending_changes[:cards] << card
558
+ end
559
+
560
+ # Private: Initial pass over ./methodologies/ section of the tempalte
561
+ # document to extract Board, List and Card information. Some of the
562
+ # objects will contain invalid references (e.g. the former :previous_id
563
+ # of a card will need to be reassigned) that we will fix at a later stage.
564
+ #
565
+ # template - A Nokogiri::XML document containing the project template
566
+ # data.
567
+ #
568
+ # Returns nothing.
569
+ def process_methodologies(template)
570
+ logger.info { 'Processing Methodologies...' }
571
+
572
+ lookup_table[:cards] = {}
573
+ lookup_table[:lists] = {}
574
+ pending_changes[:cards] = []
575
+ pending_changes[:lists] = []
576
+
577
+ template.xpath('dradis-template/methodologies/board').each do |xml_board|
578
+ xml_node_id = xml_board.at_xpath('node_id').try(:text)
579
+ node_id =
580
+ if xml_node_id.present?
581
+ lookup_table[:nodes][xml_node_id.to_i]
582
+ else
583
+ project.methodology_library.id
584
+ end
585
+
586
+ board = content_service.create_board(
587
+ name: xml_board.at_xpath('name').text,
588
+ node_id: node_id
589
+ )
590
+
591
+ xml_board.xpath('./list').each do |xml_list|
592
+ list = board.lists.create name: xml_list.at_xpath('name').text,
593
+ previous_id: xml_list.at_xpath('previous_id').text&.to_i
594
+ xml_id = Integer(xml_list.at_xpath('id').text)
595
+
596
+ lookup_table[:lists][xml_id] = list.id
597
+ pending_changes[:lists] << list
598
+
599
+ xml_list.xpath('./card').each do |xml_card|
600
+ process_card(list, xml_card)
601
+ end
602
+ end
603
+ end
604
+
605
+ logger.info { 'Done.' }
606
+ end
607
+
608
+ # Private: Pass over old ./methodologies/ sections of the template
609
+ # document to extract Board, List and Card information.
610
+ #
611
+ # template - A Nokogiri::XML document containing the project template
612
+ # data.
613
+ #
614
+ # Returns nothing.
615
+ def process_v1_methodologies(template)
616
+ xml_methodologies = template.xpath('dradis-template/methodologies/methodology')
617
+ return if xml_methodologies.empty?
618
+
619
+ logger.info { 'Processing V1 Methodologies...' }
620
+
621
+ migration = MethodologyMigrationService.new(project.id)
622
+
623
+ xml_methodologies.each do |xml_methodology|
624
+ migration.migrate(
625
+ Methodology.new(content: xml_methodology.at_xpath('text').text)
626
+ )
627
+ end
628
+
629
+ logger.info { 'Done.' }
630
+ end
631
+
632
+ # Private: Pass over new ./methodologies/ sections of the template
633
+ # document to extract Board, List and Card information.
634
+ #
635
+ # template - A Nokogiri::XML document containing the project template
636
+ # data.
637
+ #
638
+ # Returns nothing.
639
+ def process_v2_methodologies(template)
640
+ # Restore Board
641
+ process_methodologies(template)
642
+
643
+ # Reassign Card's :previous_id and :assginees
644
+ finalize_cards()
645
+
646
+ # Reassign List's :previous id
647
+ finalize_lists()
648
+ end
649
+ end
650
+ end
651
+ end