taskjuggler 3.7.1 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +26 -0
  3. data/Rakefile +4 -1
  4. data/data/tjp.vim +296 -296
  5. data/examples/ProjectTemplate/template.tjp +3 -3
  6. data/examples/Tutorial/tutorial.tjp +3 -3
  7. data/lib/taskjuggler/AppConfig.rb +1 -1
  8. data/lib/taskjuggler/BatchProcessor.rb +68 -57
  9. data/lib/taskjuggler/FileList.rb +1 -1
  10. data/lib/taskjuggler/MessageHandler.rb +0 -1
  11. data/lib/taskjuggler/ResourceScenario.rb +29 -1
  12. data/lib/taskjuggler/RuntimeConfig.rb +1 -1
  13. data/lib/taskjuggler/SheetHandlerBase.rb +5 -4
  14. data/lib/taskjuggler/StatusSheetReceiver.rb +2 -2
  15. data/lib/taskjuggler/StatusSheetSender.rb +1 -1
  16. data/lib/taskjuggler/TaskScenario.rb +7 -2
  17. data/lib/taskjuggler/TextParser/Scanner.rb +1 -1
  18. data/lib/taskjuggler/TimeSheetReceiver.rb +2 -2
  19. data/lib/taskjuggler/TimeSheetSender.rb +1 -1
  20. data/lib/taskjuggler/TimeSheets.rb +3 -2
  21. data/lib/taskjuggler/TjTime.rb +13 -6
  22. data/lib/taskjuggler/TjpSyntaxRules.rb +9 -3
  23. data/lib/taskjuggler/XMLDocument.rb +1 -1
  24. data/lib/taskjuggler/apps/Tj3Daemon.rb +2 -2
  25. data/lib/taskjuggler/apps/Tj3WebD.rb +1 -1
  26. data/lib/taskjuggler/daemon/ProjectServer.rb +2 -2
  27. data/lib/taskjuggler/reports/CSVFile.rb +1 -1
  28. data/lib/taskjuggler/reports/MspXmlRE.rb +5 -3
  29. data/lib/taskjuggler/reports/Report.rb +5 -5
  30. data/lib/taskjuggler/reports/ReportTableCell.rb +7 -5
  31. data/lib/taskjuggler/reports/TableReport.rb +2 -1
  32. data/lib/taskjuggler/reports/TraceReport.rb +2 -2
  33. data/lib/taskjuggler/version.rb +1 -1
  34. data/manual/html/Day_To_Day_Juggling.html +1 -1
  35. data/manual/html/Getting_Started.html +1 -1
  36. data/manual/html/How_To_Contribute.html +1 -1
  37. data/manual/html/Installation.html +1 -1
  38. data/manual/html/Intro.html +1 -1
  39. data/manual/html/List_Attributes.html +1 -1
  40. data/manual/html/Reporting_Bugs.html +1 -1
  41. data/manual/html/Rich_Text_Attributes.html +1 -1
  42. data/manual/html/Software.html +1 -1
  43. data/manual/html/TaskJuggler_2x_Migration.html +1 -1
  44. data/manual/html/TaskJuggler_Internals.html +1 -1
  45. data/manual/html/The_TaskJuggler_Syntax.html +1 -1
  46. data/manual/html/Tutorial.html +1 -1
  47. data/manual/html/account.html +1 -1
  48. data/manual/html/account.task.html +1 -1
  49. data/manual/html/accountprefix.html +1 -1
  50. data/manual/html/accountreport.html +2 -2
  51. data/manual/html/accountroot.html +1 -1
  52. data/manual/html/active.html +1 -1
  53. data/manual/html/adopt.task.html +1 -1
  54. data/manual/html/aggregate.html +1 -1
  55. data/manual/html/alert.html +1 -1
  56. data/manual/html/alertlevels.html +1 -1
  57. data/manual/html/allocate.html +1 -1
  58. data/manual/html/alphabet.html +1 -1
  59. data/manual/html/alternative.html +1 -1
  60. data/manual/html/author.html +1 -1
  61. data/manual/html/auxdir.html +1 -1
  62. data/manual/html/auxdir.report.html +1 -1
  63. data/manual/html/balance.html +1 -1
  64. data/manual/html/booking.resource.html +1 -1
  65. data/manual/html/booking.task.html +1 -1
  66. data/manual/html/caption.html +1 -1
  67. data/manual/html/cellcolor.column.html +1 -1
  68. data/manual/html/celltext.column.html +1 -1
  69. data/manual/html/center.html +1 -1
  70. data/manual/html/charge.html +1 -1
  71. data/manual/html/chargeset.html +1 -1
  72. data/manual/html/columnid.html +5 -2
  73. data/manual/html/columns.html +1 -1
  74. data/manual/html/complete.html +1 -1
  75. data/manual/html/copyright.html +1 -1
  76. data/manual/html/credits.html +1 -1
  77. data/manual/html/currency.html +1 -1
  78. data/manual/html/currencyformat.html +1 -1
  79. data/manual/html/dailymax.html +1 -1
  80. data/manual/html/dailymin.html +1 -1
  81. data/manual/html/dailyworkinghours.html +1 -1
  82. data/manual/html/date.extend.html +1 -1
  83. data/manual/html/date.html +1 -1
  84. data/manual/html/definitions.html +1 -1
  85. data/manual/html/depends.html +1 -1
  86. data/manual/html/details.html +1 -1
  87. data/manual/html/disabled.html +1 -1
  88. data/manual/html/duration.html +1 -1
  89. data/manual/html/efficiency.html +1 -1
  90. data/manual/html/effort.html +1 -1
  91. data/manual/html/effortdone.html +1 -1
  92. data/manual/html/effortleft.html +1 -1
  93. data/manual/html/email.html +1 -1
  94. data/manual/html/enabled.html +1 -1
  95. data/manual/html/end.column.html +1 -1
  96. data/manual/html/end.html +1 -1
  97. data/manual/html/end.limit.html +1 -1
  98. data/manual/html/end.report.html +2 -2
  99. data/manual/html/end.timesheet.html +1 -1
  100. data/manual/html/endcredit.html +1 -1
  101. data/manual/html/epilog.html +1 -1
  102. data/manual/html/export.html +2 -2
  103. data/manual/html/extend.html +1 -1
  104. data/manual/html/fail.html +1 -1
  105. data/manual/html/fdl.html +1 -1
  106. data/manual/html/flags.account.html +1 -1
  107. data/manual/html/flags.html +1 -1
  108. data/manual/html/flags.journalentry.html +1 -1
  109. data/manual/html/flags.report.html +1 -1
  110. data/manual/html/flags.resource.html +1 -1
  111. data/manual/html/flags.statussheet.html +1 -1
  112. data/manual/html/flags.task.html +1 -1
  113. data/manual/html/flags.timesheet.html +1 -1
  114. data/manual/html/fontcolor.column.html +1 -1
  115. data/manual/html/footer.html +1 -1
  116. data/manual/html/formats.export.html +1 -1
  117. data/manual/html/formats.html +1 -1
  118. data/manual/html/functions.html +1 -1
  119. data/manual/html/gapduration.html +1 -1
  120. data/manual/html/gaplength.html +1 -1
  121. data/manual/html/halign.center.html +1 -1
  122. data/manual/html/halign.column.html +1 -1
  123. data/manual/html/halign.left.html +1 -1
  124. data/manual/html/halign.right.html +1 -1
  125. data/manual/html/hasalert.html +1 -1
  126. data/manual/html/header.html +1 -1
  127. data/manual/html/headline.html +1 -1
  128. data/manual/html/height.html +1 -1
  129. data/manual/html/hideaccount.html +1 -1
  130. data/manual/html/hidejournalentry.html +1 -1
  131. data/manual/html/hidereport.html +1 -1
  132. data/manual/html/hideresource.html +2 -2
  133. data/manual/html/hidetask.html +2 -2
  134. data/manual/html/icalreport.html +1 -1
  135. data/manual/html/include.macro.html +1 -1
  136. data/manual/html/include.project.html +1 -1
  137. data/manual/html/include.properties.html +1 -1
  138. data/manual/html/index.html +1 -1
  139. data/manual/html/inherit.extend.html +1 -1
  140. data/manual/html/interval1.html +1 -1
  141. data/manual/html/interval2.html +1 -1
  142. data/manual/html/interval3.html +1 -1
  143. data/manual/html/interval4.html +1 -1
  144. data/manual/html/isactive.html +1 -1
  145. data/manual/html/ischildof.html +1 -1
  146. data/manual/html/isdependencyof.html +1 -1
  147. data/manual/html/isdutyof.html +1 -1
  148. data/manual/html/isfeatureof.html +1 -1
  149. data/manual/html/isleaf.html +1 -1
  150. data/manual/html/ismilestone.html +1 -1
  151. data/manual/html/isongoing.html +1 -1
  152. data/manual/html/isresource.html +1 -1
  153. data/manual/html/isresponsibilityof.html +1 -1
  154. data/manual/html/istask.html +1 -1
  155. data/manual/html/isvalid.html +1 -1
  156. data/manual/html/journalattributes.html +1 -1
  157. data/manual/html/journalentry.html +1 -1
  158. data/manual/html/journalmode.html +1 -1
  159. data/manual/html/leaveallowance.html +1 -1
  160. data/manual/html/leaves.html +1 -1
  161. data/manual/html/left.html +1 -1
  162. data/manual/html/length.html +1 -1
  163. data/manual/html/limits.allocate.html +1 -1
  164. data/manual/html/limits.html +1 -1
  165. data/manual/html/limits.resource.html +1 -1
  166. data/manual/html/limits.task.html +1 -1
  167. data/manual/html/listitem.column.html +1 -1
  168. data/manual/html/listtype.column.html +1 -1
  169. data/manual/html/loadunit.html +2 -2
  170. data/manual/html/logicalexpression.html +1 -1
  171. data/manual/html/logicalflagexpression.html +1 -1
  172. data/manual/html/macro.html +1 -1
  173. data/manual/html/managers.html +1 -1
  174. data/manual/html/mandatory.html +1 -1
  175. data/manual/html/markdate.html +1 -1
  176. data/manual/html/maxend.html +1 -1
  177. data/manual/html/maximum.html +1 -1
  178. data/manual/html/maxstart.html +1 -1
  179. data/manual/html/milestone.html +1 -1
  180. data/manual/html/minend.html +1 -1
  181. data/manual/html/minimum.html +1 -1
  182. data/manual/html/minstart.html +1 -1
  183. data/manual/html/monthlymax.html +1 -1
  184. data/manual/html/monthlymin.html +1 -1
  185. data/manual/html/navbar.html +1 -1
  186. data/manual/html/navigator.html +1 -1
  187. data/manual/html/newtask.html +1 -1
  188. data/manual/html/nikureport.html +1 -1
  189. data/manual/html/note.task.html +1 -1
  190. data/manual/html/novevents.html +1 -1
  191. data/manual/html/now.html +1 -1
  192. data/manual/html/number.extend.html +1 -1
  193. data/manual/html/numberformat.html +1 -1
  194. data/manual/html/onend.html +1 -1
  195. data/manual/html/onstart.html +1 -1
  196. data/manual/html/opennodes.html +1 -1
  197. data/manual/html/outputdir.html +1 -1
  198. data/manual/html/overtime.booking.html +1 -1
  199. data/manual/html/period.column.html +1 -1
  200. data/manual/html/period.limit.html +1 -1
  201. data/manual/html/period.report.html +2 -2
  202. data/manual/html/period.task.html +1 -1
  203. data/manual/html/persistent.html +1 -1
  204. data/manual/html/precedes.html +1 -1
  205. data/manual/html/priority.html +1 -1
  206. data/manual/html/priority.timesheet.html +1 -1
  207. data/manual/html/project.html +1 -1
  208. data/manual/html/projectid.html +1 -1
  209. data/manual/html/projectid.task.html +1 -1
  210. data/manual/html/projectids.html +1 -1
  211. data/manual/html/projection.html +1 -1
  212. data/manual/html/prolog.html +1 -1
  213. data/manual/html/properties.html +1 -1
  214. data/manual/html/purge.html +2 -2
  215. data/manual/html/rate.html +1 -1
  216. data/manual/html/rate.resource.html +1 -1
  217. data/manual/html/rawhtmlhead.html +1 -1
  218. data/manual/html/reference.extend.html +1 -1
  219. data/manual/html/remaining.html +1 -1
  220. data/manual/html/replace.html +1 -1
  221. data/manual/html/reportprefix.html +1 -1
  222. data/manual/html/resource.html +1 -1
  223. data/manual/html/resourceattributes.html +1 -1
  224. data/manual/html/resourceprefix.html +1 -1
  225. data/manual/html/resourcereport.html +2 -2
  226. data/manual/html/resourceroot.html +1 -1
  227. data/manual/html/resources.limit.html +1 -1
  228. data/manual/html/responsible.html +1 -1
  229. data/manual/html/richtext.extend.html +1 -1
  230. data/manual/html/right.html +1 -1
  231. data/manual/html/rollupaccount.html +1 -1
  232. data/manual/html/rollupresource.html +2 -2
  233. data/manual/html/rolluptask.html +2 -2
  234. data/manual/html/scale.column.html +1 -1
  235. data/manual/html/scenario.html +1 -1
  236. data/manual/html/scenario.ical.html +1 -1
  237. data/manual/html/scenarios.export.html +1 -1
  238. data/manual/html/scenarios.html +1 -1
  239. data/manual/html/scenariospecific.extend.html +1 -1
  240. data/manual/html/scheduled.html +1 -1
  241. data/manual/html/scheduling.html +1 -1
  242. data/manual/html/schedulingmode.html +1 -1
  243. data/manual/html/select.html +1 -1
  244. data/manual/html/selfcontained.html +1 -1
  245. data/manual/html/shift.allocate.html +1 -1
  246. data/manual/html/shift.html +1 -1
  247. data/manual/html/shift.resource.html +1 -1
  248. data/manual/html/shift.task.html +1 -1
  249. data/manual/html/shift.timesheet.html +1 -1
  250. data/manual/html/shifts.allocate.html +1 -1
  251. data/manual/html/shifts.resource.html +1 -1
  252. data/manual/html/shifts.task.html +1 -1
  253. data/manual/html/shorttimeformat.html +1 -1
  254. data/manual/html/sloppy.booking.html +1 -1
  255. data/manual/html/sloppy.projection.html +1 -1
  256. data/manual/html/sortaccounts.html +1 -1
  257. data/manual/html/sortjournalentries.html +1 -1
  258. data/manual/html/sortresources.html +1 -1
  259. data/manual/html/sorttasks.html +1 -1
  260. data/manual/html/start.column.html +1 -1
  261. data/manual/html/start.html +1 -1
  262. data/manual/html/start.limit.html +1 -1
  263. data/manual/html/start.report.html +2 -2
  264. data/manual/html/startcredit.html +1 -1
  265. data/manual/html/status.statussheet.html +1 -1
  266. data/manual/html/status.timesheet.html +1 -1
  267. data/manual/html/statussheet.html +1 -1
  268. data/manual/html/statussheetreport.html +1 -1
  269. data/manual/html/strict.projection.html +1 -1
  270. data/manual/html/summary.html +1 -1
  271. data/manual/html/supplement.html +1 -1
  272. data/manual/html/supplement.resource.html +1 -1
  273. data/manual/html/supplement.task.html +1 -1
  274. data/manual/html/tagfile.html +1 -1
  275. data/manual/html/task.html +1 -1
  276. data/manual/html/task.statussheet.html +1 -1
  277. data/manual/html/task.timesheet.html +1 -1
  278. data/manual/html/taskattributes.html +1 -1
  279. data/manual/html/taskprefix.html +1 -1
  280. data/manual/html/taskreport.html +2 -2
  281. data/manual/html/taskroot.export.html +1 -1
  282. data/manual/html/taskroot.html +1 -1
  283. data/manual/html/text.extend.html +1 -1
  284. data/manual/html/textreport.html +2 -2
  285. data/manual/html/timeformat.html +1 -1
  286. data/manual/html/timeformat1.html +1 -1
  287. data/manual/html/timeformat2.html +1 -1
  288. data/manual/html/timeoff.nikureport.html +1 -1
  289. data/manual/html/timesheet.html +1 -1
  290. data/manual/html/timesheetreport.html +1 -1
  291. data/manual/html/timezone.export.html +1 -1
  292. data/manual/html/timezone.html +1 -1
  293. data/manual/html/timezone.report.html +1 -1
  294. data/manual/html/timezone.shift.html +1 -1
  295. data/manual/html/timingresolution.html +1 -1
  296. data/manual/html/title.column.html +1 -1
  297. data/manual/html/title.html +1 -1
  298. data/manual/html/toc.html +3 -3
  299. data/manual/html/tooltip.column.html +1 -1
  300. data/manual/html/tracereport.html +2 -2
  301. data/manual/html/trackingscenario.html +1 -1
  302. data/manual/html/treelevel.html +1 -1
  303. data/manual/html/vacation.html +1 -1
  304. data/manual/html/vacation.resource.html +1 -1
  305. data/manual/html/vacation.shift.html +1 -1
  306. data/manual/html/warn.html +1 -1
  307. data/manual/html/weeklymax.html +1 -1
  308. data/manual/html/weeklymin.html +1 -1
  309. data/manual/html/weekstartsmonday.html +1 -1
  310. data/manual/html/weekstartssunday.html +1 -1
  311. data/manual/html/width.column.html +1 -1
  312. data/manual/html/width.html +1 -1
  313. data/manual/html/work.html +1 -1
  314. data/manual/html/workinghours.project.html +1 -1
  315. data/manual/html/workinghours.resource.html +1 -1
  316. data/manual/html/workinghours.shift.html +1 -1
  317. data/manual/html/yearlyworkingdays.html +1 -1
  318. data/spec/TraceReport_spec.rb +1 -1
  319. data/spec/support/DaemonControl.rb +2 -3
  320. data/taskjuggler.gemspec +0 -3
  321. data/tasks/gem.rake +0 -1
  322. data/tasks/help2man.rake +1 -1
  323. data/tasks/manual.rake +1 -1
  324. data/test/TestSuite/CSV-Reports/quotes.tjp +20 -0
  325. data/test/TestSuite/CSV-Reports/refs/celltext.csv +11 -11
  326. data/test/TestSuite/CSV-Reports/refs/quotes.csv +5 -0
  327. data/test/TestSuite/CSV-Reports/refs/resourcereport.csv +3 -3
  328. data/test/TestSuite/CSV-Reports/refs/resourcereport_with_tasks.csv +21 -21
  329. data/test/TestSuite/CSV-Reports/refs/sortByTree.csv +11 -11
  330. data/test/TestSuite/CSV-Reports/refs/sortBy_effort.up.csv +11 -11
  331. data/test/TestSuite/CSV-Reports/refs/sortBy_plan.start.down.csv +11 -11
  332. data/test/TestSuite/CSV-Reports/refs/taskreport.csv +11 -11
  333. data/test/TestSuite/CSV-Reports/refs/taskreport_with_resources.csv +29 -29
  334. data/test/TestSuite/Syntax/Correct/Timezone2.tjp +8 -0
  335. data/test/TestSuite/Syntax/Correct/template.tjp +3 -3
  336. data/test/TestSuite/Syntax/Correct/tutorial.tjp +3 -3
  337. data/test/TestSuite/Syntax/Errors/time_error.tjp +9 -0
  338. data/test/test_BatchProcessor.rb +6 -3
  339. metadata +14 -17
  340. data/man/tj3.1 +0 -130
  341. data/man/tj3client.1 +0 -145
  342. data/man/tj3d.1 +0 -93
  343. data/man/tj3man.1 +0 -76
  344. data/man/tj3ss_receiver.1 +0 -86
  345. data/man/tj3ss_sender.1 +0 -100
  346. data/man/tj3ts_receiver.1 +0 -86
  347. data/man/tj3ts_sender.1 +0 -92
  348. data/man/tj3ts_summary.1 +0 -104
  349. data/man/tj3webd.1 +0 -86
