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
|