Commit | Line | Data |
---|---|---|
86fbb8ca CD |
1 | ;;; org-taskjuggler.el --- TaskJuggler exporter for org-mode |
2 | ;; | |
b73f1974 | 3 | ;; Copyright (C) 2007-2012 Free Software Foundation, Inc. |
86fbb8ca CD |
4 | ;; |
5 | ;; Emacs Lisp Archive Entry | |
6 | ;; Filename: org-taskjuggler.el | |
86fbb8ca CD |
7 | ;; Author: Christian Egli |
8 | ;; Maintainer: Christian Egli | |
9 | ;; Keywords: org, taskjuggler, project planning | |
10 | ;; Description: Converts an org-mode buffer into a taskjuggler project plan | |
11 | ;; URL: | |
12 | ||
13 | ;; This file is part of GNU Emacs. | |
14 | ||
15 | ;; GNU Emacs is free software: you can redistribute it and/or modify | |
16 | ;; it under the terms of the GNU General Public License as published by | |
17 | ;; the Free Software Foundation, either version 3 of the License, or | |
18 | ;; (at your option) any later version. | |
19 | ||
20 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
23 | ;; GNU General Public License for more details. | |
24 | ||
25 | ;; You should have received a copy of the GNU General Public License | |
26 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | |
27 | ||
28 | ;; Commentary: | |
29 | ;; | |
30 | ;; This library implements a TaskJuggler exporter for org-mode. | |
31 | ;; TaskJuggler uses a text format to define projects, tasks and | |
32 | ;; resources, so it is a natural fit for org-mode. It can produce all | |
33 | ;; sorts of reports for tasks or resources in either HTML, CSV or PDF. | |
34 | ;; The current version of TaskJuggler requires KDE but the next | |
35 | ;; version is implemented in Ruby and should therefore run on any | |
36 | ;; platform. | |
37 | ;; | |
38 | ;; The exporter is a bit different from other exporters, such as the | |
39 | ;; HTML and LaTeX exporters for example, in that it does not export | |
40 | ;; all the nodes of a document or strictly follow the order of the | |
41 | ;; nodes in the document. | |
42 | ;; | |
43 | ;; Instead the TaskJuggler exporter looks for a tree that defines the | |
44 | ;; tasks and a optionally tree that defines the resources for this | |
45 | ;; project. It then creates a TaskJuggler file based on these trees | |
46 | ;; and the attributes defined in all the nodes. | |
47 | ;; | |
48 | ;; * Installation | |
49 | ;; | |
50 | ;; Put this file into your load-path and the following line into your | |
51 | ;; ~/.emacs: | |
52 | ;; | |
53 | ;; (require 'org-taskjuggler) | |
54 | ;; | |
55 | ;; The interactive functions are similar to those of the HTML and LaTeX | |
56 | ;; exporters: | |
57 | ;; | |
58 | ;; M-x `org-export-as-taskjuggler' | |
59 | ;; M-x `org-export-as-taskjuggler-and-open' | |
60 | ;; | |
61 | ;; * Tasks | |
62 | ;; | |
63 | ;; Let's illustrate the usage with a small example. Create your tasks | |
64 | ;; as you usually do with org-mode. Assign efforts to each task using | |
65 | ;; properties (it's easiest to do this in the column view). You should | |
66 | ;; end up with something similar to the example by Peter Jones in | |
67 | ;; http://www.contextualdevelopment.com/static/artifacts/articles/2008/project-planning/project-planning.org. | |
68 | ;; Now mark the top node of your tasks with a tag named | |
69 | ;; "taskjuggler_project" (or whatever you customized | |
70 | ;; `org-export-taskjuggler-project-tag' to). You are now ready to | |
71 | ;; export the project plan with `org-export-as-taskjuggler-and-open' | |
27e428e7 | 72 | ;; which will export the project plan and open a Gantt chart in |
86fbb8ca CD |
73 | ;; TaskJugglerUI. |
74 | ;; | |
75 | ;; * Resources | |
3ab2c837 | 76 | ;; |
86fbb8ca CD |
77 | ;; Next you can define resources and assign those to work on specific |
78 | ;; tasks. You can group your resources hierarchically. Tag the top | |
79 | ;; node of the resources with "taskjuggler_resource" (or whatever you | |
80 | ;; customized `org-export-taskjuggler-resource-tag' to). You can | |
81 | ;; optionally assign an identifier (named "resource_id") to the | |
82 | ;; resources (using the standard org properties commands) or you can | |
83 | ;; let the exporter generate identifiers automatically (the exporter | |
84 | ;; picks the first word of the headline as the identifier as long as | |
85 | ;; it is unique, see the documentation of | |
86 | ;; `org-taskjuggler-get-unique-id'). Using that identifier you can | |
87 | ;; then allocate resources to tasks. This is again done with the | |
88 | ;; "allocate" property on the tasks. Do this in column view or when on | |
89 | ;; the task type | |
90 | ;; | |
91 | ;; C-c C-x p allocate RET <resource_id> RET | |
92 | ;; | |
93 | ;; Once the allocations are done you can again export to TaskJuggler | |
94 | ;; and check in the Resource Allocation Graph which person is working | |
95 | ;; on what task at what time. | |
96 | ;; | |
97 | ;; * Export of properties | |
98 | ;; | |
99 | ;; The exporter also takes TODO state information into consideration, | |
100 | ;; i.e. if a task is marked as done it will have the corresponding | |
101 | ;; attribute in TaskJuggler ("complete 100"). Also it will export any | |
102 | ;; property on a task resource or resource node which is known to | |
103 | ;; TaskJuggler, such as limits, vacation, shift, booking, efficiency, | |
104 | ;; journalentry, rate for resources or account, start, note, duration, | |
105 | ;; end, journalentry, milestone, reference, responsible, scheduling, | |
106 | ;; etc for tasks. | |
107 | ;; | |
108 | ;; * Dependencies | |
3ab2c837 | 109 | ;; |
86fbb8ca CD |
110 | ;; The exporter will handle dependencies that are defined in the tasks |
111 | ;; either with the ORDERED attribute (see TODO dependencies in the Org | |
112 | ;; mode manual) or with the BLOCKER attribute (see org-depend.el) or | |
113 | ;; alternatively with a depends attribute. Both the BLOCKER and the | |
114 | ;; depends attribute can be either "previous-sibling" or a reference | |
115 | ;; to an identifier (named "task_id") which is defined for another | |
116 | ;; task in the project. BLOCKER and the depends attribute can define | |
117 | ;; multiple dependencies separated by either space or comma. You can | |
118 | ;; also specify optional attributes on the dependency by simply | |
119 | ;; appending it. The following examples should illustrate this: | |
120 | ;; | |
121 | ;; * Training material | |
122 | ;; :PROPERTIES: | |
123 | ;; :task_id: training_material | |
124 | ;; :ORDERED: t | |
125 | ;; :END: | |
126 | ;; ** Markup Guidelines | |
127 | ;; :PROPERTIES: | |
3ab2c837 | 128 | ;; :Effort: 2d |
86fbb8ca CD |
129 | ;; :END: |
130 | ;; ** Workflow Guidelines | |
131 | ;; :PROPERTIES: | |
3ab2c837 | 132 | ;; :Effort: 2d |
86fbb8ca CD |
133 | ;; :END: |
134 | ;; * Presentation | |
135 | ;; :PROPERTIES: | |
3ab2c837 | 136 | ;; :Effort: 2d |
86fbb8ca CD |
137 | ;; :BLOCKER: training_material { gapduration 1d } some_other_task |
138 | ;; :END: | |
3ab2c837 | 139 | ;; |
86fbb8ca CD |
140 | ;;;; * TODO |
141 | ;; - Use SCHEDULED and DEADLINE information (not just start and end | |
142 | ;; properties). | |
143 | ;; - Look at org-file-properties, org-global-properties and | |
144 | ;; org-global-properties-fixed | |
145 | ;; - What about property inheritance and org-property-inherit-p? | |
146 | ;; - Use TYPE_TODO as an way to assign resources | |
147 | ;; - Make sure multiple dependency definitions (i.e. BLOCKER on | |
148 | ;; previous-sibling and on a specific task_id) in multiple | |
149 | ;; attributes are properly exported. | |
150 | ;; | |
151 | ;;; Code: | |
152 | ||
153 | (eval-when-compile | |
154 | (require 'cl)) | |
155 | ||
156 | (require 'org) | |
157 | (require 'org-exp) | |
158 | ||
159 | ;;; User variables: | |
160 | ||
161 | (defgroup org-export-taskjuggler nil | |
162 | "Options for exporting Org-mode files to TaskJuggler." | |
163 | :tag "Org Export TaskJuggler" | |
164 | :group 'org-export) | |
165 | ||
166 | (defcustom org-export-taskjuggler-extension ".tjp" | |
167 | "Extension of TaskJuggler files." | |
168 | :group 'org-export-taskjuggler | |
372d7b21 | 169 | :version "24.1" |
86fbb8ca CD |
170 | :type 'string) |
171 | ||
172 | (defcustom org-export-taskjuggler-project-tag "taskjuggler_project" | |
173 | "Tag, property or todo used to find the tree containing all | |
174 | the tasks for the project." | |
175 | :group 'org-export-taskjuggler | |
372d7b21 | 176 | :version "24.1" |
86fbb8ca CD |
177 | :type 'string) |
178 | ||
179 | (defcustom org-export-taskjuggler-resource-tag "taskjuggler_resource" | |
180 | "Tag, property or todo used to find the tree containing all the | |
181 | resources for the project." | |
182 | :group 'org-export-taskjuggler | |
372d7b21 | 183 | :version "24.1" |
86fbb8ca CD |
184 | :type 'string) |
185 | ||
3ab2c837 BG |
186 | (defcustom org-export-taskjuggler-target-version 2.4 |
187 | "Which version of TaskJuggler the exporter is targeting." | |
188 | :group 'org-export-taskjuggler | |
372d7b21 | 189 | :version "24.1" |
3ab2c837 BG |
190 | :type 'number) |
191 | ||
86fbb8ca CD |
192 | (defcustom org-export-taskjuggler-default-project-version "1.0" |
193 | "Default version string for the project." | |
194 | :group 'org-export-taskjuggler | |
372d7b21 | 195 | :version "24.1" |
86fbb8ca CD |
196 | :type 'string) |
197 | ||
198 | (defcustom org-export-taskjuggler-default-project-duration 280 | |
199 | "Default project duration if no start and end date have been defined | |
200 | in the root node of the task tree, i.e. the tree that has been marked | |
201 | with `org-export-taskjuggler-project-tag'" | |
202 | :group 'org-export-taskjuggler | |
372d7b21 | 203 | :version "24.1" |
86fbb8ca CD |
204 | :type 'integer) |
205 | ||
3ab2c837 | 206 | (defcustom org-export-taskjuggler-default-reports |
86fbb8ca CD |
207 | '("taskreport \"Gantt Chart\" { |
208 | headline \"Project Gantt Chart\" | |
209 | columns hierarchindex, name, start, end, effort, duration, completed, chart | |
210 | timeformat \"%Y-%m-%d\" | |
211 | hideresource 1 | |
212 | loadunit shortauto | |
213 | }" | |
214 | "resourcereport \"Resource Graph\" { | |
215 | headline \"Resource Allocation Graph\" | |
216 | columns no, name, utilization, freeload, chart | |
217 | loadunit shortauto | |
218 | sorttasks startup | |
219 | hidetask ~isleaf() | |
220 | }") | |
221 | "Default reports for the project." | |
222 | :group 'org-export-taskjuggler | |
372d7b21 | 223 | :version "24.1" |
86fbb8ca CD |
224 | :type '(repeat (string :tag "Report"))) |
225 | ||
3ab2c837 | 226 | (defcustom org-export-taskjuggler-default-global-properties |
86fbb8ca CD |
227 | "shift s40 \"Part time shift\" { |
228 | workinghours wed, thu, fri off | |
229 | } | |
230 | " | |
231 | "Default global properties for the project. Here you typically | |
232 | define global properties such as shifts, accounts, rates, | |
233 | vacation, macros and flags. Any property that is allowed within | |
234 | the TaskJuggler file can be inserted. You could for example | |
3ab2c837 | 235 | include another TaskJuggler file. |
86fbb8ca CD |
236 | |
237 | The global properties are inserted after the project declaration | |
238 | but before any resource and task declarations." | |
239 | :group 'org-export-taskjuggler | |
372d7b21 | 240 | :version "24.1" |
86fbb8ca CD |
241 | :type '(string :tag "Preamble")) |
242 | ||
243 | ;;; Hooks | |
244 | ||
245 | (defvar org-export-taskjuggler-final-hook nil | |
246 | "Hook run at the end of TaskJuggler export, in the new buffer.") | |
247 | ||
248 | ;;; Autoload functions: | |
249 | ||
250 | ;; avoid compiler warning about free variable | |
251 | (defvar org-export-taskjuggler-old-level) | |
252 | ||
253 | ;;;###autoload | |
254 | (defun org-export-as-taskjuggler () | |
255 | "Export parts of the current buffer as a TaskJuggler file. | |
256 | The exporter looks for a tree with tag, property or todo that | |
257 | matches `org-export-taskjuggler-project-tag' and takes this as | |
258 | the tasks for this project. The first node of this tree defines | |
259 | the project properties such as project name and project period. | |
260 | If there is a tree with tag, property or todo that matches | |
261 | `org-export-taskjuggler-resource-tag' this three is taken as | |
262 | resources for the project. If no resources are specified, a | |
263 | default resource is created and allocated to the project. Also | |
264 | the taskjuggler project will be created with default reports as | |
265 | defined in `org-export-taskjuggler-default-reports'." | |
266 | (interactive) | |
267 | ||
268 | (message "Exporting...") | |
269 | (setq-default org-done-keywords org-done-keywords) | |
270 | (let* ((tasks | |
271 | (org-taskjuggler-resolve-dependencies | |
3ab2c837 BG |
272 | (org-taskjuggler-assign-task-ids |
273 | (org-taskjuggler-compute-task-leafiness | |
274 | (org-map-entries | |
275 | 'org-taskjuggler-components | |
276 | org-export-taskjuggler-project-tag nil 'archive 'comment))))) | |
86fbb8ca CD |
277 | (resources |
278 | (org-taskjuggler-assign-resource-ids | |
3ab2c837 BG |
279 | (org-map-entries |
280 | 'org-taskjuggler-components | |
86fbb8ca CD |
281 | org-export-taskjuggler-resource-tag nil 'archive 'comment))) |
282 | (filename (expand-file-name | |
283 | (concat | |
284 | (file-name-sans-extension | |
285 | (file-name-nondirectory buffer-file-name)) | |
286 | org-export-taskjuggler-extension))) | |
287 | (buffer (find-file-noselect filename)) | |
e66ba1df | 288 | (old-buffer (current-buffer)) |
86fbb8ca CD |
289 | (org-export-taskjuggler-old-level 0) |
290 | task resource) | |
291 | (unless tasks | |
292 | (error "No tasks specified")) | |
293 | ;; add a default resource | |
294 | (unless resources | |
3ab2c837 BG |
295 | (setq resources |
296 | `((("resource_id" . ,(user-login-name)) | |
297 | ("headline" . ,user-full-name) | |
86fbb8ca CD |
298 | ("level" . 1))))) |
299 | ;; add a default allocation to the first task if none was given | |
300 | (unless (assoc "allocate" (car tasks)) | |
301 | (let ((task (car tasks)) | |
302 | (resource-id (cdr (assoc "resource_id" (car resources))))) | |
303 | (setcar tasks (push (cons "allocate" resource-id) task)))) | |
304 | ;; add a default start date to the first task if none was given | |
305 | (unless (assoc "start" (car tasks)) | |
306 | (let ((task (car tasks)) | |
307 | (time-string (format-time-string "%Y-%m-%d"))) | |
308 | (setcar tasks (push (cons "start" time-string) task)))) | |
309 | ;; add a default version if none was given | |
310 | (unless (assoc "version" (car tasks)) | |
311 | (let ((task (car tasks)) | |
312 | (version org-export-taskjuggler-default-project-version)) | |
313 | (setcar tasks (push (cons "version" version) task)))) | |
314 | (with-current-buffer buffer | |
315 | (erase-buffer) | |
e66ba1df | 316 | (org-clone-local-variables old-buffer "^org-") |
86fbb8ca CD |
317 | (org-taskjuggler-open-project (car tasks)) |
318 | (insert org-export-taskjuggler-default-global-properties) | |
319 | (insert "\n") | |
320 | (dolist (resource resources) | |
321 | (let ((level (cdr (assoc "level" resource)))) | |
322 | (org-taskjuggler-close-maybe level) | |
323 | (org-taskjuggler-open-resource resource) | |
324 | (setq org-export-taskjuggler-old-level level))) | |
325 | (org-taskjuggler-close-maybe 1) | |
326 | (setq org-export-taskjuggler-old-level 0) | |
327 | (dolist (task tasks) | |
328 | (let ((level (cdr (assoc "level" task)))) | |
329 | (org-taskjuggler-close-maybe level) | |
330 | (org-taskjuggler-open-task task) | |
331 | (setq org-export-taskjuggler-old-level level))) | |
332 | (org-taskjuggler-close-maybe 1) | |
333 | (org-taskjuggler-insert-reports) | |
334 | (save-buffer) | |
335 | (or (org-export-push-to-kill-ring "TaskJuggler") | |
336 | (message "Exporting... done")) | |
337 | (current-buffer)))) | |
338 | ||
339 | ;;;###autoload | |
340 | (defun org-export-as-taskjuggler-and-open () | |
341 | "Export the current buffer as a TaskJuggler file and open it | |
342 | with the TaskJuggler GUI." | |
343 | (interactive) | |
344 | (let* ((file-name (buffer-file-name (org-export-as-taskjuggler))) | |
345 | (process-name "TaskJugglerUI") | |
346 | (command (concat process-name " " file-name))) | |
347 | (start-process-shell-command process-name nil command))) | |
348 | ||
3ab2c837 BG |
349 | (defun org-taskjuggler-targeting-tj3-p () |
350 | "Return true if we are targeting TaskJuggler III." | |
351 | (>= org-export-taskjuggler-target-version 3.0)) | |
352 | ||
86fbb8ca CD |
353 | (defun org-taskjuggler-parent-is-ordered-p () |
354 | "Return true if the parent of the current node has a property | |
355 | \"ORDERED\". Return nil otherwise." | |
356 | (save-excursion | |
357 | (and (org-up-heading-safe) (org-entry-get (point) "ORDERED")))) | |
358 | ||
359 | (defun org-taskjuggler-components () | |
360 | "Return an alist containing all the pertinent information for | |
361 | the current node such as the headline, the level, todo state | |
362 | information, all the properties, etc." | |
363 | (let* ((props (org-entry-properties)) | |
364 | (components (org-heading-components)) | |
365 | (level (nth 1 components)) | |
14e1337f BG |
366 | (headline |
367 | (replace-regexp-in-string | |
3ab2c837 | 368 | "\"" "\\\"" (nth 4 components) t t)) ; quote double quotes in headlines |
86fbb8ca CD |
369 | (parent-ordered (org-taskjuggler-parent-is-ordered-p))) |
370 | (push (cons "level" level) props) | |
371 | (push (cons "headline" headline) props) | |
372 | (push (cons "parent-ordered" parent-ordered) props))) | |
373 | ||
374 | (defun org-taskjuggler-assign-task-ids (tasks) | |
375 | "Given a list of tasks return the same list assigning a unique id | |
376 | and the full path to each task. Taskjuggler takes hierarchical ids. | |
377 | For that reason we have to make ids locally unique and we have to keep | |
378 | a path to the current task." | |
379 | (let ((previous-level 0) | |
380 | unique-ids unique-id | |
381 | path | |
382 | task resolved-tasks tmp) | |
383 | (dolist (task tasks resolved-tasks) | |
384 | (let ((level (cdr (assoc "level" task)))) | |
385 | (cond | |
3ab2c837 | 386 | ((< previous-level level) |
86fbb8ca CD |
387 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) |
388 | (dotimes (tmp (- level previous-level)) | |
389 | (push (list unique-id) unique-ids) | |
390 | (push unique-id path))) | |
3ab2c837 | 391 | ((= previous-level level) |
86fbb8ca CD |
392 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) |
393 | (push unique-id (car unique-ids)) | |
394 | (setcar path unique-id)) | |
3ab2c837 | 395 | ((> previous-level level) |
86fbb8ca CD |
396 | (dotimes (tmp (- previous-level level)) |
397 | (pop unique-ids) | |
398 | (pop path)) | |
399 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) | |
400 | (push unique-id (car unique-ids)) | |
401 | (setcar path unique-id))) | |
402 | (push (cons "unique-id" unique-id) task) | |
403 | (push (cons "path" (mapconcat 'identity (reverse path) ".")) task) | |
404 | (setq previous-level level) | |
405 | (setq resolved-tasks (append resolved-tasks (list task))))))) | |
406 | ||
3ab2c837 BG |
407 | (defun org-taskjuggler-compute-task-leafiness (tasks) |
408 | "Figure out if each task is a leaf by looking at it's level, | |
409 | and the level of its successor. If the successor is higher (ie | |
410 | deeper), then it's not a leaf." | |
411 | (let (new-list) | |
412 | (while (car tasks) | |
413 | (let ((task (car tasks)) | |
414 | (successor (car (cdr tasks)))) | |
415 | (cond | |
416 | ;; if a task has no successors it is a leaf | |
14e1337f | 417 | ((null successor) |
3ab2c837 BG |
418 | (push (cons (cons "leaf-node" t) task) new-list)) |
419 | ;; if the successor has a lower level than task it is a leaf | |
14e1337f | 420 | ((<= (cdr (assoc "level" successor)) (cdr (assoc "level" task))) |
3ab2c837 BG |
421 | (push (cons (cons "leaf-node" t) task) new-list)) |
422 | ;; otherwise examine the rest of the tasks | |
423 | (t (push task new-list)))) | |
424 | (setq tasks (cdr tasks))) | |
425 | (nreverse new-list))) | |
426 | ||
427 | (defun org-taskjuggler-assign-resource-ids (resources) | |
86fbb8ca CD |
428 | "Given a list of resources return the same list, assigning a |
429 | unique id to each resource." | |
3ab2c837 BG |
430 | (let (unique-ids new-list) |
431 | (dolist (resource resources new-list) | |
432 | (let ((unique-id (org-taskjuggler-get-unique-id resource unique-ids))) | |
433 | (push (cons "unique-id" unique-id) resource) | |
434 | (push unique-id unique-ids) | |
435 | (push resource new-list))) | |
436 | (nreverse new-list))) | |
86fbb8ca CD |
437 | |
438 | (defun org-taskjuggler-resolve-dependencies (tasks) | |
439 | (let ((previous-level 0) | |
440 | siblings | |
441 | task resolved-tasks) | |
442 | (dolist (task tasks resolved-tasks) | |
443 | (let* ((level (cdr (assoc "level" task))) | |
444 | (depends (cdr (assoc "depends" task))) | |
445 | (parent-ordered (cdr (assoc "parent-ordered" task))) | |
446 | (blocker (cdr (assoc "BLOCKER" task))) | |
3ab2c837 | 447 | (blocked-on-previous |
86fbb8ca CD |
448 | (and blocker (string-match "previous-sibling" blocker))) |
449 | (dependencies | |
450 | (org-taskjuggler-resolve-explicit-dependencies | |
3ab2c837 | 451 | (append |
86fbb8ca | 452 | (and depends (org-taskjuggler-tokenize-dependencies depends)) |
3ab2c837 | 453 | (and blocker (org-taskjuggler-tokenize-dependencies blocker))) |
86fbb8ca CD |
454 | tasks)) |
455 | previous-sibling) | |
456 | ; update previous sibling info | |
457 | (cond | |
3ab2c837 | 458 | ((< previous-level level) |
86fbb8ca CD |
459 | (dotimes (tmp (- level previous-level)) |
460 | (push task siblings))) | |
461 | ((= previous-level level) | |
462 | (setq previous-sibling (car siblings)) | |
463 | (setcar siblings task)) | |
3ab2c837 | 464 | ((> previous-level level) |
86fbb8ca CD |
465 | (dotimes (tmp (- previous-level level)) |
466 | (pop siblings)) | |
467 | (setq previous-sibling (car siblings)) | |
468 | (setcar siblings task))) | |
469 | ; insert a dependency on previous sibling if the parent is | |
470 | ; ordered or if the tasks has a BLOCKER attribute with value "previous-sibling" | |
471 | (when (or (and previous-sibling parent-ordered) blocked-on-previous) | |
472 | (push (format "!%s" (cdr (assoc "unique-id" previous-sibling))) dependencies)) | |
473 | ; store dependency information | |
3ab2c837 | 474 | (when dependencies |
86fbb8ca CD |
475 | (push (cons "depends" (mapconcat 'identity dependencies ", ")) task)) |
476 | (setq previous-level level) | |
477 | (setq resolved-tasks (append resolved-tasks (list task))))))) | |
478 | ||
479 | (defun org-taskjuggler-tokenize-dependencies (dependencies) | |
480 | "Split a dependency property value DEPENDENCIES into the | |
481 | individual dependencies and return them as a list while keeping | |
482 | the optional arguments (such as gapduration) for the | |
483 | dependencies. A dependency will have to match `[-a-zA-Z0-9_]+'." | |
3ab2c837 | 484 | (cond |
86fbb8ca CD |
485 | ((string-match "^ *$" dependencies) nil) |
486 | ((string-match "^[ \t]*\\([-a-zA-Z0-9_]+\\([ \t]*{[^}]+}\\)?\\)[ \t,]*" dependencies) | |
3ab2c837 | 487 | (cons |
86fbb8ca CD |
488 | (substring dependencies (match-beginning 1) (match-end 1)) |
489 | (org-taskjuggler-tokenize-dependencies (substring dependencies (match-end 0))))) | |
490 | (t (error (format "invalid dependency id %s" dependencies))))) | |
491 | ||
492 | (defun org-taskjuggler-resolve-explicit-dependencies (dependencies tasks) | |
493 | "For each dependency in DEPENDENCIES try to find a | |
494 | corresponding task with a matching property \"task_id\" in TASKS. | |
495 | Return a list containing the resolved links for all DEPENDENCIES | |
496 | where a matching tasks was found. If the dependency is | |
497 | \"previous-sibling\" it is ignored (as this is dealt with in | |
498 | `org-taskjuggler-resolve-dependencies'). If there is no matching | |
499 | task the dependency is ignored and a warning is displayed ." | |
500 | (unless (null dependencies) | |
3ab2c837 | 501 | (let* |
86fbb8ca CD |
502 | ;; the dependency might have optional attributes such as "{ |
503 | ;; gapduration 5d }", so only use the first string as id for the | |
504 | ;; dependency | |
505 | ((dependency (car dependencies)) | |
506 | (id (car (split-string dependency))) | |
3ab2c837 | 507 | (optional-attributes |
86fbb8ca CD |
508 | (mapconcat 'identity (cdr (split-string dependency)) " ")) |
509 | (path (org-taskjuggler-find-task-with-id id tasks))) | |
3ab2c837 | 510 | (cond |
86fbb8ca CD |
511 | ;; ignore previous sibling dependencies |
512 | ((equal (car dependencies) "previous-sibling") | |
513 | (org-taskjuggler-resolve-explicit-dependencies (cdr dependencies) tasks)) | |
514 | ;; if the id is found in another task use its path | |
3ab2c837 | 515 | ((not (null path)) |
86fbb8ca | 516 | (cons (mapconcat 'identity (list path optional-attributes) " ") |
3ab2c837 | 517 | (org-taskjuggler-resolve-explicit-dependencies |
86fbb8ca CD |
518 | (cdr dependencies) tasks))) |
519 | ;; warn about dangling dependency but otherwise ignore it | |
3ab2c837 BG |
520 | (t (display-warning |
521 | 'org-export-taskjuggler | |
86fbb8ca CD |
522 | (format "No task with matching property \"task_id\" found for id %s" id)) |
523 | (org-taskjuggler-resolve-explicit-dependencies (cdr dependencies) tasks)))))) | |
524 | ||
525 | (defun org-taskjuggler-find-task-with-id (id tasks) | |
526 | "Find ID in tasks. If found return the path of task. Otherwise | |
527 | return nil." | |
528 | (let ((task-id (cdr (assoc "task_id" (car tasks)))) | |
529 | (path (cdr (assoc "path" (car tasks))))) | |
3ab2c837 | 530 | (cond |
86fbb8ca CD |
531 | ((null tasks) nil) |
532 | ((equal task-id id) path) | |
533 | (t (org-taskjuggler-find-task-with-id id (cdr tasks)))))) | |
534 | ||
535 | (defun org-taskjuggler-get-unique-id (item unique-ids) | |
536 | "Return a unique id for an ITEM which can be a task or a resource. | |
537 | The id is derived from the headline and made unique against | |
538 | UNIQUE-IDS. If the (downcased) first token of the headline is not | |
539 | unique try to add more (downcased) tokens of the headline or | |
540 | finally add more underscore characters (\"_\")." | |
541 | (let* ((headline (cdr (assoc "headline" item))) | |
542 | (parts (split-string headline)) | |
543 | (id (org-taskjuggler-clean-id (downcase (pop parts))))) | |
544 | ; try to add more parts of the headline to make it unique | |
afe98dfa | 545 | (while (and (member id unique-ids) (car parts)) |
86fbb8ca CD |
546 | (setq id (concat id "_" (org-taskjuggler-clean-id (downcase (pop parts)))))) |
547 | ; if its still not unique add "_" | |
548 | (while (member id unique-ids) | |
549 | (setq id (concat id "_"))) | |
550 | id)) | |
3ab2c837 | 551 | |
86fbb8ca CD |
552 | (defun org-taskjuggler-clean-id (id) |
553 | "Clean and return ID to make it acceptable for taskjuggler." | |
3ab2c837 BG |
554 | (and id |
555 | ;; replace non-ascii by _ | |
556 | (replace-regexp-in-string | |
557 | "[^a-zA-Z0-9_]" "_" | |
558 | ;; make sure id doesn't start with a number | |
559 | (replace-regexp-in-string "^\\([0-9]\\)" "_\\1" id)))) | |
86fbb8ca CD |
560 | |
561 | (defun org-taskjuggler-open-project (project) | |
562 | "Insert the beginning of a project declaration. All valid | |
563 | attributes from the PROJECT alist are inserted. If no end date is | |
564 | specified it is calculated | |
565 | `org-export-taskjuggler-default-project-duration' days from now." | |
566 | (let* ((unique-id (cdr (assoc "unique-id" project))) | |
3ab2c837 BG |
567 | (headline (cdr (assoc "headline" project))) |
568 | (version (cdr (assoc "version" project))) | |
569 | (start (cdr (assoc "start" project))) | |
570 | (end (cdr (assoc "end" project)))) | |
571 | (insert | |
86fbb8ca CD |
572 | (format "project %s \"%s\" \"%s\" %s +%sd {\n }\n" |
573 | unique-id headline version start | |
574 | org-export-taskjuggler-default-project-duration)))) | |
575 | ||
576 | (defun org-taskjuggler-filter-and-join (items) | |
577 | "Filter all nil elements from ITEMS and join the remaining ones | |
578 | with separator \"\n\"." | |
579 | (let ((filtered-items (remq nil items))) | |
580 | (and filtered-items (mapconcat 'identity filtered-items "\n")))) | |
3ab2c837 | 581 | |
86fbb8ca | 582 | (defun org-taskjuggler-get-attributes (item attributes) |
27e428e7 | 583 | "Return all attribute as a single formatted string. ITEM is an |
86fbb8ca CD |
584 | alist representing either a resource or a task. ATTRIBUTES is a |
585 | list of symbols. Only entries from ITEM are considered that are | |
586 | listed in ATTRIBUTES." | |
3ab2c837 | 587 | (org-taskjuggler-filter-and-join |
86fbb8ca | 588 | (mapcar |
3ab2c837 BG |
589 | (lambda (attribute) |
590 | (org-taskjuggler-filter-and-join | |
86fbb8ca CD |
591 | (org-taskjuggler-get-attribute item attribute))) |
592 | attributes))) | |
593 | ||
594 | (defun org-taskjuggler-get-attribute (item attribute) | |
595 | "Return a list of strings containing the properly formatted | |
596 | taskjuggler declaration for a given ATTRIBUTE in ITEM (an alist). | |
597 | If the ATTRIBUTE is not in ITEM return nil." | |
3ab2c837 | 598 | (cond |
86fbb8ca CD |
599 | ((null item) nil) |
600 | ((equal (symbol-name attribute) (car (car item))) | |
601 | (cons (format "%s %s" (symbol-name attribute) (cdr (car item))) | |
602 | (org-taskjuggler-get-attribute (cdr item) attribute))) | |
603 | (t (org-taskjuggler-get-attribute (cdr item) attribute)))) | |
604 | ||
605 | (defun org-taskjuggler-open-resource (resource) | |
606 | "Insert the beginning of a resource declaration. All valid | |
607 | attributes from the RESOURCE alist are inserted. If the RESOURCE | |
608 | defines a property \"resource_id\" it will be used as the id for | |
609 | this resource. Otherwise it will use the ID property. If neither | |
610 | is defined it will calculate a unique id for the resource using | |
611 | `org-taskjuggler-get-unique-id'." | |
3ab2c837 BG |
612 | (let ((id (org-taskjuggler-clean-id |
613 | (or (cdr (assoc "resource_id" resource)) | |
614 | (cdr (assoc "ID" resource)) | |
86fbb8ca CD |
615 | (cdr (assoc "unique-id" resource))))) |
616 | (headline (cdr (assoc "headline" resource))) | |
617 | (attributes '(limits vacation shift booking efficiency journalentry rate))) | |
3ab2c837 BG |
618 | (insert |
619 | (concat | |
86fbb8ca CD |
620 | "resource " id " \"" headline "\" {\n " |
621 | (org-taskjuggler-get-attributes resource attributes) "\n")))) | |
622 | ||
623 | (defun org-taskjuggler-clean-effort (effort) | |
624 | "Translate effort strings into a format acceptable to taskjuggler, | |
3ab2c837 BG |
625 | i.e. REAL UNIT. A valid effort string can be anything that is |
626 | accepted by `org-duration-string-to-minutesĀ“." | |
627 | (cond | |
86fbb8ca | 628 | ((null effort) effort) |
3ab2c837 BG |
629 | (t (let* ((minutes (org-duration-string-to-minutes effort)) |
630 | (hours (/ minutes 60.0))) | |
631 | (format "%.1fh" hours))))) | |
86fbb8ca CD |
632 | |
633 | (defun org-taskjuggler-get-priority (priority) | |
634 | "Return a priority between 1 and 1000 based on PRIORITY, an | |
635 | org-mode priority string." | |
3ab2c837 | 636 | (max 1 (/ (* 1000 (- org-lowest-priority (string-to-char priority))) |
86fbb8ca CD |
637 | (- org-lowest-priority org-highest-priority)))) |
638 | ||
639 | (defun org-taskjuggler-open-task (task) | |
640 | (let* ((unique-id (cdr (assoc "unique-id" task))) | |
3ab2c837 BG |
641 | (headline (cdr (assoc "headline" task))) |
642 | (effort (org-taskjuggler-clean-effort (cdr (assoc org-effort-property task)))) | |
643 | (depends (cdr (assoc "depends" task))) | |
644 | (allocate (cdr (assoc "allocate" task))) | |
645 | (priority-raw (cdr (assoc "PRIORITY" task))) | |
646 | (priority (and priority-raw (org-taskjuggler-get-priority priority-raw))) | |
647 | (state (cdr (assoc "TODO" task))) | |
648 | (complete (or (and (member state org-done-keywords) "100") | |
649 | (cdr (assoc "complete" task)))) | |
650 | (parent-ordered (cdr (assoc "parent-ordered" task))) | |
651 | (previous-sibling (cdr (assoc "previous-sibling" task))) | |
652 | (milestone (or (cdr (assoc "milestone" task)) | |
653 | (and (assoc "leaf-node" task) | |
654 | (not (or effort | |
655 | (cdr (assoc "duration" task)) | |
656 | (cdr (assoc "end" task)) | |
657 | (cdr (assoc "period" task))))))) | |
658 | (attributes | |
659 | '(account start note duration endbuffer endcredit end | |
660 | flags journalentry length maxend maxstart minend | |
661 | minstart period reference responsible scheduling | |
662 | startbuffer startcredit statusnote))) | |
86fbb8ca | 663 | (insert |
3ab2c837 BG |
664 | (concat |
665 | "task " unique-id " \"" headline "\" {\n" | |
86fbb8ca CD |
666 | (if (and parent-ordered previous-sibling) |
667 | (format " depends %s\n" previous-sibling) | |
668 | (and depends (format " depends %s\n" depends))) | |
3ab2c837 BG |
669 | (and allocate (format " purge %s\n allocate %s\n" |
670 | (or (and (org-taskjuggler-targeting-tj3-p) "allocate") | |
671 | "allocations") | |
672 | allocate)) | |
86fbb8ca CD |
673 | (and complete (format " complete %s\n" complete)) |
674 | (and effort (format " effort %s\n" effort)) | |
675 | (and priority (format " priority %s\n" priority)) | |
3ab2c837 BG |
676 | (and milestone (format " milestone\n")) |
677 | ||
86fbb8ca CD |
678 | (org-taskjuggler-get-attributes task attributes) |
679 | "\n")))) | |
680 | ||
681 | (defun org-taskjuggler-close-maybe (level) | |
3ab2c837 | 682 | (while (> org-export-taskjuggler-old-level level) |
86fbb8ca CD |
683 | (insert "}\n") |
684 | (setq org-export-taskjuggler-old-level (1- org-export-taskjuggler-old-level))) | |
685 | (when (= org-export-taskjuggler-old-level level) | |
686 | (insert "}\n"))) | |
687 | ||
688 | (defun org-taskjuggler-insert-reports () | |
689 | (let (report) | |
690 | (dolist (report org-export-taskjuggler-default-reports) | |
691 | (insert report "\n")))) | |
692 | ||
693 | (provide 'org-taskjuggler) | |
694 | ||
695 | ;;; org-taskjuggler.el ends here |