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.
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 46 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
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 472 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 435 def []=(attributeId, scenario, value) if scenario if AttributeBase.mode == 0 # If we get values in 'provided' mode, we copy them immedidately to # all derived scenarios. overwrite = false @project.scenario(scenario).all.each do |sc| scenarioIdx = @project.scenarioIdx(sc) if @scenarioAttributes[scenarioIdx][attributeId].provided overwrite = true end if scenarioIdx == scenario @scenarioAttributes[scenarioIdx][attributeId].set(value) else @scenarioAttributes[scenarioIdx][attributeId].inherit(value) end 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 else @scenarioAttributes[scenario][attributeId].set(value) end else @attributes[attributeId].set(value) end end
Add child node as child to this node.
# File lib/taskjuggler/PropertyTreeNode.rb, line 346 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 152 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 264 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 273 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 373 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 399 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 193 def backupAttributes [ @attributes.clone, @scenarioAttributes.clone ] end
# File lib/taskjuggler/PropertyTreeNode.rb, line 519 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 368 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 137 def deep_clone self 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 292 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 406 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 417 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 320 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 Fixnums.
# File lib/taskjuggler/PropertyTreeNode.rb, line 334 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 216 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 493 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 354 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 182 def kids @children + @adoptees end
Return true if the node is a leaf node (has no children).
# File lib/taskjuggler/PropertyTreeNode.rb, line 363 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 306 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 211 def levelSeqNo(node) @children.index(node) + 1 end
# File lib/taskjuggler/PropertyTreeNode.rb, line 285 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 662 def method_missing(func, scenarioIdx, *args, &block) @data[scenarioIdx].send(func, *args, &block) end
# File lib/taskjuggler/PropertyTreeNode.rb, line 505 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 187 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 479 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 146 def ptn self end
# File lib/taskjuggler/PropertyTreeNode.rb, line 568 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 585 def query_alertmessages(query) journalMessages(@project['journal'].alertEntries(query.end, self, 1, query.start, query), query, true) end
# File lib/taskjuggler/PropertyTreeNode.rb, line 591 def query_alertsummaries(query) journalMessages(@project['journal'].alertEntries(query.end, self, 1, query.start, query), query, false) end
# File lib/taskjuggler/PropertyTreeNode.rb, line 611 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 564 def query_journal(query) @project['journal'].to_rti(query) end
# File lib/taskjuggler/PropertyTreeNode.rb, line 597 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 604 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 204 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 199 def restoreAttributes(backup) @attributes, @scenarioAttributes = backup end
Return the top-level node for this node.
# File lib/taskjuggler/PropertyTreeNode.rb, line 390 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 the attribute does not exist, an exception is raised.
# File lib/taskjuggler/PropertyTreeNode.rb, line 427 def set(attributeId, value) @attributes[attributeId].set(value) end
# File lib/taskjuggler/PropertyTreeNode.rb, line 705 def indent(tag, str) tag + str.gsub(/\n/, "\n#{' ' * tag.length}") + "\n" end
Create a blog-style list of all alert messages that match the Query.
# File lib/taskjuggler/PropertyTreeNode.rb, line 669 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