diff --git a/org-clock-csv-tests.el b/org-clock-csv-tests.el index b765f85..a9b8dfa 100644 --- a/org-clock-csv-tests.el +++ b/org-clock-csv-tests.el @@ -59,6 +59,34 @@ "Test file level category." (org-clock-csv-should-match "tests/issue-5.org" "tests/issue-5.csv")) +(ert-deftest test-issue-24 () + "Test nested tags." + (org-clock-csv-should-match "tests/issue-24.org" "tests/issue-24.csv")) + +(ert-deftest test-issue-24-nil () + "Test local tags only." + (setq org-use-tag-inheritance nil) + (org-clock-csv-should-match "tests/issue-24.org" "tests/issue-24-nil.csv") + (setq org-use-tag-inheritance t)) + +(ert-deftest test-issue-24-list () + "Test tags in list." + (setq org-use-tag-inheritance (list "ex4" "ex1" "tag1")) + (org-clock-csv-should-match "tests/issue-24.org" "tests/issue-24-list.csv") + (setq org-use-tag-inheritance t)) + +(ert-deftest test-issue-24-regexp () + "Test tags in regexp." + (setq org-use-tag-inheritance "e.[13]") + (org-clock-csv-should-match "tests/issue-24.org" "tests/issue-24-regexp.csv") + (setq org-use-tag-inheritance t)) + +(ert-deftest test-issue-24-exclude () + "Test tags exclusion." + (setq org-tags-exclude-from-inheritance (list "tag2" "ex1")) + (org-clock-csv-should-match "tests/issue-24.org" "tests/issue-24-ex.csv") + (setq org-tags-exclude-from-inheritance nil)) + (ert-deftest test-issue-26 () "Test file without title." (let ((org-clock-csv-header org-clock-csv-header-all-props) diff --git a/org-clock-csv.el b/org-clock-csv.el index c129e59..8953337 100644 --- a/org-clock-csv.el +++ b/org-clock-csv.el @@ -160,6 +160,33 @@ Returns the DEFAULT file level category if none is found." (if ph (cons ph (org-clock-csv--find-headlines ph))))) +(defun org-clock-csv--filter-inherited-tags (tags) + "Filter a list of TAGS according to +`org-tags-exclude-from-inheritance' and `org-use-tag-inheritance'." + (let ((included-tags + (cond ((not org-use-tag-inheritance) nil) + ((eq org-use-tag-inheritance t) tags) + ((listp org-use-tag-inheritance) + (mapcar (lambda (tag) (if (member tag org-use-tag-inheritance) tag)) tags)) + ((stringp org-use-tag-inheritance) + (mapcar (lambda (tag) (if (string-match org-use-tag-inheritance tag) tag)) tags))))) + (if (not org-tags-exclude-from-inheritance) + included-tags + (cl-set-difference included-tags org-tags-exclude-from-inheritance :test #'equal)))) + +(defun org-clock-csv--find-tags (headlines default) + "Search tags in HEADLINES respecting +`org-tags-exclude-from-inheritance' and `org-use-tag-inheritance'. +DEFAULT tags are also checked against those variables. + +The tags of the first headline are always added." + (remove nil (append (org-clock-csv--filter-inherited-tags + (append (split-string default ":" t) + (apply #'append ; flatten + (mapcar (lambda (headline) (org-element-property :tags headline)) + (reverse (cdr headlines)))))) + (org-element-property :tags (car headlines))))) + (defun org-clock-csv--get-properties-plist (element) "Returns a plist of the [inherited] properties drawer of an org element" ;; org-entry-properties returns an ALIST, but we don't want to have to handle @@ -172,7 +199,7 @@ Returns the DEFAULT file level category if none is found." (lambda (acc key) (plist-put acc key (org-entry-get el key t))) (org-buffer-property-keys) nil)))) -(defun org-clock-csv--parse-element (element title default-category) +(defun org-clock-csv--parse-element (element title default-category default-tags) "Ingest clock ELEMENT and produces a plist of its relevant properties." (when (and (equal (org-element-type element) 'clock) @@ -188,10 +215,8 @@ properties." (task (car headlines-values)) (parents (reverse (cdr headlines-values))) (effort (org-element-property :EFFORT task-headline)) - ;; TODO: Handle tag inheritance, respecting the value of - ;; `org-tags-exclude-from-inheritance'. (tags (mapconcat #'identity - (org-element-property :tags task-headline) ":")) + (org-clock-csv--find-tags headlines default-tags) ":")) (ishabit (when (equal "habit" (org-element-property :STYLE task-headline)) "t")) @@ -236,8 +261,9 @@ properties." or the DEFAULT value if it does not exist." (let ((value (org-element-map ast 'keyword (lambda (elem) (if (string-equal (org-element-property :key elem) property) - (org-element-property :value elem)))))) - (if (equal nil value) default (car value)))) + (org-element-property :value elem))) + nil t))) + (if (equal nil value) default value))) (defun org-clock-csv--get-entries (filelist &optional no-check) "Retrieves clock entries from files in FILELIST. @@ -251,9 +277,10 @@ When NO-CHECK is non-nil, skip checking if all files exist." (with-current-buffer (find-file-noselect file) (let* ((ast (org-element-parse-buffer)) (title (org-clock-csv--get-org-data 'TITLE ast file)) - (category (org-clock-csv--get-org-data 'CATEGORY ast ""))) + (category (org-clock-csv--get-org-data 'CATEGORY ast "")) + (tags (org-clock-csv--get-org-data 'FILETAGS ast ""))) (org-element-map ast 'clock - (lambda (c) (org-clock-csv--parse-element c title category)) + (lambda (c) (org-clock-csv--parse-element c title category tags)) nil nil))))) ;;;; Public API: diff --git a/tests/issue-24-ex.csv b/tests/issue-24-ex.csv new file mode 100644 index 0000000..037b480 --- /dev/null +++ b/tests/issue-24-ex.csv @@ -0,0 +1,8 @@ +task,parents,category,start,end,effort,ishabit,tags +Task A,,,2017-01-01 06:00,2017-01-01 07:00,,,tag1:ex1:ex2 +Task AB,Task A,,2017-01-01 07:00,2017-01-01 08:00,,,tag1:ex2:ex3 +Task ABC,Task A/Task AB,,2017-01-01 08:00,2017-01-01 09:00,,,tag1:ex2:ex3:ex4 +Task ABCD,Task A/Task AB/Task ABC,,2017-01-01 09:00,2017-01-01 10:00,,,tag1:ex2:ex3:ex4:ex5 +Task BA,Task B,,2017-01-01 10:00,2017-01-01 11:00,,,tag1:ex2:ex1 +Task BB,Task B,,2017-01-01 11:00,2017-01-01 12:00,,,tag1:ex2 +Task BBC,Task B/Task BB,,2017-01-01 12:00,2017-01-01 13:00,,,tag1:ex2:ex4 diff --git a/tests/issue-24-list.csv b/tests/issue-24-list.csv new file mode 100644 index 0000000..662b5dc --- /dev/null +++ b/tests/issue-24-list.csv @@ -0,0 +1,8 @@ +task,parents,category,start,end,effort,ishabit,tags +Task A,,,2017-01-01 06:00,2017-01-01 07:00,,,tag1:ex1:ex2 +Task AB,Task A,,2017-01-01 07:00,2017-01-01 08:00,,,tag1:ex1:ex3 +Task ABC,Task A/Task AB,,2017-01-01 08:00,2017-01-01 09:00,,,tag1:ex1:ex4 +Task ABCD,Task A/Task AB/Task ABC,,2017-01-01 09:00,2017-01-01 10:00,,,tag1:ex1:ex4:ex5 +Task BA,Task B,,2017-01-01 10:00,2017-01-01 11:00,,,tag1:ex1 +Task BB,Task B,,2017-01-01 11:00,2017-01-01 12:00,,,tag1 +Task BBC,Task B/Task BB,,2017-01-01 12:00,2017-01-01 13:00,,,tag1:ex4 diff --git a/tests/issue-24-nil.csv b/tests/issue-24-nil.csv new file mode 100644 index 0000000..c0eb1e3 --- /dev/null +++ b/tests/issue-24-nil.csv @@ -0,0 +1,8 @@ +task,parents,category,start,end,effort,ishabit,tags +Task A,,,2017-01-01 06:00,2017-01-01 07:00,,,ex1:ex2 +Task AB,Task A,,2017-01-01 07:00,2017-01-01 08:00,,,ex3 +Task ABC,Task A/Task AB,,2017-01-01 08:00,2017-01-01 09:00,,,ex4 +Task ABCD,Task A/Task AB/Task ABC,,2017-01-01 09:00,2017-01-01 10:00,,,ex5 +Task BA,Task B,,2017-01-01 10:00,2017-01-01 11:00,,,ex1 +Task BB,Task B,,2017-01-01 11:00,2017-01-01 12:00,,, +Task BBC,Task B/Task BB,,2017-01-01 12:00,2017-01-01 13:00,,,ex4 diff --git a/tests/issue-24-regexp.csv b/tests/issue-24-regexp.csv new file mode 100644 index 0000000..b0e9acf --- /dev/null +++ b/tests/issue-24-regexp.csv @@ -0,0 +1,8 @@ +task,parents,category,start,end,effort,ishabit,tags +Task A,,,2017-01-01 06:00,2017-01-01 07:00,,,ex1:ex2 +Task AB,Task A,,2017-01-01 07:00,2017-01-01 08:00,,,ex1:ex3 +Task ABC,Task A/Task AB,,2017-01-01 08:00,2017-01-01 09:00,,,ex1:ex3:ex4 +Task ABCD,Task A/Task AB/Task ABC,,2017-01-01 09:00,2017-01-01 10:00,,,ex1:ex3:ex5 +Task BA,Task B,,2017-01-01 10:00,2017-01-01 11:00,,,ex1 +Task BB,Task B,,2017-01-01 11:00,2017-01-01 12:00,,, +Task BBC,Task B/Task BB,,2017-01-01 12:00,2017-01-01 13:00,,,ex4 diff --git a/tests/issue-24.csv b/tests/issue-24.csv new file mode 100644 index 0000000..a5b7dc3 --- /dev/null +++ b/tests/issue-24.csv @@ -0,0 +1,8 @@ +task,parents,category,start,end,effort,ishabit,tags +Task A,,,2017-01-01 06:00,2017-01-01 07:00,,,tag1:tag2:ex1:ex2 +Task AB,Task A,,2017-01-01 07:00,2017-01-01 08:00,,,tag1:tag2:ex1:ex2:ex3 +Task ABC,Task A/Task AB,,2017-01-01 08:00,2017-01-01 09:00,,,tag1:tag2:ex1:ex2:ex3:ex4 +Task ABCD,Task A/Task AB/Task ABC,,2017-01-01 09:00,2017-01-01 10:00,,,tag1:tag2:ex1:ex2:ex3:ex4:ex5 +Task BA,Task B,,2017-01-01 10:00,2017-01-01 11:00,,,tag1:tag2:ex2:ex1 +Task BB,Task B,,2017-01-01 11:00,2017-01-01 12:00,,,tag1:tag2:ex2 +Task BBC,Task B/Task BB,,2017-01-01 12:00,2017-01-01 13:00,,,tag1:tag2:ex2:ex4 diff --git a/tests/issue-24.org b/tests/issue-24.org new file mode 100644 index 0000000..7b225a5 --- /dev/null +++ b/tests/issue-24.org @@ -0,0 +1,32 @@ +#+STARTUP: content +#+FILETAGS: :tag1:tag2: + +* Task A :ex1:ex2: + :LOGBOOK: + CLOCK: [2017-01-01 Sun 06:00]--[2017-01-01 Sun 07:00] => 1:00 + :END: +** Task AB :ex3: + :LOGBOOK: + CLOCK: [2017-01-01 Sun 07:00]--[2017-01-01 Sun 08:00] => 1:00 + :END: +*** Task ABC :ex4: +:LOGBOOK: +CLOCK: [2017-01-01 Sun 08:00]--[2017-01-01 Sun 09:00] => 1:00 +:END: +**** Task ABCD :ex5: +:LOGBOOK: +CLOCK: [2017-01-01 Sun 09:00]--[2017-01-01 Sun 10:00] => 1:00 +:END: +* Task B :ex2: +** Task BA :ex1: +:LOGBOOK: +CLOCK: [2017-01-01 Sun 10:00]--[2017-01-01 Sun 11:00] => 1:00 +:END: +** Task BB +:LOGBOOK: +CLOCK: [2017-01-01 Sun 11:00]--[2017-01-01 Sun 12:00] => 1:00 +:END: +*** Task BBC :ex4: +:LOGBOOK: +CLOCK: [2017-01-01 Sun 12:00]--[2017-01-01 Sun 13:00] => 1:00 +:END: