class TaskJuggler::PropertyTreeNode

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.

Attributes

adoptees[R]
children[R]
data[R]
id[R]
name[RW]
parent[R]
project[R]
propertySet[R]
sequenceNo[R]
sourceFileInfo[RW]
subId[R]

Public Class Methods

new(propertySet, id, name, parent) click to toggle source

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’).

# File lib/taskjuggler/PropertyTreeNode.rb, line 47
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

Public Instance Methods

[](attributeId, scenario) click to toggle source

Return the value of the attribute with ID attributeId. For scenario-specific attributes, scenario must indicate the index of the Scenario.

# File lib/taskjuggler/PropertyTreeNode.rb, line 501
def [](attributeId, scenario)
  @scenarioAttributes[scenario][attributeId]
  @data[scenario].instance_variable_get(('@' + attributeId).intern)
end
[]=(attributeId, scenario, value) click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 454
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
addChild(child) click to toggle source

Add child node as child to this node.

# File lib/taskjuggler/PropertyTreeNode.rb, line 347
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
adopt(property) click to toggle source

Adopt property as a step child. Also register the new relationship with the child.

# File lib/taskjuggler/PropertyTreeNode.rb, line 153
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
all() click to toggle source

Returns a list of this node and all transient sub nodes.

# File lib/taskjuggler/PropertyTreeNode.rb, line 265
def all
  res = [ self ]
  kids.each do |c|
    res = res.concat(c.all)
  end
  res
end
allLeaves(withoutSelf = false) click to toggle source

Return a list of all leaf nodes of this node.

# File lib/taskjuggler/PropertyTreeNode.rb, line 274
def allLeaves(withoutSelf = false)
  res = []
  if leaf?
    res << self unless withoutSelf
  else
    kids.each do |c|
      res += c.allLeaves
    end
  end
  res
end
ancestors(includeStepParents = false) click to toggle source

Return a list with all parent nodes of this node.

# File lib/taskjuggler/PropertyTreeNode.rb, line 374
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
attributeDefinition(attributeId) click to toggle source

Return the type of the attribute with ID attributeId.

# File lib/taskjuggler/PropertyTreeNode.rb, line 400
def attributeDefinition(attributeId)
  @propertySet.attributeDefinitions[attributeId]
end
backupAttributes() click to toggle source

This method creates a shallow copy of all attributes and returns them as an Array that can be used with restoreAttributes().

# File lib/taskjuggler/PropertyTreeNode.rb, line 194
def backupAttributes
  [ @attributes.clone, @scenarioAttributes.clone ]
end
checkFailsAndWarnings() click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 548
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
container?() click to toggle source

Return true if the node has children.

# File lib/taskjuggler/PropertyTreeNode.rb, line 369
def container?
  !@children.empty? || !@adoptees.empty?
end
deep_clone() click to toggle source

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

# File lib/taskjuggler/PropertyTreeNode.rb, line 138
def deep_clone
  self
end
force(attributeId, value) click to toggle source

Set the non-scenario-specific attribute with ID attributeId to value. No further checks are done.

# File lib/taskjuggler/PropertyTreeNode.rb, line 428
def force(attributeId, value)
  @attributes[attributeId].set(value)
end
fullId() click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 293
def fullId
  res = @subId
  unless @propertySet.flatNamespace
    t = self
    until (t = t.parent).nil?
      res = t.subId + "." + res
    end
  end
  res
end
get(attributeId) click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 407
def get(attributeId)
  # Make sure the attribute gets created if it doesn't exist already.
  @attributes[attributeId]
  instance_variable_get(('@' + attributeId).intern)
end
getAttribute(attributeId, scenarioIdx = nil) click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 418
def getAttribute(attributeId, scenarioIdx = nil)
  if scenarioIdx
    @scenarioAttributes[scenarioIdx][attributeId]
  else
    @attributes[attributeId]
  end
end
getBSIndicies() click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 321
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
getIndicies() click to toggle source

Return the ‘index’ attributes of this property, prefixed by the ‘index’ attributes of all its parents. The result is an Array of Integers.

# File lib/taskjuggler/PropertyTreeNode.rb, line 335
def getIndicies
  idcs = []
  p = self
  begin
    parent = p.parent
    idcs.insert(0, p.get('index'))
    p = parent
  end while p
  idcs
end
inheritAttributes() click to toggle source

Inherit values for the attributes from the parent node or the Project.