@@ -69,16 +69,16 @@ class TaskJuggler
69
69
  def initialize(maxCpuCores)
70
70
  @maxCpuCores = maxCpuCores
71
71
  # Jobs submitted by calling queue() are put in the @toRunQueue. The
72
- # pusher Thread will pick them up and fork them off into another
72
+ # launcher Thread will pick them up and fork them off into another
73
73
  # process.
74
- @toRunQueue = Queue.new
74
+ @toRunQueue = [ ]
75
75
  # A hash that maps the JobInfo objects of running jobs by their PID.
76
76
  @runningJobs = { }
77
77
  # A list of jobs that wait to complete their writing.
78
78
  @spoolingJobs = [ ]
79
79
  # The wait() method will then clean the @toDropQueue, executes the post
80
80
  # processing block and removes all JobInfo related objects.
81
- @toDropQueue = Queue.new
81
+ @toDropQueue = []
82
82
 
83
83
  # A semaphore to guard accesses to @runningJobs, @spoolingJobs and
84
84
  # following shared data structures.
@@ -106,28 +106,34 @@ class TaskJuggler
106
106
  # to identify the job upon completion. +block+ is a Ruby code block to be
107
107
  # executed in a separate process.
108
108
  def queue(tag = nil, &block)
109
- raise 'You cannot call queue() while wait() is running!' if @jobsOut > 0
110
-
111
- # If this is the first queued job for this run, we have to start the
112
- # helper threads.
113
- if @jobsIn == 0
114
- # The JobInfo objects in the @toRunQueue are processed by the pusher
115
- # thread. It forkes off processes to execute the code block associated
116
- # with the JobInfo.
117
- @pusher = Thread.new { pusher }
118
- # The popper thread waits for terminated childs and picks up the
119
- # results.
120
- @popper = Thread.new { popper }
121
- # The grabber thread collects $stdout and $stderr data from each child
122
- # process and stores them in the corresponding JobInfo.
123
- @grabber = Thread.new { grabber }
124
- end
125
109
 
