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