taskjuggler 3.7.1 → 3.8.1

Sign up to get free protection for your applications and to get access to all the features.
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))