git.fiddlerwoaroof.com
Browse code

Cleanup and Freshbooks bindings

fiddlerwoaroof authored on 22/02/2016 17:22:57
Showing 4 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,172 @@
1
+(in-package :timesheet.freshbooks)
2
+
3
+(defvar *api-key*)
4
+(defvar *endpoint*)
5
+
6
+(defun init ()
7
+  (ubiquitous:restore 'timesheet)
8
+  (ubiquitous:value :freshbooks :api-key)
9
+  (ubiquitous:value :freshbooks :endpoint))
10
+
11
+(xhtmlambda::def-element <::request)
12
+
13
+(defun post-to-endpoint (xml)
14
+  (let ((drakma:*text-content-types* (acons "application" "xml" drakma:*text-content-types*)))
15
+    (drakma:http-request *endpoint*
16
+                         :basic-authorization (list *api-key* "X")
17
+                         :method :post
18
+                         :content (with-output-to-string (s)
19
+                                    (format s "~w" xml)))))
20
+
21
+(defun parsed-api-call (xml)
22
+  (plump-parser:parse (post-to-endpoint xml)))
23
+
24
+(defmacro define-api-call (name (&rest args) method-name &body elements)
25
+  `(defun ,name ,args
26
+     (parsed-api-call (<:request (:method ,method-name) ,@elements))))
27
+
28
+(define-api-call list-invoices () "invoice.list")
29
+(define-api-call list-projects () "project.list")
30
+(define-api-call list-tasks () "task.list")
31
+(define-api-call list-payments () "payment.list")
32
+(define-api-call list-clients () "client.list")
33
+
34
+(defgeneric slots-for (cls)
35
+  (:method-combination append))
36
+
37
+(defmacro define-simple-class (name (&rest supers) &body elements)
38
+  (let ((schema-name (intern
39
+                       (string-join (list name "-SCHEMA"))))
40
+        (schema-supers (loop for super in supers
41
+                             collect (intern
42
+                                       (string-join
43
+                                         (list (symbol-name super)
44
+                                               "-schema"))))))
45
+    `(prog1
46
+       (defclass ,name ,supers
47
+         ,(list*
48
+            '(registry :initform (make-hash-table :test 'equal) :allocation :class)
49
+            (loop for element in elements
50
+                collect `(,element :initarg ,(make-keyword element) :initform nil))))
51
+       (defclass ,schema-name ,schema-supers ())
52
+       (defmethod slots-for append ((cls ,schema-name))
53
+         ',elements))))
54
+
55
+(define-simple-class task ()
56
+  task_id name description billable rate)
57
+
58
+(define-simple-class project ()
59
+  project_id name description rate bill_method client_id hour_budget
60
+  tasks staff)
61
+
62
+(timesheet.macros:define-printer (task s)
63
+  ((with-slots (task_id name) task
64
+     (format s "~i~a (~a):" name task_id)))
65
+  ((with-slots (task_id name) task
66
+     (format s "~a (~a)" name task_id))))
67
+
68
+(timesheet.macros:define-printer (project s)
69
+  ((with-slots (project_id name tasks) project
70
+     (format s "~i~a (~a):~%~{~a~%~}" name project_id tasks)))
71
+  ((with-slots (project_id name tasks) project
72
+     (format s "~a (~a): ~a tasks" name project_id (length tasks)))))
73
+
74
+(defparameter *task-registry* (make-hash-table :test 'equal))
75
+(defmethod initialize-instance :after ((self task) &key &allow-other-keys)
76
+  (loop for slot in '(task_id name description billable rate)
77
+        for node = (slot-value self slot)
78
+        unless (null node) do (setf (slot-value self slot) (plump:text node))))
79
+
80
+(defun register-task (task)
81
+  (with-slots (task_id) task
82
+    (setf (gethash task_id *task-registry*) task)))
83
+
84
+(defparameter *project-registry* (make-hash-table :test 'equal))
85
+(defmethod initialize-instance :after ((self project) &key &allow-other-keys)
86
+  (loop for slot in '(project_id name description rate bill_method client_id hour_budget)
87
+        for node = (slot-value self slot)
88
+        unless (null node) do (setf (slot-value self slot) (plump:text node)))
89
+
90
+  (with-slots (tasks) self
91
+    (setf tasks
92
+          (loop for task across (lquery:$ (inline tasks)  "> task")
93
+                for task_id = (lquery:$1 (inline task) "task_id" (text))
94
+                for task-obj = (gethash task_id *task-registry*)
95
+                unless task-obj do (setf task-obj (parse-task task))
96
+                collect task-obj))))
97
+
98
+(defun register-project (project)
99
+  (with-slots (name) project
100
+    (setf (gethash name *project-registry*) project)))
101
+
102
+
103
+
104
+(defun parse-task (parsed-xml)
105
+  (apply #'make-instance 'task
106
+         (loop for slot in (slots-for (make-instance 'task-schema))
107
+               nconc (let ((slot-name (format nil "> ~(~a~)" slot)))
108
+                       (list (make-keyword slot)
109
+                             (lquery:$ (inline parsed-xml)
110
+                                       slot-name
111
+                                       (node)))))))
112
+
113
+(defun parse-project (parsed-xml)
114
+  (apply #'make-instance 'project
115
+           (loop for slot in (slots-for (make-instance 'project-schema))
116
+                 nconc (let ((slot-name (format nil "> ~(~a~)" slot)))
117
+                         (list (make-keyword slot)
118
+                               (lquery:$ (inline parsed-xml)
119
+                                         slot-name
120
+                                         (node)))))))
121
+
122
+(xhtmlambda::def-element <::time_entry)
123
+(xhtmlambda::def-element <::project_id)
124
+(xhtmlambda::def-element <::task_id)
125
+(xhtmlambda::def-element <::hours)
126
+(xhtmlambda::def-element <::notes)
127
+(xhtmlambda::def-element <::date)
128
+
129
+(defun get-project (name)
130
+  (let ((projects (sort (map 'vector
131
+                             (alambda (cons (string-downcase it) it))
132
+                             (hash-table-keys *project-registry*))
133
+                        #'string<
134
+                        :key #'car))
135
+        (name (string-downcase name)))
136
+    (gethash
137
+      (cdr
138
+        (find-if (alambda (string= it name :end1 (length name)))
139
+                 projects
140
+                 :key #'car))
141
+      *project-registry*)))
142
+
143
+
144
+(defun make-time-entry (project task date hours notes)
145
+  (<:time_entry ()
146
+                (<:project_id ()
147
+                              (slot-value (get-project project)
148
+                                          'project_id))
149
+                (<:task_id () (identity task))
150
+                (<:date () (identity date))
151
+                (<:hours () (identity hours))
152
+                (<:notes () (identity notes))))
153
+
154
+(defun get-task-by-name (name)
155
+  (let ((tasks (mapcar
156
+                 (destructuring-lambda ((id . task))
157
+                   (cons (slot-value task 'name)
158
+                         id))
159
+                 (hash-table-alist *task-registry*))))
160
+    (cdr (assoc name tasks :test #'string-equal))))
161
+
162
+(defun timesheet-to-entries (timesheet-log)
163
+  (let ((task-id (get-task-by-name "General")))
164
+    (loop for (date project hours note) in timesheet-log
165
+          for fmt-date = (format nil "~:@{~2,'0d-~2,'0d-~2,'0d~}"
166
+                                 (reverse (timesheet::unroll-date date)))
167
+          collect (make-time-entry project task-id fmt-date hours note))))
168
+
169
+(defun make-entry-updates ()
170
+  (let ((updates (timesheet-to-entries (timesheet::get-log #p"/home/edwlan/bucket/time.md"))))
171
+    (loop for update in updates
172
+          collect (<:request (:method "time_entry.create") update))))
... ...
@@ -1,4 +1,9 @@
1 1
 ;;;; package.lisp
2
+(defpackage #:timesheet.packages
3
+  (:use #:cl))
4
+(in-package #:timesheet.packages)
5
+
6
+
2 7
 (defpackage #:generic-equals
3 8
   (:use #:cl)
4 9
   (:export #:==))
... ...
@@ -24,3 +29,9 @@
24 29
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
25 30
         #:timesheet.parser))
26 31
 
32
+(defpackage #:timesheet.freshbooks
33
+  (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
34
+        #:timesheet.parser)
35
+  (:import-from #:timesheet #:timesheet)
36
+  (:export #:timesheet))
37
+
... ...
@@ -18,6 +18,9 @@
18 18
                #:command-line-arguments
19 19
                #:manardb
20 20
                #:local-time-duration
21
+               #:xhtmlambda
22
+               #:drakma
23
+               #:lquery
21 24
                )
22 25
   :serial t
23 26
   :components ((:file "package")
... ...
@@ -69,7 +69,7 @@
69 69
   '((("client" #\c) :type boolean :optional t :documentation "Sort by client")
70 70
     (("reverse" #\r) :type boolean :optional t :documentation "Reverse sort")
71 71
     (("status" #\s) :type boolean :optional t
72
-                    :documentation "Print a summary of the hours worked and the prices")  
72
+                    :documentation "Print a summary of the hours worked and the prices")
73 73
     (("help" #\h) :type boolean :optional t :documentation "show help")))
74 74
 
75 75
 (defparameter *version* "0:1")
... ...
@@ -90,30 +90,33 @@
90 90
   (let ((clients (make-hash-table))
91 91
         (total-cost 0))
92 92
 
93
-    (flet ((record-client (client hours)
94
-             (let ((client (make-keyword (string-upcase client))))
95
-               (incf (gethash client clients 0) hours))))
96
-      (format t "~&~:{~4a ~10<~:(~a~)~> ~7,2F hrs  ~a~%~}" results)
93
+    (labels ((record-client (client hours)
94
+               (let ((client (make-keyword (string-upcase client))))
95
+                 (incf (gethash client clients 0) hours)))
96
+             (total-line (results)
97
+               (format nil "~26<Total~>:~7,2F hours @ ~7,2F $/hr = $~7,2F"
98
+                       (loop for (_ client time __) in results
99
+                             do (progn _ __)
100
+                             sum time
101
+                             do (record-client client time)
102
+                             do (incf total-cost (* time *rate*)))
103
+                       *rate*
104
+                       total-cost))
105
+             (fix-assoc (alist)
106
+               (mapcar (destructuring-lambda ((client . time))
107
+                         (list client time *rate* (* time *rate*)))
108
+                       alist)))
109
+
110
+      (format t "~&~:{~4a ~10<~:(~a~)~> ~7,2F hrs ~a~%~}" results)
97 111
       (when status
98 112
         (format t "~120,1,0,'-<~>")
99
-        (let ((total (format nil "~26<Total~>:~7,2F hours @ ~7,2F $/hr = $~7,2F"
100
-                             (loop for (_ client time __) in results
101
-                                   do (progn _ __)
102
-                                   sum time
103
-                                   do (record-client client time)
104
-                                   do (incf total-cost (* time *rate*)))
105
-                             *rate*
106
-                             total-cost)))
107
-          (flet ((fix-assoc (alist)
108
-                   (mapcar (destructuring-lambda ((client . time))
109
-                             (list client time *rate* (* time *rate*)))
110
-                           alist)))
111
-            (format t "~&~:{~:(~26<~a~>~):~7,2F hours @ ~7,2F $/hr = $~7,2F~%~}"
112
-                    (stable-sort
113
-                      (fix-assoc (hash-table-alist clients))
114
-                      #'string<
115
-                      :key (alambda (car it)))))
116
-          (format t total))))))   
113
+        (let ((total (total-line results)))
114
+          (format t "~&~:{~:(~26<~a~>~):~7,2F hours @ ~7,2F $/hr = $~7,2F~%~}"
115
+                  (stable-sort
116
+                    (fix-assoc (hash-table-alist clients))
117
+                    #'string<
118
+                    :key (alambda (car it))))
119
+          (format t total))))))
117 120
 
118 121
 (defun pprint-log (args &key client reverse status help)
119 122
   (when help