git.fiddlerwoaroof.com
Browse code

Build freshbooks and tests into main client

fiddlerwoaroof authored on 26/04/2016 18:30:49
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