126
110
  # Create a new JobInfo object for the job and push it to the @toRunQueue.
127
- job = JobInfo.new(@jobsIn, block, tag)
128
- # Increase job counter
129
- @lock.synchronize { @jobsIn += 1 }
130
- @toRunQueue.push(job)
111
+ @lock.synchronize do
112
+ raise 'You cannot call queue() while wait() is running!' if @jobsOut > 0
113
+
114
+ # If this is the first queued job for this run, we have to start the
115
+ # helper threads.
116
+ if @jobsIn == 0
117
+ # The JobInfo objects in the @toRunQueue are processed by the
118
+ # launcher thread. It forkes off processes to execute the code
119
+ # block associated with the JobInfo.
120
+ @launcher = Thread.new { launcher }
121
+ # The receiver thread waits for terminated child processes and picks
122
+ # up the results.
123
+ @receiver = Thread.new { receiver }
124
+ # The grabber thread collects $stdout and $stderr data from each
125
+ # child process and stores them in the corresponding JobInfo.
126
+ @grabber = Thread.new { grabber }
127
+ end
128
+
129
+ # To track a job through the queues, we use a JobInfo object to hold
130
+ # all data associated with a job.
131
+ job = JobInfo.new(@jobsIn, block, tag)
132
+ # Increase job counter
133
+ @jobsIn += 1
134
+ # Push the job to the toRunQueue.
135
+ @toRunQueue.push(job)
136
+ end
131
137
  end
