Browse code
Build freshbooks and tests into main client
fiddlerwoaroof authored on 26/04/2016 18:30:49
Showing 5 changed files
Showing 5 changed files
... | ... |
@@ -14,7 +14,14 @@ |
14 | 14 |
(ubiquitous:value :freshbooks :api-key) |
15 | 15 |
(ubiquitous:value :freshbooks :endpoint)) |
16 | 16 |
|
17 |
-(xhtmlambda::def-element <::request) |
|
17 |
+(eval-when (:load-toplevel :compile-toplevel :execute) |
|
18 |
+ (xhtmlambda::def-element <::request) |
|
19 |
+ (xhtmlambda::def-element <::time_entry) |
|
20 |
+ (xhtmlambda::def-element <::project_id) |
|
21 |
+ (xhtmlambda::def-element <::task_id) |
|
22 |
+ (xhtmlambda::def-element <::hours) |
|
23 |
+ (xhtmlambda::def-element <::notes) |
|
24 |
+ (xhtmlambda::def-element <::date)) |
|
18 | 25 |
|
19 | 26 |
(defun post-to-endpoint (xml) |
20 | 27 |
(let ((drakma:*text-content-types* (acons "application" "xml" drakma:*text-content-types*))) |
... | ... |
@@ -129,13 +136,6 @@ |
129 | 136 |
slot-name |
130 | 137 |
(node))))))) |
131 | 138 |
|
132 |
-(xhtmlambda::def-element <::time_entry) |
|
133 |
-(xhtmlambda::def-element <::project_id) |
|
134 |
-(xhtmlambda::def-element <::task_id) |
|
135 |
-(xhtmlambda::def-element <::hours) |
|
136 |
-(xhtmlambda::def-element <::notes) |
|
137 |
-(xhtmlambda::def-element <::date) |
|
138 |
- |
|
139 | 139 |
(defun get-project (name) |
140 | 140 |
(let ((projects (sort (map 'vector |
141 | 141 |
(alambda (cons (string-downcase it) it)) |
... | ... |
@@ -178,7 +178,7 @@ |
178 | 178 |
for note = (timesheet::memo entry) |
179 | 179 |
for hours = (timesheet::duration entry) |
180 | 180 |
for fmt-date = (format nil "~:@{~2,'0d-~2,'0d-~2,'0d 00:00:00~}" |
181 |
- (reverse (timesheet::unroll-date date))) |
|
181 |
+ (reverse (timesheet.cli::unroll-date date))) |
|
182 | 182 |
collect (make-time-entry project task-id fmt-date hours note)))) |
183 | 183 |
|
184 | 184 |
(defun make-entry-updates () |
185 | 185 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,59 @@ |
1 |
+ |
|
2 |
+(defpackage #:timesheet.ql |
|
3 |
+ (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils |
|
4 |
+ #:timesheet.macros #:generic-equals)) |
|
5 |
+ |
|
6 |
+(in-package #:timesheet.ql) |
|
7 |
+ |
|
8 |
+(defstruct (ql-clause (:type vector)) |
|
9 |
+ clause-type clause-parts) |
|
10 |
+ |
|
11 |
+(defun .name () |
|
12 |
+ (.map 'string |
|
13 |
+ (.is (.or #'alpha-char-p |
|
14 |
+ #'digit-char-p)))) |
|
15 |
+ |
|
16 |
+(defun .order-specifier () |
|
17 |
+ (.or (.string= "desc") |
|
18 |
+ (.string= "asc"))) |
|
19 |
+ |
|
20 |
+(defun .order-list (&optional (separator #\,)) |
|
21 |
+ (.map 'list (.let* ((name (.name)) |
|
22 |
+ (order (.optional |
|
23 |
+ (.and (.is #'whitespacep) |
|
24 |
+ (.order-specifier)))) |
|
25 |
+ (_ (.optional (.char= separator)))) |
|
26 |
+ (.identity (cons name |
|
27 |
+ (or order "asc")))))) |
|
28 |
+ |
|
29 |
+(defun .name-list (&optional (separator #\,)) |
|
30 |
+ (.map 'list (.let* ((name (.map 'string |
|
31 |
+ (.is (.or #'alpha-char-p |
|
32 |
+ #'digit-char-p)))) |
|
33 |
+ (_ (.optional (.char= separator)))) |
|
34 |
+ (.identity name)))) |
|
35 |
+ |
|
36 |
+(defun .select-clause () |
|
37 |
+ (.let* ((_ (.string= "select")) |
|
38 |
+ (_ (.is #'whitespacep)) |
|
39 |
+ (names (.name-list))) |
|
40 |
+ (.identity (vector :order-clause names)))) |
|
41 |
+ |
|
42 |
+(defun .where-clause () |
|
43 |
+ (.let* ((_ (.string= "where")) |
|
44 |
+ (_ (.is #'whitespacep)) |
|
45 |
+ (names (.name-list))) |
|
46 |
+ (.identity (vector :order-clause names)))) |
|
47 |
+ |
|
48 |
+(defun .order-clause () |
|
49 |
+ (.let* ((_ (.string= "order by")) |
|
50 |
+ (_ (.is #'whitespacep)) |
|
51 |
+ (names (.order-list))) |
|
52 |
+ (.identity (vector :order-clause names)))) |
|
53 |
+ |
|
54 |
+(defun .select-statement () |
|
55 |
+ (.let* ((_ (.string= "where")) |
|
56 |
+ (_ (.is #'whitespacep)) |
|
57 |
+ (names (.name-list))) |
|
58 |
+ (.identity (vector :order-clause names)))) |
|
59 |
+ |
... | ... |
@@ -35,7 +35,8 @@ |
35 | 35 |
|
36 | 36 |
(defpackage #:timesheet.cli |
37 | 37 |
(:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils |
38 |
- #:timesheet.parser #:timesheet #:net.didierverna.clon) |
|
38 |
+ #:timesheet.parser #:timesheet #:net.didierverna.clon |
|
39 |
+ #:plambda) |
|
39 | 40 |
(:import-from #:format-string-builder #:define-message)) |
40 | 41 |
|
41 | 42 |
(in-package #:timesheet) |
... | ... |
@@ -1,41 +1,12 @@ |
1 | 1 |
(in-package #:timesheet.cli) |
2 | 2 |
|
3 | 3 |
(defparameter *interactive* nil) |
4 |
-(defparameter *version* "0:5") |
|
5 |
- |
|
6 |
-(defsynopsis (:postfix "TIMESHEETS ...") |
|
7 |
- (text :contents "A program for managing logs of hours worked") |
|
8 |
- (group (:header "Display options") |
|
9 |
- (flag :short-name "s" :long-name "status" |
|
10 |
- :description "Print a short summary of work status") |
|
11 |
- (flag :short-name "W" |
|
12 |
- :long-name "ignore-whitespace" |
|
13 |
- :description "Ignore whitespace errors in input") |
|
14 |
- (flag :short-name "i" :long-name "interactive" |
|
15 |
- :description "Run interactively")) |
|
16 |
- (group (:header "Sort options") |
|
17 |
- (flag :short-name "r" |
|
18 |
- :long-name "reverse" |
|
19 |
- :description "Reverse the sort direction") |
|
20 |
- (flag :short-name "c" |
|
21 |
- :long-name "client" |
|
22 |
- :description "Sort records by client")) |
|
23 |
- (group (:header "Generic options") |
|
24 |
- (flag :short-name "v" :long-name "version" |
|
25 |
- :description "Show the program version") |
|
26 |
- (flag :short-name "h" :long-name "help" |
|
27 |
- :description "Show this help"))) |
|
28 |
- |
|
29 |
-(define-message version-message (version) |
|
30 |
- (:own-line () "timesheet file parser, version " :str)) |
|
4 |
+(defparameter *version* "0:6") |
|
31 | 5 |
|
32 | 6 |
(defun unroll-date (date-obj) |
33 | 7 |
(with-slots (year month day) date-obj |
34 | 8 |
(list day month year))) |
35 | 9 |
|
36 |
-(defun show-version () |
|
37 |
- (version-message t *version*)) |
|
38 |
- |
|
39 | 10 |
(defun split-time (time) |
40 | 11 |
(let ((time-parts (split-sequence #\: time))) |
41 | 12 |
(destructuring-bind (hours minutes . optional-seconds) time-parts |
... | ... |
@@ -120,7 +91,7 @@ |
120 | 91 |
(flet ((parse-string (string) |
121 | 92 |
(handler-bind ((timesheet.parser::invalid-whitespace |
122 | 93 |
(handle-invalid-whitespace ignore-whitespace-errors)) |
123 |
- (parse-error #'handle-invalid-time) |
|
94 |
+ (parse-error #'handle-invalid-time) |
|
124 | 95 |
(timesheet.parser::invalid-time #'handle-invalid-time) ) |
125 | 96 |
(smug:parse (timesheet.parser::.date-records) string)))) |
126 | 97 |
(multiple-value-bind (parsed leftovers) (parse-string (read-file-into-string file)) |
... | ... |
@@ -143,17 +114,32 @@ |
143 | 114 |
:key (alambda (apply #'local-time:encode-timestamp |
144 | 115 |
(list* 0 0 0 0 (unroll-date (date it))))))) |
145 | 116 |
|
117 |
+(defun maybe-nreverse (flag list) |
|
118 |
+ (if flag |
|
119 |
+ (nreverse list) |
|
120 |
+ list)) |
|
121 |
+ |
|
122 |
+(define-modify-macro maybe-nreversef (flag) |
|
123 |
+ (lambda (place flag) |
|
124 |
+ (maybe-nreverse flag place))) |
|
125 |
+ |
|
126 |
+(defun list-without-nulls (&rest items) |
|
127 |
+ (loop for item in items |
|
128 |
+ when item collect item)) |
|
129 |
+ |
|
146 | 130 |
(defun pprint-log (args &key client reverse status ignore-whitespace interactive) |
147 |
- (flet ((sort-results (results &optional (client client)) |
|
148 |
- (setf results (sort-by-date results)) |
|
149 |
- (when client |
|
150 |
- (setf results (stable-sort results #'string-lessp :key #'client))) |
|
151 |
- (when reverse |
|
152 |
- (setf results (nreverse results))) |
|
153 |
- results) |
|
154 |
- (get-logs (files) |
|
155 |
- (loop for file in (ensure-list files) |
|
156 |
- append (timesheet:get-log file ignore-whitespace)) )) |
|
131 |
+ (labels ((sort-func (client) |
|
132 |
+ (apply #'compose |
|
133 |
+ (list-without-nulls |
|
134 |
+ (when reverse #'nreverse) |
|
135 |
+ (when client |
|
136 |
+ (plambda (stable-sort :1 #'string-lessp :key #'client))) |
|
137 |
+ #'sort-by-date))) |
|
138 |
+ (sort-results (results &optional (client client)) |
|
139 |
+ (funcall (sort-func client) results)) |
|
140 |
+ (get-logs (files) |
|
141 |
+ (loop for file in (ensure-list files) |
|
142 |
+ append (timesheet:get-log file ignore-whitespace)) )) |
|
157 | 143 |
|
158 | 144 |
(let ((*default-time-sheet-file* (or args *default-time-sheet-file*)) |
159 | 145 |
(*interactive* interactive) |
... | ... |
@@ -166,6 +152,52 @@ |
166 | 152 |
(incomplete-results (sort-results incomplete-ranges t))) |
167 | 153 |
(pprint-results complete-results incomplete-results status))))))) |
168 | 154 |
|
155 |
+(defsynopsis (:postfix "TIMESHEETS ...") |
|
156 |
+ (text :contents "A program for managing logs of hours worked") |
|
157 |
+ (group (:header "Display options") |
|
158 |
+ (flag :short-name "s" :long-name "status" |
|
159 |
+ :description "Print a short summary of work status") |
|
160 |
+ (flag :short-name "W" |
|
161 |
+ :long-name "ignore-whitespace" |
|
162 |
+ :description "Ignore whitespace errors in input") |
|
163 |
+ (flag :short-name "i" :long-name "interactive" |
|
164 |
+ :description "Run interactively")) |
|
165 |
+ (group (:header "Sort options") |
|
166 |
+ (flag :short-name "r" |
|
167 |
+ :long-name "reverse" |
|
168 |
+ :description "Reverse the sort direction") |
|
169 |
+ (flag :short-name "c" |
|
170 |
+ :long-name "client" |
|
171 |
+ :description "Sort records by client")) |
|
172 |
+ (group (:header "Freshbooks") |
|
173 |
+ (flag :long-name "post-hours" |
|
174 |
+ :description "Post hours to freshbooks (requires manual setup of Freshbooks keys)")) |
|
175 |
+ (group (:header "Self-test options") |
|
176 |
+ (flag :long-name "run-tests" |
|
177 |
+ :description "Run the tests") |
|
178 |
+ (enum :long-name "output-style" |
|
179 |
+ :description "The kind of output to produce" |
|
180 |
+ :default-value :normal |
|
181 |
+ :enum '(:xunit :normal))) |
|
182 |
+ (group (:header "Generic options") |
|
183 |
+ (flag :short-name "v" :long-name "version" |
|
184 |
+ :description "Show the program version") |
|
185 |
+ (flag :short-name "h" :long-name "help" |
|
186 |
+ :description "Show this help"))) |
|
187 |
+ |
|
188 |
+(eval-when (:load-toplevel :compile-toplevel :execute) |
|
189 |
+ (define-message version-message (version) |
|
190 |
+ (:own-line () "timesheet file parser, version " :str))) |
|
191 |
+ |
|
192 |
+(defun show-version () |
|
193 |
+ (version-message t *version*)) |
|
194 |
+ |
|
195 |
+(defun tests-main (&optional (output-style nil output-style-p)) |
|
196 |
+ (let ((should-test:*verbose* t)) |
|
197 |
+ (ecase output-style |
|
198 |
+ (:xunit (should-test:test-for-xunit *standard-output* :package :timesheet.parser)) |
|
199 |
+ (:normal (should-test:test :package :timesheet.parser))))) |
|
200 |
+ |
|
169 | 201 |
(defun pprint-log-main () |
170 | 202 |
(make-context) |
171 | 203 |
(tagbody |
... | ... |
@@ -174,6 +206,12 @@ |
174 | 206 |
(cond |
175 | 207 |
((getopt :long-name "help") (help)) |
176 | 208 |
((getopt :long-name "version") (show-version)) |
209 |
+ ((getopt :long-name "post-hours") (let ((*print-pretty* nil)) |
|
210 |
+ (loop for item in (timesheet.freshbooks::post-time-entries-main) |
|
211 |
+ do (format t "Posted an entry") |
|
212 |
+ do (plump:serialize item) |
|
213 |
+ finally (format t "Don't forget to archive time file.")))) |
|
214 |
+ ((getopt :long-name "run-tests") (tests-main (getopt :long-name "output-style"))) |
|
177 | 215 |
(t (with-timesheet-configuration () |
178 | 216 |
(pprint-log |
179 | 217 |
(remainder) |
... | ... |
@@ -16,6 +16,7 @@ |
16 | 16 |
#:local-time-duration |
17 | 17 |
#:lquery |
18 | 18 |
#:ningle |
19 |
+ #:positional-lambda |
|
19 | 20 |
#:serapeum |
20 | 21 |
#:should-test |
21 | 22 |
#:smug |
... | ... |
@@ -30,6 +31,7 @@ |
30 | 31 |
(:file "parser") |
31 | 32 |
(:file "mvc") |
32 | 33 |
(:file "main-classes") |
34 |
+ (:file "freshbooks") |
|
33 | 35 |
(:file "timesheet"))) |
34 | 36 |
|
35 | 37 |
|