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' | |
e66ba1df | 72 | ;; which will export the project plan and open a gant 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 | |
169 | :type 'string) | |
170 | ||
171 | (defcustom org-export-taskjuggler-project-tag "taskjuggler_project" | |
172 | "Tag, property or todo used to find the tree containing all | |
173 | the tasks for the project." | |
174 | :group 'org-export-taskjuggler | |
175 | :type 'string) | |
176 | ||
177 | (defcustom org-export-taskjuggler-resource-tag "taskjuggler_resource" | |
178 | "Tag, property or todo used to find the tree containing all the | |
179 | resources for the project." | |
180 | :group 'org-export-taskjuggler | |
181 | :type 'string) | |
182 | ||
3ab2c837 BG |
183 | (defcustom org-export-taskjuggler-target-version 2.4 |
184 | "Which version of TaskJuggler the exporter is targeting." | |
185 | :group 'org-export-taskjuggler | |
186 | :type 'number) | |
187 | ||
86fbb8ca CD |
188 | (defcustom org-export-taskjuggler-default-project-version "1.0" |
189 | "Default version string for the project." | |
190 | :group 'org-export-taskjuggler | |
191 | :type 'string) | |
192 | ||
193 | (defcustom org-export-taskjuggler-default-project-duration 280 | |
194 | "Default project duration if no start and end date have been defined | |
195 | in the root node of the task tree, i.e. the tree that has been marked | |
196 | with `org-export-taskjuggler-project-tag'" | |
197 | :group 'org-export-taskjuggler | |
198 | :type 'integer) | |
199 | ||
3ab2c837 | 200 | (defcustom org-export-taskjuggler-default-reports |
86fbb8ca CD |
201 | '("taskreport \"Gantt Chart\" { |
202 | headline \"Project Gantt Chart\" | |
203 | columns hierarchindex, name, start, end, effort, duration, completed, chart | |
204 | timeformat \"%Y-%m-%d\" | |
205 | hideresource 1 | |
206 | loadunit shortauto | |
207 | }" | |
208 | "resourcereport \"Resource Graph\" { | |
209 | headline \"Resource Allocation Graph\" | |
210 | columns no, name, utilization, freeload, chart | |
211 | loadunit shortauto | |
212 | sorttasks startup | |
213 | hidetask ~isleaf() | |
214 | }") | |
215 | "Default reports for the project." | |
216 | :group 'org-export-taskjuggler | |
217 | :type '(repeat (string :tag "Report"))) | |
218 | ||
3ab2c837 | 219 | (defcustom org-export-taskjuggler-default-global-properties |
86fbb8ca CD |
220 | "shift s40 \"Part time shift\" { |
221 | workinghours wed, thu, fri off | |
222 | } | |
223 | " | |
224 | "Default global properties for the project. Here you typically | |
225 | define global properties such as shifts, accounts, rates, | |
226 | vacation, macros and flags. Any property that is allowed within | |
227 | the TaskJuggler file can be inserted. You could for example | |
3ab2c837 | 228 | include another TaskJuggler file. |
86fbb8ca CD |
229 | |
230 | The global properties are inserted after the project declaration | |
231 | but before any resource and task declarations." | |
232 | :group 'org-export-taskjuggler | |
233 | :type '(string :tag "Preamble")) | |
234 | ||
235 | ;;; Hooks | |
236 | ||
237 | (defvar org-export-taskjuggler-final-hook nil | |
238 | "Hook run at the end of TaskJuggler export, in the new buffer.") | |
239 | ||
240 | ;;; Autoload functions: | |
241 | ||
242 | ;; avoid compiler warning about free variable | |
243 | (defvar org-export-taskjuggler-old-level) | |
244 | ||
245 | ;;;###autoload | |
246 | (defun org-export-as-taskjuggler () | |
247 | "Export parts of the current buffer as a TaskJuggler file. | |
248 | The exporter looks for a tree with tag, property or todo that | |
249 | matches `org-export-taskjuggler-project-tag' and takes this as | |
250 | the tasks for this project. The first node of this tree defines | |
251 | the project properties such as project name and project period. | |
252 | If there is a tree with tag, property or todo that matches | |
253 | `org-export-taskjuggler-resource-tag' this three is taken as | |
254 | resources for the project. If no resources are specified, a | |
255 | default resource is created and allocated to the project. Also | |
256 | the taskjuggler project will be created with default reports as | |
257 | defined in `org-export-taskjuggler-default-reports'." | |
258 | (interactive) | |
259 | ||
260 | (message "Exporting...") | |
261 | (setq-default org-done-keywords org-done-keywords) | |
262 | (let* ((tasks | |
263 | (org-taskjuggler-resolve-dependencies | |
3ab2c837 BG |
264 | (org-taskjuggler-assign-task-ids |
265 | (org-taskjuggler-compute-task-leafiness | |
266 | (org-map-entries | |
267 | 'org-taskjuggler-components | |
268 | org-export-taskjuggler-project-tag nil 'archive 'comment))))) | |
86fbb8ca CD |
269 | (resources |
270 | (org-taskjuggler-assign-resource-ids | |
3ab2c837 BG |
271 | (org-map-entries |
272 | 'org-taskjuggler-components | |
86fbb8ca CD |
273 | org-export-taskjuggler-resource-tag nil 'archive 'comment))) |
274 | (filename (expand-file-name | |
275 | (concat | |
276 | (file-name-sans-extension | |
277 | (file-name-nondirectory buffer-file-name)) | |
278 | org-export-taskjuggler-extension))) | |
279 | (buffer (find-file-noselect filename)) | |
e66ba1df | 280 | (old-buffer (current-buffer)) |
86fbb8ca CD |
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) | |
e66ba1df | 308 | (org-clone-local-variables old-buffer "^org-") |
86fbb8ca CD |
309 | (org-taskjuggler-open-project (car tasks)) |
310 | (insert org-export-taskjuggler-default-global-properties) | |
311 | (insert "\n") | |
312 | (dolist (resource resources) | |
313 | (let ((level (cdr (assoc "level" resource)))) | |
314 | (org-taskjuggler-close-maybe level) | |
315 | (org-taskjuggler-open-resource resource) | |
316 | (setq org-export-taskjuggler-old-level level))) | |
317 | (org-taskjuggler-close-maybe 1) | |
318 | (setq org-export-taskjuggler-old-level 0) | |
319 | (dolist (task tasks) | |
320 | (let ((level (cdr (assoc "level" task)))) | |
321 | (org-taskjuggler-close-maybe level) | |
322 | (org-taskjuggler-open-task task) | |
323 | (setq org-export-taskjuggler-old-level level))) | |
324 | (org-taskjuggler-close-maybe 1) | |
325 | (org-taskjuggler-insert-reports) | |
326 | (save-buffer) | |
327 | (or (org-export-push-to-kill-ring "TaskJuggler") | |
328 | (message "Exporting... done")) | |
329 | (current-buffer)))) | |
330 | ||
331 | ;;;###autoload | |
332 | (defun org-export-as-taskjuggler-and-open () | |
333 | "Export the current buffer as a TaskJuggler file and open it | |
334 | with the TaskJuggler GUI." | |
335 | (interactive) | |
336 | (let* ((file-name (buffer-file-name (org-export-as-taskjuggler))) | |
337 | (process-name "TaskJugglerUI") | |
338 | (command (concat process-name " " file-name))) | |
339 | (start-process-shell-command process-name nil command))) | |
340 | ||
3ab2c837 BG |
341 | (defun org-taskjuggler-targeting-tj3-p () |
342 | "Return true if we are targeting TaskJuggler III." | |
343 | (>= org-export-taskjuggler-target-version 3.0)) | |
344 | ||
86fbb8ca CD |
345 | (defun org-taskjuggler-parent-is-ordered-p () |
346 | "Return true if the parent of the current node has a property | |
347 | \"ORDERED\". Return nil otherwise." | |
348 | (save-excursion | |
349 | (and (org-up-heading-safe) (org-entry-get (point) "ORDERED")))) | |
350 | ||
351 | (defun org-taskjuggler-components () | |
352 | "Return an alist containing all the pertinent information for | |
353 | the current node such as the headline, the level, todo state | |
354 | information, all the properties, etc." | |
355 | (let* ((props (org-entry-properties)) | |
356 | (components (org-heading-components)) | |
357 | (level (nth 1 components)) | |
14e1337f BG |
358 | (headline |
359 | (replace-regexp-in-string | |
3ab2c837 | 360 | "\"" "\\\"" (nth 4 components) t t)) ; quote double quotes in headlines |
86fbb8ca CD |
361 | (parent-ordered (org-taskjuggler-parent-is-ordered-p))) |
362 | (push (cons "level" level) props) | |
363 | (push (cons "headline" headline) props) | |
364 | (push (cons "parent-ordered" parent-ordered) props))) | |
365 | ||
366 | (defun org-taskjuggler-assign-task-ids (tasks) | |
367 | "Given a list of tasks return the same list assigning a unique id | |
368 | and the full path to each task. Taskjuggler takes hierarchical ids. | |
369 | For that reason we have to make ids locally unique and we have to keep | |
370 | a path to the current task." | |
371 | (let ((previous-level 0) | |
372 | unique-ids unique-id | |
373 | path | |
374 | task resolved-tasks tmp) | |
375 | (dolist (task tasks resolved-tasks) | |
376 | (let ((level (cdr (assoc "level" task)))) | |
377 | (cond | |
3ab2c837 | 378 | ((< previous-level level) |
86fbb8ca CD |
379 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) |
380 | (dotimes (tmp (- level previous-level)) | |
381 | (push (list unique-id) unique-ids) | |
382 | (push unique-id path))) | |
3ab2c837 | 383 | ((= previous-level level) |
86fbb8ca CD |
384 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) |
385 | (push unique-id (car unique-ids)) | |
386 | (setcar path unique-id)) | |
3ab2c837 | 387 | ((> previous-level level) |
86fbb8ca CD |
388 | (dotimes (tmp (- previous-level level)) |
389 | (pop unique-ids) | |
390 | (pop path)) | |
391 | (setq unique-id (org-taskjuggler-get-unique-id task (car unique-ids))) | |
392 | (push unique-id (car unique-ids)) | |
393 | (setcar path unique-id))) | |
394 | (push (cons "unique-id" unique-id) task) | |
395 | (push (cons "path" (mapconcat 'identity (reverse path) ".")) task) | |
396 | (setq previous-level level) | |
397 | (setq resolved-tasks (append resolved-tasks (list task))))))) | |
398 | ||
3ab2c837 BG |
399 | (defun org-taskjuggler-compute-task-leafiness (tasks) |
400 | "Figure out if each task is a leaf by looking at it's level, | |
401 | and the level of its successor. If the successor is higher (ie | |
402 | deeper), then it's not a leaf." | |
403 | (let (new-list) | |
404 | (while (car tasks) | |
405 | (let ((task (car tasks)) | |
406 | (successor (car (cdr tasks)))) | |
407 | (cond | |
408 | ;; if a task has no successors it is a leaf | |
14e1337f | 409 | ((null successor) |
3ab2c837 BG |
410 | (push (cons (cons "leaf-node" t) task) new-list)) |
411 | ;; if the successor has a lower level than task it is a leaf | |
14e1337f | 412 | ((<= (cdr (assoc "level" successor)) (cdr (assoc "level" task))) |
3ab2c837 BG |
413 | (push (cons (cons "leaf-node" t) task) new-list)) |
414 | ;; otherwise examine the rest of the tasks | |
415 | (t (push task new-list)))) | |
416 | (setq tasks (cdr tasks))) | |
417 | (nreverse new-list))) | |
418 | ||
419 | (defun org-taskjuggler-assign-resource-ids (resources) | |
86fbb8ca CD |
420 | "Given a list of resources return the same list, assigning a |
421 | unique id to each resource." | |
3ab2c837 BG |
422 | (let (unique-ids new-list) |
423 | (dolist (resource resources new-list) | |
424 | (let ((unique-id (org-taskjuggler-get-unique-id resource unique-ids))) | |
425 | (push (cons "unique-id" unique-id) resource) | |
426 | (push unique-id unique-ids) | |
427 | (push resource new-list))) | |
428 | (nreverse new-list))) | |
86fbb8ca CD |
429 | |
430 | (defun org-taskjuggler-resolve-dependencies (tasks) | |
431 | (let ((previous-level 0) | |
432 | siblings | |
433 | task resolved-tasks) | |
434 | (dolist (task tasks resolved-tasks) | |
435 | (let* ((level (cdr (assoc "level" task))) | |
436 | (depends (cdr (assoc "depends" task))) | |
437 | (parent-ordered (cdr (assoc "parent-ordered" task))) | |
438 | (blocker (cdr (assoc "BLOCKER" task))) | |
3ab2c837 | 439 | (blocked-on-previous |
86fbb8ca CD |
440 | (and blocker (string-match "previous-sibling" blocker))) |
441 | (dependencies | |
442 | (org-taskjuggler-resolve-explicit-dependencies | |
3ab2c837 | 443 | (append |
86fbb8ca | 444 | (and depends (org-taskjuggler-tokenize-dependencies depends)) |
3ab2c837 | 445 | (and blocker (org-taskjuggler-tokenize-dependencies blocker))) |
86fbb8ca CD |
446 | tasks)) |
447 | previous-sibling) | |
448 | ; update previous sibling info | |
449 | (cond | |
3ab2c837 | 450 | ((< previous-level level) |
86fbb8ca CD |
451 | (dotimes (tmp (- level previous-level)) |
452 | (push task siblings))) | |
453 | ((= previous-level level) | |
454 | (setq previous-sibling (car siblings)) | |
455 | (setcar siblings task)) | |
3ab2c837 | 456 | ((> previous-level level) |
86fbb8ca CD |
457 | (dotimes (tmp (- previous-level level)) |
458 | (pop siblings)) | |
459 | (setq previous-sibling (car siblings)) | |
460 | (setcar siblings task))) | |
461 | ; insert a dependency on previous sibling if the parent is | |
462 | ; ordered or if the tasks has a BLOCKER attribute with value "previous-sibling" | |
463 | (when (or (and previous-sibling parent-ordered) blocked-on-previous) | |
464 | (push (format "!%s" (cdr (assoc "unique-id" previous-sibling))) dependencies)) | |
465 | ; store dependency information | |
3ab2c837 | 466 | (when dependencies |
86fbb8ca CD |
467 | (push (cons "depends" (mapconcat 'identity dependencies ", ")) task)) |
468 | (setq previous-level level) | |
469 | (setq resolved-tasks (append resolved-tasks (list task))))))) | |
470 | ||
471 | (defun org-taskjuggler-tokenize-dependencies (dependencies) | |
472 | "Split a dependency property value DEPENDENCIES into the | |
473 | individual dependencies and return them as a list while keeping | |
474 | the optional arguments (such as gapduration) for the | |
475 | dependencies. A dependency will have to match `[-a-zA-Z0-9_]+'." | |
3ab2c837 | 476 | (cond |
86fbb8ca CD |
477 | ((string-match "^ *$" dependencies) nil) |
478 | ((string-match "^[ \t]*\\([-a-zA-Z0-9_]+\\([ \t]*{[^}]+}\\)?\\)[ \t,]*" dependencies) | |
3ab2c837 | 479 | (cons |
86fbb8ca CD |
480 | (substring dependencies (match-beginning 1) (match-end 1)) |
481 | (org-taskjuggler-tokenize-dependencies (substring dependencies (match-end 0))))) | |
482 | (t (error (format "invalid dependency id %s" dependencies))))) | |
483 | ||
484 | (defun org-taskjuggler-resolve-explicit-dependencies (dependencies tasks) | |
485 | "For each dependency in DEPENDENCIES try to find a | |
486 | corresponding task with a matching property \"task_id\" in TASKS. | |
487 | Return a list containing the resolved links for all DEPENDENCIES | |
488 | where a matching tasks was found. If the dependency is | |
489 | \"previous-sibling\" it is ignored (as this is dealt with in | |
490 | `org-taskjuggler-resolve-dependencies'). If there is no matching | |
491 | task the dependency is ignored and a warning is displayed ." | |
492 | (unless (null dependencies) | |
3ab2c837 | 493 | (let* |
86fbb8ca CD |
494 | ;; the dependency might have optional attributes such as "{ |
495 | ;; gapduration 5d }", so only use the first string as id for the | |
496 | ;; dependency | |
497 | ((dependency (car dependencies)) | |
498 | (id (car (split-string dependency))) | |
3ab2c837 | 499 | (optional-attributes |
86fbb8ca CD |
500 | (mapconcat 'identity (cdr (split-string dependency)) " ")) |
501 | (path (org-taskjuggler-find-task-with-id id tasks))) | |
3ab2c837 | 502 | (cond |
86fbb8ca CD |
503 | ;; ignore previous sibling dependencies |
504 | ((equal (car dependencies) "previous-sibling") | |
505 | (org-taskjuggler-resolve-explicit-dependencies (cdr dependencies) tasks)) | |
506 | ;; if the id is found in another task use its path | |
3ab2c837 | 507 | ((not (null path)) |
86fbb8ca | 508 | (cons (mapconcat 'identity (list path optional-attributes) " ") |
3ab2c837 | 509 | (org-taskjuggler-resolve-explicit-dependencies |
86fbb8ca CD |
510 | (cdr dependencies) tasks))) |
511 | ;; warn about dangling dependency but otherwise ignore it | |
3ab2c837 BG |
512 | (t (display-warning |
513 | 'org-export-taskjuggler | |
86fbb8ca CD |
514 | (format "No task with matching property \"task_id\" found for id %s" id)) |
515 | (org-taskjuggler-resolve-explicit-dependencies (cdr dependencies) tasks)))))) | |
516 | ||
517 | (defun org-taskjuggler-find-task-with-id (id tasks) | |
518 | "Find ID in tasks. If found return the path of task. Otherwise | |
519 | return nil." | |
520 | (let ((task-id (cdr (assoc "task_id" (car tasks)))) | |
521 | (path (cdr (assoc "path" (car tasks))))) | |
3ab2c837 | 522 | (cond |
86fbb8ca CD |
523 | ((null tasks) nil) |
524 | ((equal task-id id) path) | |
525 | (t (org-taskjuggler-find-task-with-id id (cdr tasks)))))) | |
526 | ||
527 | (defun org-taskjuggler-get-unique-id (item unique-ids) | |
528 | "Return a unique id for an ITEM which can be a task or a resource. | |
529 | The id is derived from the headline and made unique against | |
530 | UNIQUE-IDS. If the (downcased) first token of the headline is not | |
531 | unique try to add more (downcased) tokens of the headline or | |
532 | finally add more underscore characters (\"_\")." | |
533 | (let* ((headline (cdr (assoc "headline" item))) | |
534 | (parts (split-string headline)) | |
535 | (id (org-taskjuggler-clean-id (downcase (pop parts))))) | |
536 | ; try to add more parts of the headline to make it unique | |
afe98dfa | 537 | (while (and (member id unique-ids) (car parts)) |
86fbb8ca CD |
538 | (setq id (concat id "_" (org-taskjuggler-clean-id (downcase (pop parts)))))) |
539 | ; if its still not unique add "_" | |
540 | (while (member id unique-ids) | |
541 | (setq id (concat id "_"))) | |
542 | id)) | |
3ab2c837 | 543 | |
86fbb8ca CD |
544 | (defun org-taskjuggler-clean-id (id) |
545 | "Clean and return ID to make it acceptable for taskjuggler." | |
3ab2c837 BG |
546 | (and id |
547 | ;; replace non-ascii by _ | |
548 | (replace-regexp-in-string | |
549 | "[^a-zA-Z0-9_]" "_" | |
550 | ;; make sure id doesn't start with a number | |
551 | (replace-regexp-in-string "^\\([0-9]\\)" "_\\1" id)))) | |
86fbb8ca CD |
552 | |
553 | (defun org-taskjuggler-open-project (project) | |
554 | "Insert the beginning of a project declaration. All valid | |
555 | attributes from the PROJECT alist are inserted. If no end date is | |
556 | specified it is calculated | |
557 | `org-export-taskjuggler-default-project-duration' days from now." | |
558 | (let* ((unique-id (cdr (assoc "unique-id" project))) | |
3ab2c837 BG |
559 | (headline (cdr (assoc "headline" project))) |
560 | (version (cdr (assoc "version" project))) | |
561 | (start (cdr (assoc "start" project))) | |
562 | (end (cdr (assoc "end" project)))) | |
563 | (insert | |
86fbb8ca CD |
564 | (format "project %s \"%s\" \"%s\" %s +%sd {\n }\n" |
565 | unique-id headline version start | |
566 | org-export-taskjuggler-default-project-duration)))) | |
567 | ||
568 | (defun org-taskjuggler-filter-and-join (items) | |
569 | "Filter all nil elements from ITEMS and join the remaining ones | |
570 | with separator \"\n\"." | |
571 | (let ((filtered-items (remq nil items))) | |
572 | (and filtered-items (mapconcat 'identity filtered-items "\n")))) | |
3ab2c837 | 573 | |
86fbb8ca | 574 | (defun org-taskjuggler-get-attributes (item attributes) |
e66ba1df | 575 | "Return all attribute as a single formated string. ITEM is an |
86fbb8ca CD |
576 | alist representing either a resource or a task. ATTRIBUTES is a |
577 | list of symbols. Only entries from ITEM are considered that are | |
578 | listed in ATTRIBUTES." | |
3ab2c837 | 579 | (org-taskjuggler-filter-and-join |
86fbb8ca | 580 | (mapcar |
3ab2c837 BG |
581 | (lambda (attribute) |
582 | (org-taskjuggler-filter-and-join | |
86fbb8ca CD |
583 | (org-taskjuggler-get-attribute item attribute))) |
584 | attributes))) | |
585 | ||
586 | (defun org-taskjuggler-get-attribute (item attribute) | |
587 | "Return a list of strings containing the properly formatted | |
588 | taskjuggler declaration for a given ATTRIBUTE in ITEM (an alist). | |
589 | If the ATTRIBUTE is not in ITEM return nil." | |
3ab2c837 | 590 | (cond |
86fbb8ca CD |
591 | ((null item) nil) |
592 | ((equal (symbol-name attribute) (car (car item))) | |
593 | (cons (format "%s %s" (symbol-name attribute) (cdr (car item))) | |
594 | (org-taskjuggler-get-attribute (cdr item) attribute))) | |
595 | (t (org-taskjuggler-get-attribute (cdr item) attribute)))) | |
596 | ||
597 | (defun org-taskjuggler-open-resource (resource) | |
598 | "Insert the beginning of a resource declaration. All valid | |
599 | attributes from the RESOURCE alist are inserted. If the RESOURCE | |
600 | defines a property \"resource_id\" it will be used as the id for | |
601 | this resource. Otherwise it will use the ID property. If neither | |
602 | is defined it will calculate a unique id for the resource using | |
603 | `org-taskjuggler-get-unique-id'." | |
3ab2c837 BG |
604 | (let ((id (org-taskjuggler-clean-id |
605 | (or (cdr (assoc "resource_id" resource)) | |
606 | (cdr (assoc "ID" resource)) | |
86fbb8ca CD |
607 | (cdr (assoc "unique-id" resource))))) |
608 | (headline (cdr (assoc "headline" resource))) | |
609 | (attributes '(limits vacation shift booking efficiency journalentry rate))) | |
3ab2c837 BG |
610 | (insert |
611 | (concat | |
86fbb8ca CD |
612 | "resource " id " \"" headline "\" {\n " |
613 | (org-taskjuggler-get-attributes resource attributes) "\n")))) | |
614 | ||
615 | (defun org-taskjuggler-clean-effort (effort) | |
616 | "Translate effort strings into a format acceptable to taskjuggler, | |
3ab2c837 BG |
617 | i.e. REAL UNIT. A valid effort string can be anything that is |
618 | accepted by `org-duration-string-to-minutesĀ“." | |
619 | (cond | |
86fbb8ca | 620 | ((null effort) effort) |
3ab2c837 BG |
621 | (t (let* ((minutes (org-duration-string-to-minutes effort)) |
622 | (hours (/ minutes 60.0))) | |
623 | (format "%.1fh" hours))))) | |
86fbb8ca CD |
624 | |
625 | (defun org-taskjuggler-get-priority (priority) | |
626 | "Return a priority between 1 and 1000 based on PRIORITY, an | |
627 | org-mode priority string." | |
3ab2c837 | 628 | (max 1 (/ (* 1000 (- org-lowest-priority (string-to-char priority))) |
86fbb8ca CD |
629 | (- org-lowest-priority org-highest-priority)))) |
630 | ||
631 | (defun org-taskjuggler-open-task (task) | |
632 | (let* ((unique-id (cdr (assoc "unique-id" task))) | |
3ab2c837 BG |
633 | (headline (cdr (assoc "headline" task))) |
634 | (effort (org-taskjuggler-clean-effort (cdr (assoc org-effort-property task)))) | |
635 | (depends (cdr (assoc "depends" task))) | |
636 | (allocate (cdr (assoc "allocate" task))) | |
637 | (priority-raw (cdr (assoc "PRIORITY" task))) | |
638 | (priority (and priority-raw (org-taskjuggler-get-priority priority-raw))) | |
639 | (state (cdr (assoc "TODO" task))) | |
640 | (complete (or (and (member state org-done-keywords) "100") | |
641 | (cdr (assoc "complete" task)))) | |
642 | (parent-ordered (cdr (assoc "parent-ordered" task))) | |
643 | (previous-sibling (cdr (assoc "previous-sibling" task))) | |
644 | (milestone (or (cdr (assoc "milestone" task)) | |
645 | (and (assoc "leaf-node" task) | |
646 | (not (or effort | |
647 | (cdr (assoc "duration" task)) | |
648 | (cdr (assoc "end" task)) | |
649 | (cdr (assoc "period" task))))))) | |
650 | (attributes | |
651 | '(account start note duration endbuffer endcredit end | |
652 | flags journalentry length maxend maxstart minend | |
653 | minstart period reference responsible scheduling | |
654 | startbuffer startcredit statusnote))) | |
86fbb8ca | 655 | (insert |
3ab2c837 BG |
656 | (concat |
657 | "task " unique-id " \"" headline "\" {\n" | |
86fbb8ca CD |
658 | (if (and parent-ordered previous-sibling) |
659 | (format " depends %s\n" previous-sibling) | |
660 | (and depends (format " depends %s\n" depends))) | |
3ab2c837 BG |
661 | (and allocate (format " purge %s\n allocate %s\n" |
662 | (or (and (org-taskjuggler-targeting-tj3-p) "allocate") | |
663 | "allocations") | |
664 | allocate)) | |
86fbb8ca CD |
665 | (and complete (format " complete %s\n" complete)) |
666 | (and effort (format " effort %s\n" effort)) | |
667 | (and priority (format " priority %s\n" priority)) | |
3ab2c837 BG |
668 | (and milestone (format " milestone\n")) |
669 | ||
86fbb8ca CD |
670 | (org-taskjuggler-get-attributes task attributes) |
671 | "\n")))) | |
672 | ||
673 | (defun org-taskjuggler-close-maybe (level) | |
3ab2c837 | 674 | (while (> org-export-taskjuggler-old-level level) |
86fbb8ca CD |
675 | (insert "}\n") |
676 | (setq org-export-taskjuggler-old-level (1- org-export-taskjuggler-old-level))) | |
677 | (when (= org-export-taskjuggler-old-level level) | |
678 | (insert "}\n"))) | |
679 | ||
680 | (defun org-taskjuggler-insert-reports () | |
681 | (let (report) | |
682 | (dolist (report org-export-taskjuggler-default-reports) | |
683 | (insert report "\n")))) | |
684 | ||
685 | (provide 'org-taskjuggler) | |
686 | ||
687 | ;;; org-taskjuggler.el ends here |