132
138
 
133
139
  # Wait for all jobs to complete. The code block will get the JobInfo
@@ -138,29 +144,27 @@ class TaskJuggler
138
144
 
139
145
  # When we have received as many jobs in the @toDropQueue than we have
140
146
  # started then we're done.
141
- while !@lock.synchronize { @jobsIn == @jobsOut }
142
- if @toDropQueue.empty?
143
- sleep(@timeout)
144
- else
145
- # We have completed jobs.
146
- while !@toDropQueue.empty?
147
- # Pop a job from the @toDropQueue and call the block with it.
148
- job = @toDropQueue.pop
149
- # Remove the job related entries from the housekeeping tables.
150
- @lock.synchronize { @jobsOut += 1 }
151
-
147
+ while @lock.synchronize { @jobsOut < @jobsIn }
148
+ job = nil
149
+ @lock.synchronize do
150
+ if !@toDropQueue.empty? && (job = @toDropQueue.pop)
152
151
  # Call the post-processing block that was passed to wait() with
153
152
  # the JobInfo object as argument.
153
+ @jobsOut += 1
154
154
  yield(job)
155
155
  end
156
156
  end
157
+
158
+ unless job
159
+ sleep(@timeout)
160
+ end
157
161
  end
158
162
 
159
163
  # Signal threads to stop