# File lib/taskjuggler/PropertyTreeNode.rb, line 217
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
inherited(attributeId, scenarioIdx = nil) click to toggle source

Returns true if the value of the attribute attributeId (in scenario scenarioIdx) has been inherited from a parent node or scenario.

# File lib/taskjuggler/PropertyTreeNode.rb, line 522
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
isChildOf?(ancestor) click to toggle source

Find out if this property is a direct or indirect child of ancestor.

# File lib/taskjuggler/PropertyTreeNode.rb, line 355
def isChildOf?(ancestor)
  parent = self
  while parent = parent.parent
    return true if (parent == ancestor)
  end
  false
end
kids() click to toggle source

Return a list of all children including adopted kids.

# File lib/taskjuggler/PropertyTreeNode.rb, line 183
def kids
  @children + @adoptees
end
leaf?() click to toggle source

Return true if the node is a leaf node (has no children).

# File lib/taskjuggler/PropertyTreeNode.rb, line 364
def leaf?
  @children.empty? && @adoptees.empty?
end
level() click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 307
def level
  return @level if @level >= 0

  t = self
  @level = 0
  until (t = t.parent).nil?
    @level += 1
  end
  @level
end
levelSeqNo(node) click to toggle source

Return the index of the child node.

# File lib/taskjuggler/PropertyTreeNode.rb, line 212
def levelSeqNo(node)
  @children.index(node) + 1
end
logicalId() click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 286
def logicalId
  fullId
end
method_missing(func, scenarioIdx = 0, *args, &block) click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 711
def method_missing(func, scenarioIdx = 0, *args, &block)
  @data[scenarioIdx].send(func, *args, &block)
end
modified?(attributeId, scenarioIdx = nil) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 534
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
parents() click to toggle source

Return a list of all parents including step parents.

# File lib/taskjuggler/PropertyTreeNode.rb, line 188
def parents
  (@parent ? [ @parent ] : []) + @stepParents
end
provided(attributeId, scenarioIdx = nil) click to toggle source

Returns true if the value of the attribute attributeId (in scenario scenarioIdx) has been provided by the user.

# File lib/taskjuggler/PropertyTreeNode.rb, line 508
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
ptn() click to toggle source

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.

# File lib/taskjuggler/PropertyTreeNode.rb, line 147
def ptn
  self
end
query_alert(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 615
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
query_alertmessages(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 632
def query_alertmessages(query)
  journalMessages(@project['journal'].alertEntries(query.end, self, 1,
                                                   query.start, query),
                  query, true)
end
query_alertsummaries(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 638
def query_alertsummaries(query)
  journalMessages(@project['journal'].alertEntries(query.end, self, 1,
                                                   query.start, query),
                  query, false)
end
query_alerttrend(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 658
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
query_children(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 593
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
query_journal(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 611
def query_journal(query)
  @project['journal'].to_rti(query)
end
query_journalmessages(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 644
def query_journalmessages(query)
  journalMessages(@project['journal'].currentEntries(query.end, self, 0,
                                                     query.start,
                                                     query.hideJournalEntry),
                  query, true)
end
query_journalsummaries(query) click to toggle source
# File lib/taskjuggler/PropertyTreeNode.rb, line 651
def query_journalsummaries(query)
  journalMessages(@project['journal'].currentEntries(query.end, self, 0,
                                                     query.start,
                                                     query.hideJournalEntry),
                  query, false)
end
removeReferences(property) click to toggle source

Remove any references in the stored data that references the property.

# File lib/taskjuggler/PropertyTreeNode.rb, line 205
def removeReferences(property)
  @children.delete(property)
  @adoptees.delete(property)
  @stepParents.delete(property)
end
restoreAttributes(backup) click to toggle source

Restore the attributes to a previously saved state. backup is an Array generated by backupAttributes().

# File lib/taskjuggler/PropertyTreeNode.rb, line 200
def restoreAttributes(backup)
  @attributes, @scenarioAttributes = backup
end
root() click to toggle source

Return the top-level node for this node.

# File lib/taskjuggler/PropertyTreeNode.rb, line 391
def root
  n = self
  while n.parent
    n = n.parent
  end
  n
end
set(attributeId, value) click to toggle source

Set the non-scenario-specific attribute with ID attributeId to value. In case an already provided value is overwritten again, an exeception is raised.

# File lib/taskjuggler/PropertyTreeNode.rb, line 435
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