| 1 | ;;; org-taskjuggler.el --- TaskJuggler exporter for org-mode |
| 2 | ;; |
| 3 | ;; Copyright (C) 2007-2011 Free Software Foundation, Inc. |
| 4 | ;; |
| 5 | ;; Emacs Lisp Archive Entry |
| 6 | ;; Filename: org-taskjuggler.el |
| 7 | ;; Version: 7.7 |
| 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' |
| 73 | ;; which will export the project plan and open a Gantt chart in |
| 74 | ;; TaskJugglerUI. |
| 75 | ;; |
| 76 | ;; * Resources |
| 77 | ;; |
| 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 |
| 110 | ;; |
| 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: |
| 129 | ;; :Effort: 2d |
| 130 | ;; :END: |
| 131 | ;; ** Workflow Guidelines |
| 132 | ;; :PROPERTIES: |
| 133 | ;; :Effort: 2d |
| 134 | ;; :END: |
| 135 | ;; * Presentation |
| 136 | ;; :PROPERTIES: |
| 137 | ;; :Effort: 2d |
| 138 | ;; :BLOCKER: training_material { gapduration 1d } some_other_task |
| 139 | ;; :END: |
| 140 | ;; |
| 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 | |
| 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 | |
| 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 | |
| 201 | (defcustom org-export-taskjuggler-default-reports |
| 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 | |
| 220 | (defcustom org-export-taskjuggler-default-global-properties |
| 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 |
| 229 | include another TaskJuggler file. |
| 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 |
| 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))))) |
| 270 | (resources |
| 271 | (org-taskjuggler-assign-resource-ids |
| 272 | (org-map-entries |
| 273 | 'org-taskjuggler-components |
| 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 |
| 287 | (setq resources |
| 288 | `((("resource_id" . ,(user-login-name)) |
| 289 | ("headline" . ,user-full-name) |
| 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 | |
| 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 | |
| 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)) |
| 357 | (headline |
| 358 | (replace-regexp-in-string |
| 359 | "\"" "\\\"" (nth 4 components) t t)) ; quote double quotes in headlines |
| 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 |
| 377 | ((< previous-level level) |
| 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))) |
| 382 | ((= previous-level level) |
| 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)) |
| 386 | ((> previous-level level) |
| 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 | |
| 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 |
| 408 | ((null successor) |
| 409 | (push (cons (cons "leaf-node" t) task) new-list)) |
| 410 | ;; if the successor has a lower level than task it is a leaf |
| 411 | ((<= (cdr (assoc "level" successor)) (cdr (assoc "level" task))) |
| 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) |
| 419 | "Given a list of resources return the same list, assigning a |
| 420 | unique id to each resource." |
| 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))) |
| 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))) |
| 438 | (blocked-on-previous |
| 439 | (and blocker (string-match "previous-sibling" blocker))) |
| 440 | (dependencies |
| 441 | (org-taskjuggler-resolve-explicit-dependencies |
| 442 | (append |
| 443 | (and depends (org-taskjuggler-tokenize-dependencies depends)) |
| 444 | (and blocker (org-taskjuggler-tokenize-dependencies blocker))) |
| 445 | tasks)) |
| 446 | previous-sibling) |
| 447 | ; update previous sibling info |
| 448 | (cond |
| 449 | ((< previous-level level) |
| 450 | (dotimes (tmp (- level previous-level)) |
| 451 | (push task siblings))) |
| 452 | ((= previous-level level) |
| 453 | (setq previous-sibling (car siblings)) |
| 454 | (setcar siblings task)) |
| 455 | ((> previous-level level) |
| 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 |
| 465 | (when dependencies |
| 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_]+'." |
| 475 | (cond |
| 476 | ((string-match "^ *$" dependencies) nil) |
| 477 | ((string-match "^[ \t]*\\([-a-zA-Z0-9_]+\\([ \t]*{[^}]+}\\)?\\)[ \t,]*" dependencies) |
| 478 | (cons |
| 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) |
| 492 | (let* |
| 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))) |
| 498 | (optional-attributes |
| 499 | (mapconcat 'identity (cdr (split-string dependency)) " ")) |
| 500 | (path (org-taskjuggler-find-task-with-id id tasks))) |
| 501 | (cond |
| 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 |
| 506 | ((not (null path)) |
| 507 | (cons (mapconcat 'identity (list path optional-attributes) " ") |
| 508 | (org-taskjuggler-resolve-explicit-dependencies |
| 509 | (cdr dependencies) tasks))) |
| 510 | ;; warn about dangling dependency but otherwise ignore it |
| 511 | (t (display-warning |
| 512 | 'org-export-taskjuggler |
| 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))))) |
| 521 | (cond |
| 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 |
| 536 | (while (and (member id unique-ids) (car parts)) |
| 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)) |
| 542 | |
| 543 | (defun org-taskjuggler-clean-id (id) |
| 544 | "Clean and return ID to make it acceptable for taskjuggler." |
| 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)))) |
| 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))) |
| 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 |
| 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")))) |
| 572 | |
| 573 | (defun org-taskjuggler-get-attributes (item attributes) |
| 574 | "Return all attributes as a single formatted string. ITEM is an |
| 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." |
| 578 | (org-taskjuggler-filter-and-join |
| 579 | (mapcar |
| 580 | (lambda (attribute) |
| 581 | (org-taskjuggler-filter-and-join |
| 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." |
| 589 | (cond |
| 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'." |
| 603 | (let ((id (org-taskjuggler-clean-id |
| 604 | (or (cdr (assoc "resource_id" resource)) |
| 605 | (cdr (assoc "ID" resource)) |
| 606 | (cdr (assoc "unique-id" resource))))) |
| 607 | (headline (cdr (assoc "headline" resource))) |
| 608 | (attributes '(limits vacation shift booking efficiency journalentry rate))) |
| 609 | (insert |
| 610 | (concat |
| 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, |
| 616 | i.e. REAL UNIT. A valid effort string can be anything that is |
| 617 | accepted by `org-duration-string-to-minutesĀ“." |
| 618 | (cond |
| 619 | ((null effort) effort) |
| 620 | (t (let* ((minutes (org-duration-string-to-minutes effort)) |
| 621 | (hours (/ minutes 60.0))) |
| 622 | (format "%.1fh" hours))))) |
| 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." |
| 627 | (max 1 (/ (* 1000 (- org-lowest-priority (string-to-char priority))) |
| 628 | (- org-lowest-priority org-highest-priority)))) |
| 629 | |
| 630 | (defun org-taskjuggler-open-task (task) |
| 631 | (let* ((unique-id (cdr (assoc "unique-id" task))) |
| 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))) |
| 654 | (insert |
| 655 | (concat |
| 656 | "task " unique-id " \"" headline "\" {\n" |
| 657 | (if (and parent-ordered previous-sibling) |
| 658 | (format " depends %s\n" previous-sibling) |
| 659 | (and depends (format " depends %s\n" depends))) |
| 660 | (and allocate (format " purge %s\n allocate %s\n" |
| 661 | (or (and (org-taskjuggler-targeting-tj3-p) "allocate") |
| 662 | "allocations") |
| 663 | allocate)) |
| 664 | (and complete (format " complete %s\n" complete)) |
| 665 | (and effort (format " effort %s\n" effort)) |
| 666 | (and priority (format " priority %s\n" priority)) |
| 667 | (and milestone (format " milestone\n")) |
| 668 | |
| 669 | (org-taskjuggler-get-attributes task attributes) |
| 670 | "\n")))) |
| 671 | |
| 672 | (defun org-taskjuggler-close-maybe (level) |
| 673 | (while (> org-export-taskjuggler-old-level level) |
| 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 |