160
164
  @terminate = true
161
165
  # Wait for treads to finish
162
- @pusher.join
163
- @popper.join
166
+ @launcher.join
167
+ @receiver.join
164
168
  @grabber.join
165
169
 
166
170
  # Reset some variables so we can reuse the object for further job runs.
@@ -175,25 +179,22 @@ class TaskJuggler
175
179
 
176
180
  # This function runs in a separate thread to pop JobInfo items from the
177
181
  # @toRunQueue and create child processes for them.
178
- def pusher
182
+ def launcher
179
183
  # Run until the terminate flag is set.
180
184
  until @terminate
181
- if @toRunQueue.empty? ||
182
- @lock.synchronize{ @runningJobs.length >= @maxCpuCores }
185
+ job = nil
186
+ unless @lock.synchronize { @runningJobs.length < @maxCpuCores &&
187
+ (job = @toRunQueue.pop) }
183
188
  # We have no jobs in the @toRunQueue or all CPU cores in use already.
184
189
  sleep(@timeout)
185
190
  else
186
191
  @lock.synchronize do
187
- # Get a new job from the @toRunQueue
188
- job = @toRunQueue.pop
189
-
190
192
  job.openPipes
191
- # Add the receiver end of the pipe to the @pipes Array.
193
+ # Add the receiver end of the pipe to the pipes Arrays.
192
194
  @pipes << job.stdoutP
195
+ @pipes << job.stderrP
193
196
  # Map the pipe end to this JobInfo object.
194
197
  @pipeToJob[job.stdoutP] = job
195
- # Same for $stderr.
196
- @pipes << job.stderrP
197
198
  @pipeToJob[job.stderrP] = job
198
199
 
199
200
  pid = fork do
@@ -224,15 +225,20 @@ class TaskJuggler
224
225
 
225
226
  # This function runs in a separate thread to wait for completed jobs. It
226
227
  # waits for the process completion and stores the result in the
227
- # corresponding JobInfo object.
228
- def popper
228
+ # corresponding JobInfo object. Aborted jobs are pushed to the
229
+ # @toDropQueue while completed jobs are pushed to the @spoolingJobs queue.
230
+ def receiver
229
231
  until @terminate
230
- if @runningJobs.empty?
231
- # No pending jobs, wait a bit.
232
- sleep(@timeout)
233
- else
232
+ pid = retVal = nil
233
+ begin
234
234
  # Wait for the next job to complete.
235
235
  pid, retVal = Process.wait2
236
+ rescue Errno::ECHILD
237
+ # No running jobs. Wait a bit.
238
+ sleep(@timeout)
239
+ end
240
+
241
+ if pid && retVal
236
242
  job = nil
237
243
  @lock.synchronize do
238
244
  # Get the JobInfo object that corresponds to the process ID. The
@@ -242,7 +248,7 @@ class TaskJuggler
242
248
  # Remove the job from the @runningJobs Hash.
243
249
  @runningJobs.delete(pid)
244
250
  # Save the return value.
245
- job.retVal = retVal.dup
251
+ job.retVal = retVal.exitstatus
246
252
  if retVal.signaled?
247
253
  cleanPipes(job)
248
254
  # Aborted jobs will probably not send an EOT. So we fastrack
@@ -269,22 +275,27 @@ class TaskJuggler
269
275
  res = nil
270
276
  begin
271
277
  @lock.synchronize do
272
- if (res = select(@pipes, nil, @pipes, @timeout))
278
+ if (res = IO.select(@pipes, nil, nil, @timeout))
273
279
  # We have output data from at least one child. Check which pipe
274
280
  # actually triggered the select.
275
281
  res[0].each do |pipe|
276
282
  # Find the corresponding JobInfo object.
277
283
  job = @pipeToJob[pipe]
278
- # Store the output.
284
+
285
+ # Store the standard output.
279
286
  if pipe == job.stdoutP
280
287
  # Look for the EOT character to signal the end of the text.
281
- if (c = pipe.getc) == ?\004
288
+ if pipe.closed? || (c = pipe.read_nonblock(1)) == ?\004
282
289
  job.stdoutEOT = true
283
290
  else
284
291
  job.stdout << c
285
292
  end
286
- else
287
- if (c = pipe.getc) == ?\004
293
+ end
294
+
295
+ # Store the error output.
296
+ if pipe == job.stderrP
297
+ # Look for the EOT character to signal the end of the text.
298
+ if pipe.closed? || (c = pipe.read_nonblock(1)) == ?\004
288
299
  job.stderrEOT = true
289
300
  else
290
301
  job.stderr << c
@@ -17,7 +17,7 @@ class TaskJuggler
17
17
  class FileRecord
18
18
 
19
19
  def initialize(fileName)
20
- @name = fileName.dup.untaint
20
+ @name = fileName.dup
21
21
  @mtime = File.mtime(@name)
22
22
  end
23
23
 
@@ -248,7 +248,6 @@ class TaskJuggler
248
248
 
249
249
  timeStamp = Time.new.strftime("%Y-%m-%d %H:%M:%S")
250
250
  begin
251
- @logFile.untaint
252
251
  File.open(@logFile, 'a') do |f|
253
252
  f.write("#{timeStamp} #{type} #{@appName}[#{Process.pid}]: " +
254
253
  "#{message}\n")
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = ResourceScenario.rb -- The TaskJuggler III Project Management Software
5
5
  #
6
- # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
6
+ # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2020
7
7
  # by Chris Schlaeger <cs@taskjuggler.org>
8
8
  #
9
9
  # This program is free software; you can redistribute it and/or modify
@@ -322,6 +322,34 @@ class TaskJuggler
322
322
  end
323
323
  end
324
324
 
