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
Public Class Methods
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
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
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
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 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
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
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
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
Return the type of the attribute with ID attributeId.
# File lib/taskjuggler/PropertyTreeNode.rb, line 400 def attributeDefinition(attributeId) @propertySet.attributeDefinitions[attributeId] end
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
# 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
Return true if the node has children.
# File lib/taskjuggler/PropertyTreeNode.rb, line 369 def container? !@children.empty? || !@adoptees.empty? end
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
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
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
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
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
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
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
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
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
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
Return a list of all children including adopted kids.
# File lib/taskjuggler/PropertyTreeNode.rb, line 183 def kids @children + @adoptees end
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
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
Return the index of the child node.
# File lib/taskjuggler/PropertyTreeNode.rb, line 212 def levelSeqNo(node) @children.index(node) + 1 end
# File lib/taskjuggler/PropertyTreeNode.rb, line 286 def logicalId fullId end
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
# 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
Return a list of all parents including step parents.
# File lib/taskjuggler/PropertyTreeNode.rb, line 188 def parents (@parent ? [ @parent ] : []) + @stepParents end
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
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
# 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
# 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
# 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
# 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
# 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
# File lib/taskjuggler/PropertyTreeNode.rb, line 611 def query_journal(query) @project['journal'].to_rti(query) end
# 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
# 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
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
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
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 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