diff options
author | Nicolas Goaziou <n.goaziou@gmail.com> | 2013-02-18 00:22:22 +0100 |
---|---|---|
committer | Nicolas Goaziou <n.goaziou@gmail.com> | 2013-02-18 00:26:39 +0100 |
commit | af9191d18b3c8224fc48661bc5831b3b06944f4c (patch) | |
tree | 6ca3400621abd3098dff700c04255180218df6e8 | |
parent | 324a2cb3f99f7fd76a5dd4ab41a9db440d2908b2 (diff) | |
download | org-mode-af9191d18b3c8224fc48661bc5831b3b06944f4c.tar.gz |
ox-taskjuggler: Port TaskJuggler back-end to new export framework
* contrib/lisp/ox-taskjuggler.el: New file.
* lisp/org.el (org-export-backends): Add new back-end in customize
interface.
-rw-r--r-- | contrib/lisp/ox-taskjuggler.el | 855 | ||||
-rw-r--r-- | lisp/org.el | 3 |
2 files changed, 857 insertions, 1 deletions
diff --git a/contrib/lisp/ox-taskjuggler.el b/contrib/lisp/ox-taskjuggler.el new file mode 100644 index 0000000..46402ba --- /dev/null +++ b/contrib/lisp/ox-taskjuggler.el @@ -0,0 +1,855 @@ +;;; ox-taskjuggler.el --- TaskJuggler Back-End for Org Export Engine +;; +;; Copyright (C) 2007-2013 Free Software Foundation, Inc. +;; +;; Emacs Lisp Archive Entry +;; Filename: ox-taskjuggler.el +;; Author: Christian Egli +;; Nicolas Goaziou <n dot goaziou at gmail dot com> +;; Maintainer: Christian Egli +;; Keywords: org, taskjuggler, project planning +;; Description: Converts an Org mode buffer into a TaskJuggler project plan + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;; Commentary: +;; +;; This library implements a TaskJuggler exporter for Org mode. +;; TaskJuggler uses a text format to define projects, tasks and +;; resources, so it is a natural fit for Org mode. It can produce all +;; sorts of reports for tasks or resources in either HTML, CSV or PDF. +;; The current version of TaskJuggler requires KDE but the next +;; version is implemented in Ruby and should therefore run on any +;; platform. +;; +;; The exporter does not export all the nodes of a document or +;; strictly follow the order of the nodes in the document. +;; +;; Instead the TaskJuggler exporter looks for a tree that defines the +;; tasks and a optionally tree that defines the resources for this +;; project. It then creates a TaskJuggler file based on these trees +;; and the attributes defined in all the nodes. +;; +;; * Installation +;; +;; Put this file into your load-path and the following line into your +;; ~/.emacs: +;; +;; (add-to-list 'org-export-backends 'taskjuggler) +;; +;; or customize `org-export-backends' variable. +;; +;; The interactive functions are the following: +;; +;; M-x `org-taskjuggler-export' +;; M-x `org-taskjuggler-export-and-open' +;; +;; * Tasks +;; +;; Let's illustrate the usage with a small example. Create your tasks +;; as you usually do with org-mode. Assign efforts to each task using +;; properties (it's easiest to do this in the column view). You +;; should end up with something similar to the example by Peter Jones +;; in: +;; +;; http://www.contextualdevelopment.com/static/artifacts/articles/2008/project-planning/project-planning.org. +;; +;; Now mark the top node of your tasks with a tag named +;; "taskjuggler_project" (or whatever you customized +;; `org-taskjuggler-project-tag' to). You are now ready to export the +;; project plan with `org-taskjuggler-export-and-open' which will +;; export the project plan and open a Gantt chart in TaskJugglerUI. +;; +;; * Resources +;; +;; Next you can define resources and assign those to work on specific +;; tasks. You can group your resources hierarchically. Tag the top +;; node of the resources with "taskjuggler_resource" (or whatever you +;; customized `org-taskjuggler-resource-tag' to). You can optionally +;; assign an identifier (named "resource_id") to the resources (using +;; the standard org properties commands) or you can let the exporter +;; generate identifiers automatically (the exporter picks the first +;; word of the headline as the identifier as long as it is unique, see +;; the documentation of `org-taskjuggler--build-unique-id'). Using that +;; identifier you can then allocate resources to tasks. This is again +;; done with the "allocate" property on the tasks. Do this in column +;; view or when on the task type +;; +;; C-c C-x p allocate RET <resource_id> RET +;; +;; Once the allocations are done you can again export to TaskJuggler +;; and check in the Resource Allocation Graph which person is working +;; on what task at what time. +;; +;; * Export of properties +;; +;; The exporter also takes TODO state information into consideration, +;; i.e. if a task is marked as done it will have the corresponding +;; attribute in TaskJuggler ("complete 100"). Also it will export any +;; property on a task resource or resource node which is known to +;; TaskJuggler, such as limits, vacation, shift, booking, efficiency, +;; journalentry, rate for resources or account, start, note, duration, +;; end, journalentry, milestone, reference, responsible, scheduling, +;; etc for tasks. +;; +;; * Dependencies +;; +;; The exporter will handle dependencies that are defined in the tasks +;; either with the ORDERED attribute (see TODO dependencies in the Org +;; mode manual) or with the BLOCKER attribute (see org-depend.el) or +;; alternatively with a depends attribute. Both the BLOCKER and the +;; depends attribute can be either "previous-sibling" or a reference +;; to an identifier (named "task_id") which is defined for another +;; task in the project. BLOCKER and the depends attribute can define +;; multiple dependencies separated by either space or comma. You can +;; also specify optional attributes on the dependency by simply +;; appending it. The following examples should illustrate this: +;; +;; * Training material +;; :PROPERTIES: +;; :task_id: training_material +;; :ORDERED: t +;; :END: +;; ** Markup Guidelines +;; :PROPERTIES: +;; :Effort: 2d +;; :END: +;; ** Workflow Guidelines +;; :PROPERTIES: +;; :Effort: 2d +;; :END: +;; * Presentation +;; :PROPERTIES: +;; :Effort: 2d +;; :BLOCKER: training_material { gapduration 1d } some_other_task +;; :END: +;; +;;;; * TODO +;; - Look at org-file-properties, org-global-properties and +;; org-global-properties-fixed +;; - What about property inheritance and org-property-inherit-p? +;; - Use TYPE_TODO as an way to assign resources +;; +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'ox) + + + +;;; User Variables + +(defgroup org-export-taskjuggler nil + "Options specific for TaskJuggler export back-end." + :tag "Org Export TaskJuggler" + :group 'org-export) + +(defcustom org-taskjuggler-extension ".tjp" + "Extension of TaskJuggler files." + :group 'org-export-taskjuggler + :version "24.2" + :type 'string) + +(defcustom org-taskjuggler-project-tag "taskjuggler_project" + "Tag marking project's tasks. +This tag is used to find the tree containing all the tasks for +the project." + :group 'org-export-taskjuggler + :version "24.2" + :type 'string) + +(defcustom org-taskjuggler-resource-tag "taskjuggler_resource" + "Tag marking project's resources. +This tag is used to find the tree containing all the resources +for the project." + :group 'org-export-taskjuggler + :version "24.2" + :type 'string) + +(defcustom org-taskjuggler-report-tag "taskjuggler_report" + "Tag marking project's reports. +This tag is used to find the tree containing all the reports for +the project." + :group 'org-export-taskjuggler + :version "24.2" + :type 'string) + +(defcustom org-taskjuggler-target-version 2.4 + "Which version of TaskJuggler the exporter is targeting." + :group 'org-export-taskjuggler + :version "24.2" + :type 'number) + +(defcustom org-taskjuggler-default-project-version "1.0" + "Default version string for the project. +This value can also be set with the \":VERSION:\" property +associated to the headline defining the project." + :group 'org-export-taskjuggler + :version "24.2" + :type 'string) + +(defcustom org-taskjuggler-default-project-duration 280 + "Default project duration. +The value will be used if no start and end date have been defined +in the root node of the task tree, i.e. the tree that has been +marked with `org-taskjuggler-project-tag'" + :group 'org-export-taskjuggler + :version "24.2" + :type 'integer) + +(defcustom org-taskjuggler-default-reports + '("taskreport \"Gantt Chart\" { + headline \"Project Gantt Chart\" + columns hierarchindex, name, start, end, effort, duration, completed, chart + timeformat \"%Y-%m-%d\" + hideresource 1 + loadunit shortauto +}" + "resourcereport \"Resource Graph\" { + headline \"Resource Allocation Graph\" + columns no, name, utilization, freeload, chart + loadunit shortauto + sorttasks startup + hidetask ~isleaf() +}") + "Default reports for the project." + :group 'org-export-taskjuggler + :version "24.2" + :type '(repeat (string :tag "Report"))) + +(defcustom org-taskjuggler-default-global-header "" + "Default global header for the project. +This goes before project declaration, and might be useful for +early macros." + :group 'org-export-taskjuggler + :version "24.2" + :type '(string :tag "Preamble")) + +(defcustom org-taskjuggler-default-global-properties + "shift s40 \"Part time shift\" { + workinghours wed, thu, fri off +} +" + "Default global properties for the project. + +Here you typically define global properties such as shifts, +accounts, rates, vacation, macros and flags. Any property that +is allowed within the TaskJuggler file can be inserted. You +could for example include another TaskJuggler file. + +The global properties are inserted after the project declaration +but before any resource and task declarations." + :group 'org-export-taskjuggler + :version "24.2" + :type '(string :tag "Preamble")) + +(defcustom org-taskjuggler-valid-task-attributes + '(account start note duration endbuffer endcredit end + flags journalentry length limits maxend maxstart minend + minstart period reference responsible scheduling + startbuffer startcredit statusnote chargeset charge) + "Valid attributes for Taskjuggler tasks. +If one of these appears as a property for a headline, it will be +exported with the corresponding task." + :group 'org-export-taskjuggler) + +(defcustom org-taskjuggler-valid-resource-attributes + '(limits vacation shift booking efficiency journalentry rate + workinghours flags) + "Valid attributes for Taskjuggler resources. +If one of these appears as a property for a headline, it will be +exported with the corresponding resource." + :group 'org-export-taskjuggler) + +(defcustom org-taskjuggler-valid-report-attributes + '(headline columns definitions timeformat hideresource hidetask + loadunit sorttasks formats period) + "Valid attributes for Taskjuggler reports. +If one of these appears as a property for a headline, it will be +exported with the corresponding report." + :group 'org-export-taskjuggler) + +(defcustom org-taskjuggler-keep-project-as-task t + "Non-nil keeps the project headline as an umbrella task for all tasks. +Setting this to nil will allow maintaining completely separated +task buckets, while still sharing the same resources pool." + :group 'org-export-taskjuggler + :type 'boolean) + + + +;;; Hooks + +(defvar org-taskjuggler-final-hook nil + "Hook run after a TaskJuggler files has been saved. +This hook is run with the name of the file as argument.") + + + +;;; Back-End Definition + +(org-export-define-backend taskjuggler + ((template . org-taskjuggler-project-plan)) + :menu-entry + (?J "Export to TaskJuggler" + ((?j "As TJP file" (lambda (a s v b) (org-taskjuggler-export a s v))) + (?o "As TJP file and open" + (lambda (a s v b) + (if a (org-taskjuggler-export a s v) + (org-taskjuggler-export-and-open s v)))))) + ;; This property will be used to store unique ids in communication + ;; channel. Ids will be retrieved with `org-taskjuggler-get-id'. + :options-alist ((:taskjuggler-unique-ids nil nil nil))) + + + +;;; Unique IDs + +(defun org-taskjuggler-assign-task-ids (tasks info) + "Assign a unique ID to each task in TASKS. +TASKS is a list of headlines. Return value is an alist between +headlines and their associated ID. IDs are hierarchical, which +means they only need to be unique among the task siblings." + (let* (alist + (build-id + (lambda (tasks local-ids) + (org-element-map tasks 'headline + (lambda (task) + (let ((id (org-taskjuggler--build-unique-id task local-ids))) + (push id local-ids) + (push (cons task id) alist) + (funcall build-id (org-element-contents task) nil))) + info nil 'headline)))) + (funcall build-id tasks nil) + alist)) + +(defun org-taskjuggler-assign-resource-ids (resources info) + "Assign a unique ID to each resource within RESOURCES. +RESOURCES is a list of headlines. Return value is an alist +between headlines and their associated ID." + (let (ids) + (org-element-map resources 'headline + (lambda (resource) + (let ((id (org-taskjuggler--build-unique-id resource ids))) + (push id ids) + (cons resource id))) + info))) + + + +;;; Accessors + +(defun org-taskjuggler-get-project (info) + "Return project in parse tree. +INFO is a plist used as a communication channel. First headline +in buffer with `org-taskjuggler-project-tag' defines the project. +If no such task is defined, pick the first headline in buffer. +If there is no headline at all, return nil." + (or (org-element-map (plist-get info :parse-tree) 'headline + (lambda (hl) + (and (member org-taskjuggler-project-tag + (org-export-get-tags hl info)) + hl)) + info t) + (org-element-map tree 'headline 'identity info t))) + +(defun org-taskjuggler-get-id (item info) + "Return id for task or resource ITEM. +ITEM is a headline. Return value is a string." + (cdr (assq item (plist-get info :taskjuggler-unique-ids)))) + +(defun org-taskjuggler-get-name (item) + "Return name for task or resource ITEM. +ITEM is a headline. Return value is a string." + ;; Quote double quotes in name. + (replace-regexp-in-string + "\"" "\\\"" (org-element-property :raw-value item) t t)) + +(defun org-taskjuggler-get-start (item) + "Return start date for task or resource ITEM. +ITEM is a headline. Return value is a string or nil if ITEM +doesn't have any start date defined.." + (let ((scheduled (org-element-property :scheduled item))) + (and scheduled (org-timestamp-format scheduled "%Y-%02m-%02d")))) + +(defun org-taskjuggler-get-end (item) + "Return end date for task or resource ITEM. +ITEM is a headline. Return value is a string or nil if ITEM +doesn't have any end date defined.." + (let ((deadline (org-element-property :deadline item))) + (and deadline (org-timestamp-format deadline "%Y-%02m-%02d")))) + + + +;;; Internal Functions + +(defun org-taskjuggler--indent-string (s) + "Indent string S by 2 spaces. +Return new string. If S is the empty string, return it." + (if (equal "" s) s (replace-regexp-in-string "^ *\\S-" " \\&" s))) + +(defun org-taskjuggler--build-attributes (item attributes) + "Return attributes string for task, resource or report ITEM. +ITEM is an headline. ATTRIBUTES is a list of symbols +representing valid attributes for ITEM." + (mapconcat + (lambda (attribute) + (let ((value (org-element-property + (intern (upcase (format ":%s" attribute))) + item))) + (and value (format "%s %s\n" attribute value)))) + (remq nil attributes) "")) + +(defun org-taskjuggler--build-unique-id (item unique-ids) + "Return a unique id for a given task or a resource. +ITEM is an `headline' type element representing the task or +resource. Its id is derived from its name and made unique +against UNIQUE-IDS. If the (downcased) first token of the +headline is not unique try to add more (downcased) tokens of the +headline or finally add more underscore characters (\"_\")." + (let* ((parts (org-split-string (org-element-property :raw-value item))) + (id (org-taskjuggler--clean-id (downcase (pop parts))))) + ;; Try to add more parts of the headline to make it unique. + (while (and (car parts) (member id unique-ids)) + (setq id (concat id "_" + (org-taskjuggler--clean-id (downcase (pop parts)))))) + ;; If it's still not unique, add "_". + (while (member id unique-ids) + (setq id (concat id "_"))) + id)) + +(defun org-taskjuggler--clean-id (id) + "Clean and return ID to make it acceptable for TaskJuggler. +ID is a string." + ;; Replace non-ascii by "_". + (replace-regexp-in-string + "[^a-zA-Z0-9_]" "_" + ;; Make sure id doesn't start with a number. + (replace-regexp-in-string "^\\([0-9]\\)" "_\\1" id))) + + + +;;; Dependencies + +(defun org-taskjuggler-resolve-dependencies (task info) + "Return a list of all tasks TASK depends on. +TASK is an headline. INFO is a plist used as a communication +channel." + (let ((deps-ids + ;; Get all dependencies specified in BLOCKER and DEPENDS task + ;; properties. Clean options from them. + (let ((deps (concat (org-element-property :BLOCKER task) + (org-element-property :DEPENDS task)))) + (and deps + (org-split-string (replace-regexp-in-string "{.*?}" "" deps) + "[ ,]* +")))) + depends) + (when deps-ids + ;; Find tasks with :task_id: property matching id in DEPS-IDS. + ;; Add them to DEPENDS. + (let* ((project (org-taskjuggler-get-project info)) + (tasks (if org-taskjuggler-keep-project-as-task project + (org-element-contents project)))) + (setq depends + (org-element-map tasks 'headline + (lambda (task) + (let ((task-id (org-element-property :TASK_ID task))) + (and task-id (member task-id deps-ids) task))) + info))) + ;; Check BLOCKER and DEPENDS properties. If "previous-sibling" + ;; belongs to DEPS-ID, add it to DEPENDS. + (when (and (member-ignore-case "previous-sibling" deps-ids) + (not (org-export-first-sibling-p task info))) + (let ((prev (org-export-get-previous-element task info))) + (and (not (memq prev depends)) (push prev depends))))) + ;; Check ORDERED status of parent. + (let ((parent (org-export-get-parent task))) + (when (and parent + (org-element-property :ORDERED parent) + (not (org-export-first-sibling-p task info))) + (push (org-export-get-previous-element task info) depends))) + ;; Return dependencies. + depends)) + +(defun org-taskjuggler-format-dependencies (dependencies task info) + "Format DEPENDENCIES to match TaskJuggler syntax. +DEPENDENCIES is list of dependencies for TASK, as returned by +`org-taskjuggler-resolve-depedencies'. TASK is an headline. +INFO is a plist used as a communication channel. Return value +doesn't include leading \"depends\"." + (let ((dep-str (concat (org-element-property :BLOCKER task) + " " + (org-element-property :DEPENDS task))) + (get-path + (lambda (dep) + ;; Return path to DEP relatively to TASK. + (let ((parent (org-export-get-parent dep)) + (exclamations 1) + (option + (let ((id (org-element-property :TASK_ID dep))) + (and id + (string-match (concat id " +\\({.*?}\\)") dep-str) + (org-match-string-no-properties 1)))) + path) + ;; Compute number of exclamation marks. + (while (not (org-element-map parent 'headline + (lambda (task) (eq task dep)))) + (incf exclamations) + (setq parent (org-export-get-parent parent))) + ;; Build path from DEP to PARENT. + (while (not (eq parent dep)) + (push (org-taskjuggler-get-id dep info) path) + (setq dep (org-export-get-parent dep))) + ;; Return full path. Add dependency options, if any. + (concat (make-string exclamations ?!) + (mapconcat 'identity path ".") + (and option (concat " " option))))))) + ;; Return dependencies string, without the leading "depends". + (mapconcat (lambda (dep) (funcall get-path dep)) dependencies ", "))) + + + +;;; Translator Functions + +(defun org-taskjuggler-project-plan (contents info) + "Build TaskJuggler project plan. +CONTENTS is ignored. INFO is a plist holding export options. +Return complete project plan as a string in TaskJuggler syntax." + (let* ((tree (plist-get info :parse-tree)) + (project (or (org-taskjuggler-get-project info) + (error "No project specified")))) + (concat + ;; 1. Insert header. + (org-element-normalize-string org-taskjuggler-default-global-header) + ;; 2. Insert project. + (org-taskjuggler--build-project project info) + ;; 3. Insert global properties. + (org-element-normalize-string org-taskjuggler-default-global-properties) + ;; 4. Insert resources. Provide a default one if none is + ;; specified. + (let ((main-resources + ;; Collect contents from various trees marked with + ;; `org-taskjuggler-resource-tag'. Only gather top level + ;; resources. + (apply 'append + (org-element-map tree 'headline + (lambda (hl) + (and (member org-taskjuggler-resource-tag + (org-export-get-tags hl info)) + (org-element-map (org-element-contents hl) 'headline + 'identity info nil 'headline))) + info nil 'headline)))) + ;; Assign a unique ID to each resource. Store it under + ;; `:taskjuggler-unique-ids' property in INFO. + (setq info + (plist-put info :taskjuggler-unique-ids + (org-taskjuggler-assign-resource-ids + main-resources info))) + (concat + (if main-resources + (mapconcat + (lambda (resource) (org-taskjuggler--build-resource resource info)) + main-resources "") + (format "resource %s \"%s\" {\n}\n" (user-login-name) user-full-name)) + ;; 5. Insert tasks. + (let ((main-tasks + ;; If `org-taskjuggler-keep-project-as-task' is + ;; non-nil, there is only one task. Otherwise, every + ;; direct children of PROJECT is a top level task. + (if org-taskjuggler-keep-project-as-task (list project) + (or (org-element-map (org-element-contents project) 'headline + 'identity info nil 'headline) + (error "No task specified"))))) + ;; Assign a unique ID to each task. Add it to + ;; `:taskjuggler-unique-ids' property in INFO. + (setq info + (plist-put info :taskjuggler-unique-ids + (append + (org-taskjuggler-assign-task-ids main-tasks info) + (plist-get info :taskjuggler-unique-ids)))) + ;; If no resource is allocated among tasks, allocate one to + ;; the first task. + (unless (org-element-map main-tasks 'headline + (lambda (task) (org-element-property :ALLOCATE task)) + info t) + (org-element-put-property + (car main-tasks) :ALLOCATE + (or (org-taskjuggler-get-id (car main-resources) info) + (user-login-name)))) + (mapconcat + (lambda (task) (org-taskjuggler--build-task task info)) + main-tasks "")) + ;; 6. Insert reports. If no report is defined, insert default + ;; reports. + (let ((main-reports + ;; Collect contents from various trees marked with + ;; `org-taskjuggler-report-tag'. Only gather top level + ;; reports. + (apply 'append + (org-element-map tree 'headline + (lambda (hl) + (and (member org-taskjuggler-report-tag + (org-export-get-tags hl info)) + (org-element-map (org-element-contents hl) + 'headline 'identity info nil 'headline))) + info nil 'headline)))) + (if main-reports + (mapconcat + (lambda (report) (org-taskjuggler--build-report report info)) + main-reports "") + (mapconcat 'org-element-normalize-string + org-taskjuggler-default-reports "")))))))) + +(defun org-taskjuggler--build-project (project info) + "Return a project declaration. +PROJECT is an headline. INFO is a plist used as a communication +channel. If no start date is specified, start today. If no end +date is specified, end `org-taskjuggler-default-project-duration' +days from now." + (format "project %s \"%s\" \"%s\" %s %s {\n}\n" + (org-taskjuggler-get-id project info) + (org-taskjuggler-get-name project) + ;; Version is obtained through :TASKJUGGLER_VERSION: + ;; property or `org-taskjuggler-default-project-version'. + (or (org-element-property :VERSION project) + org-taskjuggler-default-project-version) + (or (org-taskjuggler-get-start project) + (format-time-string "%Y-%m-%d")) + (let ((end (org-taskjuggler-get-end project))) + (or (and end (format "- %s" end)) + (format "+%sd" org-taskjuggler-default-project-duration))))) + +(defun org-taskjuggler--build-resource (resource info) + "Return a resource declaration. + +RESOURCE is an headline. INFO is a plist used as a communication +channel. + +All valid attributes from RESOURCE are inserted. If RESOURCE +defines a property \"resource_id\" it will be used as the id for +this resource. Otherwise it will use the ID property. If +neither is defined a unique id will be associated to it." + (concat + ;; Opening resource. + (format "resource %s \"%s\" {\n" + (org-taskjuggler--clean-id + (or (org-element-property :RESOURCE_ID resource) + (org-element-property :ID resource) + (org-taskjuggler-get-id resource info))) + (org-taskjuggler-get-name resource)) + ;; Add attributes. + (org-taskjuggler--indent-string + (org-taskjuggler--build-attributes + resource org-taskjuggler-valid-resource-attributes)) + ;; Add inner resources. + (org-taskjuggler--indent-string + (mapconcat + 'identity + (org-element-map (org-element-contents resource) 'headline + (lambda (hl) (org-taskjuggler--build-resource hl info)) + info nil 'headline) + "")) + ;; Closing resource. + "}\n")) + +(defun org-taskjuggler--build-report (report) + "Return a report declaration. +REPORT is an headline. INFO is a plist used as a communication +channel." + (concat + ;; Opening report. + (format "%s \"%s\" {\n" + (or (org-element-property :REPORT_KIND report) "taskreport") + (org-taskjuggler-get-name report)) + ;; Add attributes. + (org-taskjuggler--indent-string + (org-taskjuggler--build-attributes + report org-taskjuggler-valid-report-attributes)) + ;; Add inner reports. + (org-taskjuggler--indent-string + (mapconcat + 'identity + (org-element-map (org-element-contents report) 'headline + (lambda (hl) (org-taskjuggler--build-report hl info)) + info nil 'headline) + "")) + ;; Closing report. + "}\n")) + +(defun org-taskjuggler--build-task (task info) + "Return a task declaration. + +TASK is an headline. INFO is a plist used as a communication +channel. + +All valid attributes from TASK are inserted. If TASK defines +a property \"task_id\" it will be used as the id for this task. +Otherwise it will use the ID property. If neither is defined +a unique id will be associated to it." + (let* ((allocate (org-element-property :ALLOCATE task)) + (complete + (if (eq (org-element-property :todo-type task) 'done) "100" + (org-element-property :COMPLETE task))) + (depends (org-taskjuggler-resolve-dependencies task info)) + (effort (org-element-property :EFFORT task)) + (milestone + (or (org-element-property :MILESTONE task) + (and (org-element-map (org-element-contents task) 'headline + 'identity info t) ; Has task any child? + (not (or effort + (org-element-property :LENGTH task) + (org-element-property :DURATION task) + (and (org-taskjuggler-get-start task) + (org-taskjuggler-get-end task)) + (org-element-property :PERIOD task)))))) + (priority + (let ((pri (org-element-property :priority task))) + (and pri + (max 1 (/ (* 1000 (- org-lowest-priority pri)) + (- org-lowest-priority org-highest-priority))))))) + (concat + ;; Opening task. + (format "task %s \"%s\" {\n" + (org-taskjuggler-get-id task info) + (org-taskjuggler-get-name task)) + ;; Add default attributes. + (and depends + (format " depends %s\n" + (org-taskjuggler-format-dependencies depends task info))) + (and allocate + (format " purge %s\n allocate %s\n" + ;; Compatibility for previous TaskJuggler versions. + (if (>= org-export-taskjuggler-target-version 3.0) "allocate" + "allocations") + allocate)) + (and complete (format " complete %s\n" comptete)) + (and effort + (format " effort %s\n" + (let* ((minutes (org-duration-string-to-minutes effort)) + (hours (/ minutes 60.0))) + (format "%.1fh" hours)))) + (and priority (format " priority %s\n" complete)) + (and milestone " milestone\n") + ;; Add other valid attributes. + (org-taskjuggler--indent-string + (org-taskjuggler--build-attributes + task org-taskjuggler-valid-task-attributes)) + ;; Add inner tasks. + (org-taskjuggler--indent-string + (mapconcat 'identity + (org-element-map (org-element-contents task) 'headline + (lambda (hl) (org-taskjuggler--build-task hl info)) + info nil 'headline) + "")) + ;; Closing task. + "}\n"))) + + + +;;; Interactive Functions + +;;;###autoload +(defun org-taskjuggler-export (&optional async subtreep visible-only) + "Export current buffer to a TaskJuggler file. + +The exporter looks for a tree with tag that matches +`org-taskjuggler-project-tag' and takes this as the tasks for +this project. The first node of this tree defines the project +properties such as project name and project period. + +If there is a tree with tag that matches +`org-taskjuggler-resource-tag' this tree is taken as resources +for the project. If no resources are specified, a default +resource is created and allocated to the project. + +Also the TaskJuggler project will be created with default reports +as defined in `org-taskjuggler-default-reports'. + +If narrowing is active in the current buffer, only export its +narrowed part. + +If a region is active, export that region. + +A non-nil optional argument ASYNC means the process should happen +asynchronously. The resulting file should be accessible through +the `org-export-stack' interface. + +When optional argument SUBTREEP is non-nil, export the sub-tree +at point, extracting information from the headline properties +first. + +When optional argument VISIBLE-ONLY is non-nil, don't export +contents of hidden elements. + +Return output file's name." + (interactive) + (let ((outfile + (org-export-output-file-name org-taskjuggler-extension subtreep))) + (if async + (org-export-async-start + (lambda (f) + (org-export-add-to-stack f 'taskjuggler) + (run-hook-with-args 'org-taskjuggler-final-hook f)) + `(expand-file-name + (org-export-to-file 'taskjuggler ,outfile ,subtreep ,visible-only))) + (org-export-to-file 'taskjuggler outfile subtreep visible-only) + (run-hook-with-args 'org-taskjuggler-final-hook outfile) + outfile))) + +;;;###autoload +(defun org-taskjuggler-export-and-open (&optional subtreep visible-only) + "Export current buffer to a TaskJuggler file and open it. + +The exporter looks for a tree with tag that matches +`org-taskjuggler-project-tag' and takes this as the tasks for +this project. The first node of this tree defines the project +properties such as project name and project period. + +If there is a tree with tag that matches +`org-taskjuggler-resource-tag' this tree is taken as resources +for the project. If no resources are specified, a default +resource is created and allocated to the project. + +Also the TaskJuggler project will be created with default reports +as defined in `org-taskjuggler-default-reports'. + +If narrowing is active in the current buffer, only export its +narrowed part. + +If a region is active, export that region. + +When optional argument SUBTREEP is non-nil, export the sub-tree +at point, extracting information from the headline properties +first. + +When optional argument VISIBLE-ONLY is non-nil, don't export +contents of hidden elements. + +Open file with the TaskJuggler GUI." + (interactive) + (let* ((file (org-taskjuggler-export nil subtreep visible-only)) + (process-name "TaskJugglerUI") + (command (concat process-name " " file))) + (start-process-shell-command process-name nil command))) + + +(provide 'ox-taskjuggler) + +;; Local variables: +;; generated-autoload-file: "org-loaddefs.el" +;; sentence-end-double-space: t +;; End: + +;;; ox-taskjuggler.el ends here diff --git a/lisp/org.el b/lisp/org.el index 76c2dca..62bee4e 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -493,7 +493,8 @@ depends on, if any." (const :tag " infojs: Set up Sebastian Rose's JavaScript org-info.js" jsinfo) (const :tag "C confluence Export buffer to Confluence Wiki format" confluence) (const :tag "C groff Export buffer to Groff format" groff) - (const :tag "C koma-letter Export buffer to KOMA Scrlttrl2 format" koma-letter))) + (const :tag "C koma-letter Export buffer to KOMA Scrlttrl2 format" koma-letter) + (const :tag "C taskjuggler Export buffer to TaskJuggler format" taskjuggler))) (eval-after-load 'ox '(mapc |