325
+ # A list of the tasks that the resource has been allocated to work on in
326
+ # the report time frame.
327
+ def query_duties(query)
328
+ list = []
329
+ iv = TimeInterval.new(query.start, query.end)
330
+ @duties.each do |task|
331
+ if task.hasResourceAllocated?(@scenarioIdx, iv, @property)
332
+ if query.listItem
333
+ rti = RichText.new(query.listItem, RTFHandlers.create(@project)).
334
+ generateIntermediateFormat
335
+ unless rti
336
+ error('bad_resource_ts_query',
337
+ "Syntax error in query statement for task attribute " +
338
+ "'resources'.")
339
+ end
340
+ q = query.dup
341
+ q.property = task
342
+ q.scopeProperty = @property
343
+ rti.setQuery(q)
344
+ list << "<nowiki>#{rti.to_s}</nowiki>"
345
+ else
346
+ list << "<nowiki>#{task.name} (#{task.id})</nowiki>"
347
+ end
348
+ end
349
+ end
350
+ query.assignList(list)
351
+ end
352
+
325
353
  # The effort allocated to the Resource in the specified interval. In case a
326
354
  # Task is given as scope property only the effort allocated to this Task is
327
355
  # taken into account.
@@ -48,7 +48,7 @@ class RuntimeConfig
48
48
  return false unless (p = @config)
49
49
  sections.each do |sec|
50
50
  p = p['_' + sec]
51
- unless p
51
+ unless p && p.is_a?(Hash)
52
52
  debug("Section #{section} not found in config file")
53
53
  return false
54
54
  end
@@ -166,11 +166,12 @@ class TaskJuggler
166
166
  inReplyTo = nil)
167
167
  case @emailDeliveryMethod
168
168
  when 'smtp'
169
- Mail.defaults do
170
- delivery_method :smtp, {
169
+ settings_dto = {
171
170
  :address => @smtpServer,
172
- :port => 25
173
- }
171
+ :port => 25,
172
+ }
173
+ Mail.defaults do
174
+ delivery_method :smtp, settings_dto
174
175
  end
175
176
  when 'sendmail'
176
177
  Mail.defaults do
@@ -35,9 +35,9 @@ class TaskJuggler
35
35
  @logFile = 'statussheets.log'
36
36
 
37
37
  # Regular expression to identify status sheets.
38
- @sheetHeader = /^[ ]*statussheet\s([a-z][a-z0-9_]*)\s[0-9\-:+]*\s-\s([0-9]*-[0-9]*-[0-9]*)/
38
+ @sheetHeader = /^[ ]*statussheet\s([a-zA-Z_][a-zA-Z0-9_]*)\s[0-9\-:+]*\s-\s([0-9]*-[0-9]*-[0-9]*)/
39
39
  # Regular expression to extract the sheet signature (time period).
