File: PropertyTreeNode.rb

package info (click to toggle)
tj3 3.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,048 kB
  • sloc: ruby: 36,481; javascript: 1,113; sh: 19; makefile: 17
file content (761 lines) | stat: -rw-r--r-- 26,176 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = PropertyTreeNode.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019
#               by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'taskjuggler/MessageHandler'

class TaskJuggler

  # This class is the base object for all Project properties. A Project property
  # is a e. g. a Task, a Resource or other objects. Such properties can be
  # arranged in tree form by assigning child properties to an existing property.
  # The parent object needs to exist at object creation time. The
  # PropertyTreeNode class holds all data and methods that are common to the
  # different types of properties. Each property can have a set of predifined
  # attributes. The PropertySet class holds collections of the same
  # PropertyTreeNode objects and the defined attributes.
  # Each PropertySet has a predefined set of attributes, but the attribute set
  # can be extended by the user. E.g. a task has the predefined attribute
  # 'start' and 'end' date. The user can extend tasks with a user defined
  # attribute like an URL that contains more details about the task.
  class PropertyTreeNode

    include MessageHandler

    attr_reader :propertySet, :id, :subId, :parent, :project, :sequenceNo,
                :children, :adoptees
    attr_accessor :name, :sourceFileInfo
    attr_reader :data

    # Create a new PropertyTreeNode object. _propertySet_ is the PropertySet
    # that this PropertyTreeNode object belongs to. The PropertySet determines
    # the attributes that are common to all Nodes in the set. _id_ is a String
    # that is unique in the namespace of the set. _name_ is a user readable,
    # short description of the object. _parent_ is the PropertyTreeNode that
    # sits above this node in the object hierachy. A root object has a _parent_
    # of nil. For sets with hierachical name spaces, parent can be nil and
    # specified by a hierachical _id_ (e. g. 'father.son').
    def initialize(propertySet, id, name, parent)
      @propertySet = propertySet
      @project = propertySet.project
      @parent = parent

      # Scenario specific data
      @data = nil

      # Attributes are created on-demand. We need to be careful that a pure
      # check for existance does not create them unecessarily.
      @attributes = Hash.new do |hash, attributeId|
        unless (aType = attributeDefinition(attributeId))
          raise ArgumentError,
            "Unknown attribute '#{attributeId}' requested for " +
            "#{self.class.to_s.sub(/TaskJuggler::/, '')} '#{fullId}'"
        end
        unless aType.scenarioSpecific
          hash[attributeId] = aType.objClass.new(@propertySet, aType, self)
        else
          raise ArgumentError, "Attribute '#{attributeId}' is scenario specific"
        end
      end
      @scenarioAttributes = Array.new(@project.scenarioCount) do |scenarioIdx|
        Hash.new do |hash, attributeId|
          unless (aType = attributeDefinition(attributeId))
            raise ArgumentError,
              "Unknown attribute '#{attributeId}' requested for " +
              "#{self.class.to_s.sub(/TaskJuggler::/, '')} '#{fullId}'"
          end
          if aType.scenarioSpecific
            hash[attributeId] = aType.objClass.new(@propertySet, aType,
                                                   @data[scenarioIdx])
          else
            raise ArgumentError,
              "Attribute '#{attributeId}' is not scenario specific"
          end
        end
      end

      # If _id_ is still nil, we generate a unique id.
      unless id
        tag = self.class.to_s.gsub(/TaskJuggler::/, '')
        id = '_' + tag + '_' + (propertySet.items + 1).to_s
        id = parent.fullId + '.' + id if !@propertySet.flatNamespace && parent
      end
      if !@propertySet.flatNamespace && id.include?('.')
        parentId = id[0..(id.rindex('.') - 1)]
        # Set parent to the parent property if it's still nil.
        @parent = @propertySet[parentId] unless @parent
        if $DEBUG
          if !@parent || !@propertySet[@parent.fullId]
            raise "Fatal Error: parent must be member of same property set"
          end
          if parentId != @parent.fullId
            raise "Fatal Error: parent (#{@parent.fullId}) and parent ID " +
                "(#{@parentId}) don't match"
          end
        end
        @subId = id[(id.rindex('.') + 1).. -1]
      else
        @subId = id
      end
      # The attribute 'id' is either the short ID or the full hierarchical ID.
      set('id', fullId)

      # The name of the property.
      @name = name
      set('name', name)

      @level = -1
      @sourceFileInfo = nil

      @sequenceNo = @propertySet.items + 1
      set('seqno', @sequenceNo)
      # This is a list of the real sub nodes of this PropertyTreeNode.
      @children = []
      # This is a list of the adopted sub nodes of this PropertyTreeNode.
      @adoptees = []
      # In case we have a parent object, we register this object as child of
      # the parent.
      if (@parent)
        @parent.addChild(self)
      end
      # This is a list of the PropertyTreeNode objects that have adopted this
      # node.
      @stepParents = []
    end

    # We only use deep_clone for attributes, never for properties. Since
    # attributes may reference properties these references should remain
    # references.
    def deep_clone
      self
    end

    # We often use PTNProxy objects to represent PropertyTreeNode objects. The
    # proxy usually does a good job acting like a PropertyTreeNode. But in
    # some situations, we want to make sure to operate on the PropertyTreeNode
    # and not the PTNProxy. Both classes provide a ptn() method that always
    # return the PropertyTreeNode.
    def ptn
      self
    end

    # Adopt _property_ as a step child. Also register the new relationship
    # with the child.
    def adopt(property)
      # A property cannot adopt itself.
      if self == property
        error('adopt_self', 'A property cannot adopt itself')
      end

      # A top level task must never contain the same leaf task more then once!
      allOfRoot = root.all
      property.allLeaves.each do |adoptee|
        if allOfRoot.include?(adoptee)
          error('adopt_duplicate_child',
                "The task '#{adoptee.fullId}' has already been adopted by " +
                "property '#{root.fullId}' or any of its sub-properties.")
        end
      end

      @adoptees << property
      property.getAdopted(self)
    end

    # Get adopted by _property_. Also register the new relationship with the
    # step parent. This method is for internal use only. Other classes should
    # alway use PropertyTreeNode::adopt().
    def getAdopted(property) # :nodoc:
      return if @stepParents.include?(property)

      @stepParents << property
    end

    # Return a list of all children including adopted kids.
    def kids
      @children + @adoptees
    end

    # Return a list of all parents including step parents.
    def parents
      (@parent ? [ @parent ] : []) + @stepParents
    end

    # This method creates a shallow copy of all attributes and returns them as
    # an Array that can be used with restoreAttributes().
    def backupAttributes
      [ @attributes.clone, @scenarioAttributes.clone ]
    end

    # Restore the attributes to a previously saved state. _backup_ is an Array
    # generated by backupAttributes().
    def restoreAttributes(backup)
      @attributes, @scenarioAttributes = backup
    end

    # Remove any references in the stored data that references the _property_.
    def removeReferences(property)
      @children.delete(property)
      @adoptees.delete(property)
      @stepParents.delete(property)
    end

    # Return the index of the child _node_.
    def levelSeqNo(node)
      @children.index(node) + 1
    end

    # Inherit values for the attributes from the parent node or the Project.
    def inheritAttributes
      # Inherit non-scenario-specific values
      @propertySet.eachAttributeDefinition do |attrDef|
        next if attrDef.scenarioSpecific || !attrDef.inheritedFromParent

        aId = attrDef.id
        if parent
          # Inherit values from parent property
          if parent.provided(aId) || parent.inherited(aId)
            @attributes[aId].inherit(parent.get(aId))
          end
        else
          # Inherit selected values from project if top-level property
          if attrDef.inheritedFromProject
            if @project[aId]
              @attributes[aId].inherit(@project[aId])
            end
          end
        end
      end

      # Inherit scenario-specific values
      @propertySet.eachAttributeDefinition do |attrDef|
        next if !attrDef.scenarioSpecific || !attrDef.inheritedFromParent

        @project.scenarioCount.times do |scenarioIdx|
          if parent
            # Inherit scenario specific values from parent property
            if parent.provided(attrDef.id, scenarioIdx) ||
               parent.inherited(attrDef.id, scenarioIdx)
              @scenarioAttributes[scenarioIdx][attrDef.id].inherit(
                  parent[attrDef.id, scenarioIdx])
            end
          else
            # Inherit selected values from project if top-level property
            if attrDef.inheritedFromProject
              if @project[attrDef.id] &&
                 @scenarioAttributes[scenarioIdx][attrDef.id]
                @scenarioAttributes[scenarioIdx][attrDef.id].inherit(
                    @project[attrDef.id])
              end
            end
          end
        end
      end
    end

    # Returns a list of this node and all transient sub nodes.
    def all
      res = [ self ]
      kids.each do |c|
        res = res.concat(c.all)
      end
      res
    end

    # Return a list of all leaf nodes of this node.
    def allLeaves(withoutSelf = false)
      res = []
      if leaf?
        res << self unless withoutSelf
      else
        kids.each do |c|
          res += c.allLeaves
        end
      end
      res
    end

    def logicalId
      fullId
    end

    # Return the full id of this node. For PropertySet objects with a flat
    # namespace, this is just the ID. Otherwise, the full ID is composed of all
    # IDs from the root node to this node, separating the IDs by a dot.
    def fullId
      res = @subId
      unless @propertySet.flatNamespace
        t = self
        until (t = t.parent).nil?
          res = t.subId + "." + res
        end
      end
      res
    end

    # Returns the level that this property is on. Top-level properties return
    # 0, their children 1 and so on. This value is cached internally, so it does
    # not have to be calculated each time the function is called.
    def level
      return @level if @level >= 0

      t = self
      @level = 0
      until (t = t.parent).nil?
        @level += 1
      end
      @level
    end

    # Return the hierarchical index of this node. In project management lingo
    # this is called the Breakdown Structure Index (BSI). The result is an Array
    # with an index for each level from the root to this node.
    def getBSIndicies
      idcs = []
      p = self
      begin
        parent = p.parent
        idcs.insert(0, parent ? parent.levelSeqNo(p) :
                                @propertySet.levelSeqNo(p))
        p = parent
      end while p
      idcs
    end

    # Return the 'index' attributes of this property, prefixed by the 'index'
    # attributes of all its parents. The result is an Array of Integers.
    def getIndicies
      idcs = []
      p = self
      begin
        parent = p.parent
        idcs.insert(0, p.get('index'))
        p = parent
      end while p
      idcs
    end

    # Add _child_ node as child to this node.
    def addChild(child)
      if $DEBUG && child.propertySet != @propertySet
        raise "Child nodes must belong to the same property set as the parent"
      end
      @children.push(child)
    end

    # Find out if this property is a direct or indirect child of _ancestor_.
    def isChildOf?(ancestor)
      parent = self
      while parent = parent.parent
        return true if (parent == ancestor)
      end
      false
    end

    # Return true if the node is a leaf node (has no children).
    def leaf?
      @children.empty? && @adoptees.empty?
    end

    # Return true if the node has children.
    def container?
      !@children.empty? || !@adoptees.empty?
    end

    # Return a list with all parent nodes of this node.
    def ancestors(includeStepParents = false)
      nodes = []
      if includeStepParents
        parents.each do |parent|
          nodes << parent
          nodes += parent.ancestors(true)
        end
      else
        n = self
        while n.parent
          nodes << (n = n.parent)
        end
      end
      nodes
    end

    # Return the top-level node for this node.
    def root
      n = self
      while n.parent
        n = n.parent
      end
      n
    end

    # Return the type of the attribute with ID _attributeId_.
    def attributeDefinition(attributeId)
      @propertySet.attributeDefinitions[attributeId]
    end

    # Return the value of the non-scenario-specific attribute with ID
    # _attributeId_. This method works for built-in attributes as well.
    # In case the attribute does not exist, an exception is raised.
    def get(attributeId)
      # Make sure the attribute gets created if it doesn't exist already.
      @attributes[attributeId]
      instance_variable_get(('@' + attributeId).intern)
    end

    # Return the value of the attribute with ID _attributeId_. This method
    # works for built-in attributes as well. In case this is a
    # scenario-specific attribute, the scenario index needs to be provided by
    # _scenarioIdx_, otherwise it must be nil.  In case the attribute does not
    # exist, an exception is raised.
    def getAttribute(attributeId, scenarioIdx = nil)
      if scenarioIdx
        @scenarioAttributes[scenarioIdx][attributeId]
      else
        @attributes[attributeId]
      end
    end

    # Set the non-scenario-specific attribute with ID _attributeId_ to
    # _value_. No further checks are done.
    def force(attributeId, value)
      @attributes[attributeId].set(value)
    end

    # Set the non-scenario-specific attribute with ID _attributeId_ to _value_.
    # In case an already provided value is overwritten again, an exeception is
    # raised.
    def set(attributeId, value)
      attr = @attributes[attributeId]
      # Assignments to list attributes always append. We don't
      # consider this an overwrite.
      overwrite = attr.provided && !attr.isList?
      attr.set(value)

      # We only raise the overwrite error after the value has been set.
      if overwrite
        raise AttributeOverwrite,
          "Overwriting a previously provided value for attribute " +
          "#{attributeId}"
      end
    end

    # Set the scenario specific attribute with ID _attributeId_ for the
    # scenario with index _scenario_ to _value_. If _scenario_ is nil, the
    # attribute must not be scenario specific. In case the attribute does not
    # exist, an exception is raised.
    def []=(attributeId, scenario, value)
      overwrite = false

      if scenario
        if AttributeBase.mode == 0
          # If we get values in 'provided' mode, we copy them immedidately to
          # all derived scenarios.
          @project.scenario(scenario).all.each do |sc|
            scenarioIdx = @project.scenarioIdx(sc)
            attr = @scenarioAttributes[scenarioIdx][attributeId]

            if attr.provided && !attr.isList?
              # Assignments to list attributes always append. We don't
              # consider this an overwrite.
              overwrite = true
            end

            if scenarioIdx == scenario
              attr.set(value)
            else
              attr.inherit(value)
            end
          end
        else
          attr = @scenarioAttributes[scenario][attributeId]
          overwrite = attr.provided && !attr.isList?

          attr.set(value)
        end
      else
        attr = @attributes[attributeId]
        overwrite = attr.provided && !attr.isList?
        attr.set(value)
      end

      # We only raise the overwrite error after all scenarios have been
      # set. For some attributes the overwrite is actually allowed.
      if overwrite
        raise AttributeOverwrite,
          "Overwriting a previously provided value for attribute " +
          "#{attributeId}"
      end
    end

    # Return the value of the attribute with ID _attributeId_. For
    # scenario-specific attributes, _scenario_ must indicate the index of the
    # Scenario.
    def [](attributeId, scenario)
      @scenarioAttributes[scenario][attributeId]
      @data[scenario].instance_variable_get(('@' + attributeId).intern)
    end

    # Returns true if the value of the attribute _attributeId_ (in scenario
    # _scenarioIdx_) has been provided by the user.
    def provided(attributeId, scenarioIdx = nil)
      if scenarioIdx
        unless @scenarioAttributes[scenarioIdx].has_key?(attributeId)
          return false
        end
        @scenarioAttributes[scenarioIdx][attributeId].provided
      else
        return false unless @attributes.has_key?(attributeId)
        @attributes[attributeId].provided
      end
    end

    # Returns true if the value of the attribute _attributeId_ (in scenario
    # _scenarioIdx_) has been inherited from a parent node or scenario.
    def inherited(attributeId, scenarioIdx = nil)
      if scenarioIdx
        unless @scenarioAttributes[scenarioIdx].has_key?(attributeId)
          return false
        end
        @scenarioAttributes[scenarioIdx][attributeId].inherited
      else
        return false unless @attributes.has_key?(attributeId)
        @attributes[attributeId].inherited
      end
    end

    def modified?(attributeId, scenarioIdx = nil)
      if scenarioIdx
        unless @scenarioAttributes[scenarioIdx].has_key?(attributeId)
          return false
        end

        @scenarioAttributes[scenarioIdx][attributeId].provided ||
        @scenarioAttributes[scenarioIdx][attributeId].inherited
      else
        return false unless @attributes.has_key?(attributeId)
        @attributes[attributeId].provided || @attributes[attributeId].inherited
      end
    end

    def checkFailsAndWarnings
      if @attributes.has_key?('fail') || @attributes.has_key?('warn')
        propertyType = case self
                       when Task
                         'task'
                       when Resource
                         'resource'
                       else
                         'unknown'
                       end
        queryAttrs = { 'project' => @project,
                       'property' => self,
                       'scopeProperty' => nil,
                       'start' => @project['start'],
                       'end' => @project['end'],
                       'loadUnit' => :days,
                       'numberFormat' => @project['numberFormat'],
                       'timeFormat' => nil,
                       'currencyFormat' => @project['currencyFormat'] }
        query = Query.new(queryAttrs)
        if @attributes['fail']
          @attributes['fail'].get.each do |expr|
            if expr.eval(query)
              error("#{propertyType}_fail_check",
                    "User defined check failed for #{propertyType} " +
                    "#{fullId} \n" +
                    "Condition: #{expr.to_s}\n" +
              "Result:    #{expr.to_s(query)}")
            end
          end
        end
        if @attributes['warn']
          @attributes['warn'].get.each do |expr|
            if expr.eval(query)
              warning("#{propertyType}_warn_check",
                      "User defined warning triggered for #{propertyType} " +
                      "#{fullId} \n" +
                      "Condition: #{expr.to_s}\n" +
              "Result:    #{expr.to_s(query)}")
            end
          end
        end
      end
    end

    def query_children(query)
      list = []
      kids.each do |property|
        if query.listItem
          rti = RichText.new(query.listItem, RTFHandlers.create(@project)).
            generateIntermediateFormat
          q = query.dup
          q.property = property
          rti.setQuery(q)
          list << "<nowiki>#{rti.to_s}</nowiki>"
        else
          list << "<nowiki>#{property.name} (#{property.fullId})</nowiki>"
        end
      end

      query.assignList(list)
    end

    def query_journal(query)
      @project['journal'].to_rti(query)
    end

    def query_alert(query)
      journal = @project['journal']
      query.sortable = query.numerical = alert =
        journal.alertLevel(query.end, self, query)
      alertLevel = @project['alertLevels'][alert]
      query.string = alertLevel.name
      rText = "<fcol:#{alertLevel.color}><nowiki>#{alertLevel.name}" +
              "</nowiki></fcol>"
      unless (rti = RichText.new(rText, RTFHandlers.create(@project)).
              generateIntermediateFormat)
        warning('ptn_journal', "Syntax error in journal message")
        return nil
      end
      rti.blockMode = false
      query.rti = rti
    end

    def query_alertmessages(query)
      journalMessages(@project['journal'].alertEntries(query.end, self, 1,
                                                       query.start, query),
                      query, true)
    end

    def query_alertsummaries(query)
      journalMessages(@project['journal'].alertEntries(query.end, self, 1,
                                                       query.start, query),
                      query, false)
    end

    def query_journalmessages(query)
      journalMessages(@project['journal'].currentEntries(query.end, self, 0,
                                                         query.start,
                                                         query.hideJournalEntry),
                      query, true)
    end

    def query_journalsummaries(query)
      journalMessages(@project['journal'].currentEntries(query.end, self, 0,
                                                         query.start,
                                                         query.hideJournalEntry),
                      query, false)
    end

    def query_alerttrend(query)
      journal = @project['journal']
      startAlert = journal.alertLevel(query.start, self, query)
      endAlert = journal.alertLevel(query.end, self, query)
      if startAlert < endAlert
        query.sortable = 0
        query.string = 'Up'
      elsif startAlert > endAlert
        query.sortable = 2
        query.string = 'Down'
      else
        query.sortable = 1
        query.string = 'Flat'
      end
    end

    # Dump the class data in human readable form. Used for debugging only.
    def to_s # :nodoc:
      res = "#{self.class} #{fullId} \"#{@name}\"\n" +
            "  Sequence No: #{@sequenceNo}\n"

      res += "  Parent: #{@parent.fullId}\n" if @parent
      children = ""
      @children.each do |c|
        children += ', ' unless children.empty?
        children += c.fullId
      end
      res += '  Children: ' + children + "\n"  unless children.empty?
      @attributes.sort.each do |key, attr|
        res += indent("  #{key}: ", attr.to_s)
      end
      unless @scenarioAttributes.empty?
        project.scenarioCount.times do |sc|
          break if @scenarioAttributes[sc].nil?
          headerShown = false
          @scenarioAttributes[sc].sort.each do |key, attr|
            unless headerShown
              res += "  Scenario #{project.scenario(sc).get('id')} (#{sc})\n"
              headerShown = true
            end
            res += indent("    #{key}: ", attr.to_s)
          end
        end
      end
      res += '-' * 75 + "\n"
    end

    alias to_str to_s

    # Many PropertyTreeNode functions are scenario specific. These functions are
    # provided by the class *Scenario classes. In case we can't find a function
    # called for the base class we try to find it in corresponding *Scenario
    # class.
    def method_missing(func, scenarioIdx = 0, *args, &block)
      @data[scenarioIdx].send(func, *args, &block)
    end

  private

    # Create a blog-style list of all alert messages that match the Query.
    def journalMessages(entries, query, longVersion)
      # The components of the message are either UTF-8 text or RichText. For
      # the RichText components, we use the originally provided markup since
      # we compose the result as RichText markup first.
      rText = ''
      entries.each do |entry|
        rText += "==== <nowiki>" + entry.headline + "</nowiki> ====\n"
        rText += "''Reported on #{entry.date.to_s(query.timeFormat)}'' "
        if entry.author
          rText += "''by <nowiki>#{entry.author.name}</nowiki>''"
        end
        rText += "\n\n"
        unless entry.flags.empty?
          rText += "''Flags:'' #{entry.flags.join(', ')}\n\n"
        end
        if entry.summary
          rText += entry.summary.richText.inputText + "\n\n"
        end
        if longVersion && entry.details
          rText += entry.details.richText.inputText + "\n\n"
        end
      end
      # Now convert the RichText markup String into RichTextIntermediate
      # format.
      unless (rti = RichText.new(rText, RTFHandlers.create(@project)).
              generateIntermediateFormat)
        warning('ptn_journal', "Syntax error in journal message")
        return nil
      end
      # No section numbers, please!
      rti.sectionNumbers = false
      # We use a special class to allow CSS formating.
      rti.cssClass = 'tj_journal'
      query.rti = rti
    end

    def indent(tag, str)
      tag + str.gsub(/\n/, "\n#{' ' * tag.length}") + "\n"
    end

  end

end