40
- @signatureFilter = /^[ ]*statussheet\s[a-z][a-z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
40
+ @signatureFilter = /^[ ]*statussheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
41
41
  @emailSubject = "Status report from %s for %s"
42
42
  end
43
43
 
@@ -39,7 +39,7 @@ class TaskJuggler
39
39
  # The log file
40
40
  @logFile = 'statussheets.log'
41
41
 
42
- @signatureFilter = /^[ ]*statussheet\s[a-z][a-z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
42
+ @signatureFilter = /^[ ]*statussheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
43
43
  @introText = <<'EOT'
44
44
  Please find enclosed your weekly status report template. Please fill out the
45
45
  form and send it back to the sender of this email. You can either use the
@@ -1508,6 +1508,7 @@ class TaskJuggler
1508
1508
  end
1509
1509
  q = query.dup
1510
1510
  q.property = resource
1511
+ q.scopeProperty = @property
1511
1512
  rti.setQuery(q)
1512
1513
  list << "<nowiki>#{rti.to_s}</nowiki>"
1513
1514
  else
@@ -2179,8 +2180,12 @@ class TaskJuggler
2179
2180
  # been set already.
2180
2181
  if @scheduled && @effort == 0 && @length == 0 && @duration == 0 &&
2181
2182
  !@milestone
2182
- @start = @project.idxToDate(firstSlotIdx) unless @start
2183
- @end = @project.idxToDate(lastSlotIdx + 1) unless @end
2183
+ unless @start || !firstSlotIdx
2184
+ @start = @project.idxToDate(firstSlotIdx)
2185
+ end
2186
+ unless @end || !lastSlotIdx
2187
+ @end = @project.idxToDate(lastSlotIdx + 1)
2188
+ end
2184
2189
  end
2185
2190
  end
2186
2191
 
@@ -169,7 +169,7 @@ class TaskJuggler::TextParser
169
169
 
170
170
  def initialize(fileName, log, textScanner)
171
171
  super(log, textScanner)
172
- @fileName = fileName.dup.untaint
172
+ @fileName = fileName.dup
173
173
  data = (fileName == '.' ? $stdin : File.new(@fileName, 'r')).read
174
174
  begin
175
175
  @stream = StringIO.new(data.forceUTF8Encoding)
@@ -32,9 +32,9 @@ class TaskJuggler
32
32
  @logFile = 'timesheets.log'
33
33
 
34
34
  # Regular expression to identify time sheets.
35
- @sheetHeader = /^[ ]*timesheet\s([a-z][a-z0-9_]*)\s[0-9\-:+]*\s-\s([0-9]*-[0-9]*-[0-9]*)/
35
+ @sheetHeader = /^[ ]*timesheet\s([a-zA-Z_][a-zA-Z0-9_]*)\s[0-9\-:+]*\s-\s([0-9]*-[0-9]*-[0-9]*)/
36
36
  # Regular expression to extract the sheet signature (time period).
37
- @signatureFilter = /^[ ]*timesheet\s[a-z][a-z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
37
+ @signatureFilter = /^[ ]*timesheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
38
38
  @emailSubject = "Report from %s for %s"
39
39
  end
40
40
 
@@ -38,7 +38,7 @@ class TaskJuggler
38
38
  # The log file
39
39
  @logFile = 'timesheets.log'
40
40
 
41
- @signatureFilter = /^[ ]*timesheet\s[a-z][a-z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
41
+ @signatureFilter = /^[ ]*timesheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/
42
42
  @introText = <<'EOT'
43
43
  Please find enclosed your weekly report template. Please fill out
44
44
  the form and send it back to the sender of this email. You can either
@@ -325,9 +325,10 @@ class TaskJuggler
325
325
  def totalGrossWorkingSlots
326
326
  project = @resource.project
327
327
  # Calculate the number of weeks in the report
328
- weeksToReport = (@interval.end - @interval.start) / (60 * 60 * 24 * 7)
328
+ weeksToReport = (@interval.end - @interval.start).to_f /
329
+ (60 * 60 * 24 * 7)
329
330
 
330
- daysToSlots(project.weeklyWorkingDays * weeksToReport)
331
+ daysToSlots((project.weeklyWorkingDays * weeksToReport).to_i)
331
332
  end
332
333
 
333
334
  # Compute the total number of actual working time slots of the
@@ -501,18 +501,25 @@ class TaskJuggler
501
501
  end
502
502
 
503
503
  @time = Time.utc(year, month, day, hour, minute, second)
504
- sign = zone[0] == ?- ? 1 : -1
504
+ sign = zone[0] == ?- ? -1 : 1
505
505
  tzHour = zone[1..2].to_i
506
- if tzHour < 0 || tzHour > 12
507
- raise TjException.new, "Time zone adjustment hour out of range " +
508
- "(0 - 12) but is #{tzHour}"
509
- end
510
506
  tzMinute = zone[3..4].to_i
511
507
  if tzMinute < 0 || tzMinute > 59
512
508
  raise TjException.new, "Time zone adjustment minute out of range " +
513
509
  "(0 - 59) but is #{tzMinute}"
514
510
  end
515
- @time += sign * (tzHour * 3600 + tzMinute * 60)
511
+
512
+ time_offset = sign * (tzHour * 3600 + tzMinute * 60)
513
+ # UTC-1200 is the most westerly time zone but UTC+1400 is the most
514
+ # easterly time zone (Republic of Kiribati).
515
+ if time_offset < -12 * 3600 || time_offset > 14 * 3600
516
+ raise TjException.new, "Time zone adjustment out of range " +
517
+ "(-1200 - +1400} but is #{zone})"
518
+ end
519
+
520
+ # The time offset must be substracted from the base time to convert it
521
+ # to UTC.
522
+ @time -= time_offset
516
523
  else
517
524
  @time = Time.mktime(year, month, day, hour, minute, second)
518
525
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = TjpSyntaxRules.rb -- The TaskJuggler III Project Management Software
5
5
  #
6
- # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
6
+ # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2020
7
7
  # by Chris Schlaeger <cs@taskjuggler.org>
8
8
  #
9
9
  # This program is free software; you can redistribute it and/or modify
@@ -673,7 +673,7 @@ EOT
673
673
  def rule_color
674
674
  pattern(%w( $STRING ), lambda {
675
675
  col = @val[0]
676
- unless /#[0-9A-Fa-f]{3}/ =~ col || /#[0-9A-Fa-f]{3}/ =~ col
676
+ unless /#[0-9A-Fa-f]{3}/ =~ col || /#[0-9A-Fa-f]{6}/ =~ col
677
677
  error('bad_color',
678
678
  "Color values must be specified as '#RGB' or '#RRGGBB' values",
679
679
  @sourceFileInfo[0])
@@ -3932,7 +3932,13 @@ EOT
3932
3932
  descr('The duration of a task')
3933
3933
 
3934
3934
  singlePattern('_duties')
3935
- descr('List of tasks that the resource is allocated to')
3935
+ descr(<<'EOT'
3936
+ List of tasks that the resource is allocated to
3937
+
3938
+ The list can be customized by the [[listitem.column|listitem]] and
3939
+ [[listtype.column|listtype]] attribute.
3940
+ EOT
3941
+ )
3936
3942
 
3937
3943
  singlePattern('_efficiency')
3938
3944
  descr('Measure for how efficient a resource can perform tasks')
@@ -52,7 +52,7 @@ class TaskJuggler
52
52
 
53
53
  # Write the XMLDocument to the specified file.
54
54
  def write(filename)
55
- f = filename == '.' ? $stdout : File.new(filename.untaint, 'w')
55
+ f = filename == '.' ? $stdout : File.new(filename, 'w')
56
56
  @elements.each do |element|
57
57
  f.puts element.to_s(0)
58
58
  end
@@ -37,7 +37,7 @@ class TaskJuggler
37
37
  @port = nil
38
38
  @webServer = false
39
39
  @webServerPort = 8080
40
- @webdPidFile = File.join(Dir.getwd, ".tj3webd-#{$$}.pid").untaint
40
+ @webdPidFile = File.join(Dir.getwd, ".tj3webd-#{$$}.pid")
41
41
  end
42
42
 
43
43
  def processArguments(argv)
@@ -92,7 +92,7 @@ EOT
92
92
  # Set some config variables if corresponding data was provided via the
93
93
  # command line.
94
94
  broker.port = @port if @port
95
- broker.uriFile = @uriFile.untaint
95
+ broker.uriFile = @uriFile
96
96
  broker.projectFiles = sortInputFiles(files) unless files.empty?
97
97
  broker.daemonize = @daemonize
98
98
  # Create log files for standard IO for each child process if the daemon
@@ -89,7 +89,7 @@ EOT
89
89
  # Set some config variables if corresponding data was provided via the
90
90
  # command line.
91
91
  webServer.port = @port if @port
92
- webServer.uriFile = @uriFile.untaint
92
+ webServer.uriFile = @uriFile
93
93
  webServer.webServerPort = @webServerPort if @webServerPort
94
94
  webServer.daemonize = @daemonize
95
95
  webServer.pidFile = @pidFile
@@ -137,9 +137,9 @@ class TaskJuggler
137
137
  # directory. The second one is the master project file (.tjp file).
138
138
  # Additionally a list of optional .tji files can be provided.
139
139
  def loadProject(args)
140
- dirAndFiles = args.dup.untaint
140
+ dirAndFiles = args.dup
141
141
  # The first argument is the working directory
142
- Dir.chdir(args.shift.untaint)
142
+ Dir.chdir(args.shift)
143
143
 
144
144
  # Save a time stamp of when the project file loading started.
145
145
  @modifiedCheck = TjTime.new
@@ -221,7 +221,7 @@ class TaskJuggler
221
221
  field.to_s
222
222
  else
223
223
  # Duplicate quote characters.
224
- f = field.gsub(/@quote/, "#{@quote * 2}")
224
+ f = field.gsub(Regexp.new(@quote), "#{@quote * 2}")
225
225
  # Enclose the field in quote characters
226
226
  @quote + f.to_s + @quote
227
227
  end
@@ -249,11 +249,15 @@ EOT
249
249
  t << XMLNamedText.new('1', 'Manual')
250
250
  t << XMLNamedText.new('0', 'Summary')
251
251
  t << XMLNamedText.new('0', 'Estimated')
252
- t << XMLNamedText.new('5', 'DurationFormat')
252
+ t << XMLNamedText.new('7', 'DurationFormat')
253
253
  if task['milestone', @scenarioIdx]
254
254
  t << XMLNamedText.new('1', 'Milestone')
255
255
  else
256
+ duration = task['end', @scenarioIdx] - task['start', @scenarioIdx]
257
+ t << XMLNamedText.new(durationToMsp(duration), 'Duration')
258
+ t << XMLNamedText.new(durationToMsp(duration), 'Work')
256
259
  t << XMLNamedText.new('0', 'Milestone')
260
+ t << XMLNamedText.new('1', 'EffortDriven')
257
261
  t << XMLNamedText.new(percentComplete.to_i.to_s,
258
262
  'PercentComplete')
259
263
  t << XMLNamedText.new(percentComplete.to_i.to_s,
@@ -394,8 +398,6 @@ EOT
394
398
  end
395
399
 
396
400
  def durationToMsp(duration)
397
- return '' if duration == 0
398
-
399
401
  hours = (duration / (60 * 60)).to_i
400
402
  minutes = ((duration - (hours * 60 * 60)) / 60).to_i
401
403
  seconds = (duration % 60).to_i
@@ -401,7 +401,7 @@ EOT
401
401
  # The directory needs to be in the same directory as the HTML report.
402
402
  auxDstDir = File.dirname(absoluteFileName(@name)) + '/'
403
403
  # Find the data directory that came with the TaskJuggler installation.
404
- auxSrcDir = AppConfig.dataDirs("data/#{dirName}")[0].untaint
404
+ auxSrcDir = AppConfig.dataDirs("data/#{dirName}")[0]
405
405
  # Raise an error if we haven't found the data directory
406
406
  if auxSrcDir.nil? || !File.exist?(auxSrcDir)
407
407
  dataDirError(dirName, AppConfig.dataSearchDirs("data/#{dirName}"))
@@ -419,13 +419,13 @@ EOT
419
419
  end
420
420
 
421
421
  def directoryUpToDate?(auxSrcDir, auxDstDir)
422
- return false unless File.exist?(auxDstDir.untaint)
422
+ return false unless File.exist?(auxDstDir)
423
423
 
424
424
  Dir.entries(auxSrcDir).each do |file|
425
425
  next if file == '.' || file == '..'
426
426
 
427
- srcFile = (auxSrcDir + '/' + file).untaint
428
- dstFile = (auxDstDir + '/' + file).untaint
427
+ srcFile = (auxSrcDir + '/' + file)
428
+ dstFile = (auxDstDir + '/' + file)
429
429
  return false if !File.exist?(dstFile) ||
430
430
  File.mtime(srcFile) > File.mtime(dstFile)
431
431
  end
@@ -473,7 +473,7 @@ EOT
473
473
  end
474
474
 
475
475
  def absoluteFileName(name)
476
- ((absoluteFileName?(name) ? '' : @project.outputDir) + name).untaint
476
+ (absoluteFileName?(name) ? '' : @project.outputDir) + name
477
477
  end
478
478
 
479
479
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = ReportTableCell.rb -- The TaskJuggler III Project Management Software
5
5
  #
6
- # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
6
+ # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2024
7
7
  # by Chris Schlaeger <cs@taskjuggler.org>
8
8
  #
9
9
  # This program is free software; you can redistribute it and/or modify
@@ -21,7 +21,7 @@ class TaskJuggler
21
21
  class ReportTableCell
22
22
 
23
23
  attr_reader :line
24
- attr_accessor :data, :category, :hidden, :alignment, :padding,
24
+ attr_accessor :data, :category, :hidden, :alignment, :padding, :force_string,
25
25
  :text, :tooltip, :showTooltipHint,
26
26
  :iconTooltip,
27
27
  :cellColor, :indent, :icon, :fontSize, :fontColor,
@@ -60,6 +60,8 @@ class TaskJuggler
60
60
  @alignment = :center
61
61
  # Horizontal padding between frame and cell content
62
62
  @padding = 3
63
+ # Don't convert Strings that look like numbers to String
64
+ @force_string = false
63
65
  # Whether or not to indent the cell. If not nil, it is an Integer
64
66
  # indicating the indentation level.
65
67
  @indent = nil
@@ -181,11 +183,11 @@ class TaskJuggler
181
183
 
182
184
  # Try to convert numbers and other types to their native Ruby type if
183
185
  # they are supported by CSVFile.
184
- native = CSVFile.strToNative(cell)
186
+ native = @force_string ? cell : CSVFile.strToNative(cell)
185
187
 
186
188
  # Only for String objects, we add the indentation.
187
- csv[lineIdx][columnIdx] = (native.is_a?(String) ? indent + native :
188
- native)
189
+ csv[lineIdx][columnIdx] = (native.is_a?(String) && !@force_string ?
190
+ indent + native : native)
189
191
  end
190
192
 
191
193
  return columns
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = TableReport.rb -- The TaskJuggler III Project Management Software
5
5
  #
6
- # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
6
+ # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2024
7
7
  # by Chris Schlaeger <cs@taskjuggler.org>
8
8
  #
9
9
  # This program is free software; you can redistribute it and/or modify
@@ -755,6 +755,7 @@ class TaskJuggler
755
755
  def genCalculatedCell(query, line, columnDef)
756
756
  # Create a new cell
757
757
  cell = newCell(query, line)
758
+ cell.force_string = true if columnDef.id == 'bsi'
758
759
 
759
760
  unless setScenarioSettings(cell, query.scenarioIdx,
760
761
  TableReport.scenarioSpecific?(columnDef.id))