git.fiddlerwoaroof.com
Browse code

Simplifying architecture and rewriting

fiddlerwoaroof authored on 25/09/2015 21:29:39
Showing 10 changed files
... ...
@@ -1,4 +1,5 @@
1 1
 (ql:quickload :parenscript)
2
+(ql:quickload :cl-markup)
2 3
 
3 4
 (defpackage :angular
4 5
   (:use :parenscript :cl)
... ...
@@ -20,12 +21,6 @@
20 21
 (defmacro+ps scope-var (name value)
21 22
   `(setf ($s ,name) ,value))
22 23
 
23
-(defmacro+ps resource (name url params &body body)
24
-  `(var ,name
25
-        ($resource ,url ,(cons 'create params)
26
-                   ,(cons 'create (loop for (name form) in body
27
-                                         append (list name (cons 'create form)))))))
28
-
29 24
 (defmacro+ps scope-function (name arguments &body body)
30 25
   `(scope-var ,name (lambda ,arguments
31 26
                       ,@body)))
... ...
@@ -37,18 +32,49 @@
37 32
          (lambda ,(loop for x in dependencies
38 33
                         collect x)
39 34
            ,@body
40
-           nil)))
35
+           )))
41 36
 
42 37
 
43 38
 ; This is just for slimv's sake
44 39
 (defmacro defcontroller (name dependencies &body body)
45 40
   (declare (ignore name dependencies body)))
46 41
 
47
-(defpsmacro def-module (module-name dependencies &body body)
48
-  `(macrolet ((defcontroller (name dependencies &body body)
42
+
43
+(defmacro+ps resource (name url params &body body)
44
+  `(var ,name
45
+        ($resource ,url ,(cons 'create params)
46
+                   ,(cons 'create (loop for (name form) in body
47
+                                         append (list name (cons 'create form)))))))
48
+
49
+
50
+(defpsmacro defdirective (module-name name dependencies &body body)
51
+  (let ((dependencies (cons '$scope dependencies)))
52
+    `(chain ,module-name
53
+            (directive ,(symbol-to-js-string name)
54
+                       ,(build-lambda dependencies
55
+                                      `((create
56
+                                          ,@body)))))))
57
+(defpsmacro markup (&body body)
58
+  `(lisp (cl-markup:markup ,@body)))
59
+
60
+(defun def-module-function (module-name block-name function-name)
61
+  `(,block-name (name dependencies &body body)
62
+                (let ((dependencies (cons '$scope dependencies)))
63
+                  `(chain ,',module-name
64
+                          (,',function-name ,(symbol-to-js-string name)
65
+                                            ,(build-lambda dependencies body))))))
66
+
67
+(defmacro+ps defmodule (module-name dependencies &body body)
68
+  `(macrolet ((defdirective (name dependencies &body body)
49 69
                 (let ((dependencies (cons '$scope dependencies)))
50
-                  (list 'chain ',module-name `(controller ,(symbol-to-js-string name)
51
-                                                          ,(build-lambda dependencies body))))))
70
+                  `(chain ,',module-name
71
+                          (directive ,(symbol-to-js-string name)
72
+                                     ,(build-lambda dependencies
73
+                                                    `((create
74
+                                                        ,@body)))))))
75
+
76
+              ,(def-module-function module-name 'defservice 'service)
77
+              ,(def-module-function module-name 'defcontroller 'controller))
52 78
      (progn (var ,module-name ((@ angular module) ,(symbol-to-js-string module-name) ,dependencies))
53 79
             ,@body)))
54 80
 
55 81
new file mode 100644
... ...
@@ -0,0 +1,54 @@
1
+(in-package :whitespace)
2
+(defun base-template-f (&optional demo)
3
+  (cl-markup:xhtml5
4
+    (:head
5
+      (:title "Whitespace")
6
+      (:script :src "https://code.jquery.com/jquery-2.1.4.min.js" :type "text/javascript" "")
7
+      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular.js" :type "text/javascript" "")
8
+      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular-resource.js" :type "text/javascript" "")
9
+      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular-sanitize.js" :type "text/javascript" "")
10
+      (:script :src "/static/js/fold.js" :type "text/javascript" "")
11
+      (:script :src "/static/js/whitespace-angular.js" :type "text/javascript" "")
12
+      (:link :rel "stylesheet" :href "/static/css/reset.css")
13
+      (:link :rel "stylesheet" :href "/static/css/baseline_post.css")
14
+      (:link :rel "stylesheet" :href "/static/css/formalize.css")
15
+      (:link :rel "stylesheet" :href "/static/css/main.css")
16
+      (:link :rel "stylesheet" :href "/static/css/content.css")
17
+      (:link :rel "stylesheet" :href "/theme/light.css")
18
+      (:link :rel "icon" :href "/static/images/Whitespace_favicon.png" :type "image/x-icon")
19
+      (:link :rel "shortcut icon" :href "/static/images/Whitespace_favicon.png" :type "image/x-icon"))
20
+
21
+    (:body :ng-app "whitespace" :ng-controller "MainCtrl"
22
+     (:header
23
+       (:button :class "flip-button" "…")
24
+       (:h1 "Whitespace")
25
+       )
26
+     (:section :id "content"
27
+      (:section :id "sidebar"
28
+       (:ul :class "menu" 
29
+        (:li :ng-repeat "feed in feeds.result"
30
+         (:a :ng-click "toggleClosed(feed)" "{{ feed.title }}"))))
31
+      (:main
32
+        (cl-markup:raw
33
+          (unless demo
34
+            (cl-markup:markup
35
+              (:form :name "add-form" :id "add-form" :ng-submit "addFeed()"
36
+               (:input :type "text" :name "url" :class "urltext" :ng-model "addForm.url" :placeholder "http://example.com/feed.rss . . ." "")
37
+               (:input :type "hidden" :name "api" :value "yes" "")
38
+               (:button :type "submit" :class "fsub" "+")))))
39
+        (:img :ng-class "{spinner: true, hide: feeds.result !== undefined}" :src "/static/images/spinner.gif" "")
40
+        (:div :class "hide" :ng-class "{hide: feeds.result === undefined}"
41
+         (:section :ng-class "{feed: true, closed: !feed.closed}" :ng-repeat "feed in feeds.result"
42
+          (:section :class "feed-header" :ng-click "toggleClosed(feed)"
43
+           (:h2 "{{ feed.title }}")
44
+           (:h3 "{{ feed.description }}"))
45
+          (:ul :class "post-list"
46
+           (:li :ng-class "{link: true, closed: !item.closed}" :ng-repeat "item in feed.items"
47
+            (:section :class "link-header" :ng-click "toggleClosed(item)"
48
+             (:h4 "{{item.title}}")
49
+             (:p :class "link-info"
50
+              (:a :target "_blank" :ng-href "{{item.link}}" :class "link-url" "{{item.link}}")
51
+              (:span :class "link-date" "{{item.date}}")))
52
+            (:section :class "link-content"
53
+             (:div :ng-bind-html "renderHtml(item.description)" ""))))))))
54
+     (:footer))))
... ...
@@ -54,8 +54,8 @@
54 54
 (sheeple:defreply get-user-info ((a =endpoint-schema=) (b sheeple:=string=)))
55 55
 (sheeple:defreply get-access-token ((a =endpoint-schema=) (b sheeple:=string=)))
56 56
 
57
-(defparameter *FBOOK-INFO* (sheeple:clone =service-info=))
58
-(defparameter *GOOG-INFO* (sheeple:clone =service-info=))
57
+(defparameter *fbook-info* (sheeple:clone =service-info=))
58
+(defparameter *goog-info* (sheeple:clone =service-info=))
59 59
 (defparameter *endpoint-schema* nil)
60 60
 ; goog is well behaved
61 61
 (defparameter *goog-endpoint-schema* (defobject (=endpoint-schema= *goog-info*)))
... ...
@@ -212,89 +212,112 @@
212 212
                                          ("access_token" . ,access-token))
213 213
                            ))))
214 214
 
215
+(defun google-login-entry (params)
216
+  (declare (ignore params))
217
+  (with-context-variables (session)
218
+    (let ((state (gen-state 36)))
219
+      (setf (gethash :state session) state)
220
+      (with-endpoints *goog-endpoint-schema*
221
+        (setf (gethash :endpoint-schema session) *goog-endpoint-schema*)
222
+        (multiple-value-bind (content rcode headers) (do-auth-request *goog-endpoint-schema* state)
223
+          (if (< rcode 400)
224
+            `(302 (:location ,(cdr (assoc :location headers))))
225
+            content))))))
226
+
227
+(defun facebook-login-entry (params)
228
+  (declare (ignore params))
229
+  (let ((session (ningle:context :session))
230
+        (state (gen-state 36)))
231
+      (setf (gethash :state session) state)
232
+      (with-endpoints *fbook-endpoint-schema*
233
+        (setf (gethash :endpoint-schema session) *fbook-endpoint-schema*)
234
+        (multiple-value-bind (content rcode headers uri) (do-auth-request *fbook-endpoint-schema* state)
235
+          (declare (ignore headers))
236
+          (if (< rcode 400)
237
+            `(302 (:location ,(format nil "~a" uri)))
238
+            content)))))
239
+
240
+(defun google-callback (login-callback)
241
+  (lambda (params)
242
+    (let ((received-state (cdr (string-assoc "state" params)))
243
+          (code (cdr (string-assoc "code" params))))
244
+      (check-state received-state
245
+                   (with-context-variables (session)
246
+                     (with-endpoints *goog-endpoint-schema*
247
+                       (let* ((a-t (get-access-token *goog-endpoint-schema* code))
248
+                              (access-token (assoc-cdr :access--token a-t)) ;; Argh
249
+                              (id-token (assoc-cdr :id--token a-t))
250
+                              (decoded (cljwt:decode id-token :fail-if-unsupported nil))
251
+                              (user-info (get-user-info *goog-endpoint-schema* access-token)))
252
+                         (setf (gethash :idtoken session) id-token
253
+                               (gethash :accesstoken session) access-token
254
+                               (gethash :userinfo session) user-info
255
+                               (gethash :app-user session) (funcall login-callback
256
+                                                                    user-info
257
+                                                                    decoded
258
+                                                                    access-token))
259
+                         '(302 (:location "/"))
260
+                         )))
261
+                   '(403 '() "Out, vile imposter!")))))
262
+
263
+(defmacro setup-session ((session) &rest rest &key nonsense &allow-other-keys)
264
+  (declare (ignorable nonsense))
265
+  (cons 'progn
266
+        (iterate:iterate (iterate:for key   in rest       by #'cddr )
267
+                         (iterate:for value in (cdr rest) by #'cddr)
268
+                         (iterate:collect `(setf (gethash ,(alexandria:make-keyword (key)) ,session) ,value)))))
269
+
270
+(defun vars-to-symbol-macrolets (vars obj)
271
+  (iterate:iterate (iterate:for var in vars)
272
+                   (iterate:collect `(,var (gethash ,(alexandria:make-keyword var) ,obj)))))
273
+
274
+(defmacro with-session-values (vars session &body body)
275
+  (alexandria:once-only (session)
276
+    `(symbol-macrolet ,(vars-to-symbol-macrolets vars session)
277
+       ,@body)))
215 278
 
216
-(defun oauth2-login-middleware (&key google-info facebook-info (login-callback #'identity))
217
-  (lambda (app)
218
-    ;(in-package :cl-oid-connect)
219
-    (load-facebook-info facebook-info)
220
-    (load-goog-endpoint-schema)
221
-    (load-google-info google-info)
222
-
223
-    (def-route ("/login/google" (params) :app app)
224
-      (with-context-variables (session)
225
-        (let ((state (gen-state 36)))
226
-          (setf (gethash :state session) state)
227
-          (with-endpoints *goog-endpoint-schema*
228
-            (setf (gethash :endpoint-schema session) *goog-endpoint-schema*)
229
-            (multiple-value-bind (content rcode headers) (do-auth-request *goog-endpoint-schema* state)
230
-              (if (< rcode 400)
231
-                `(302 (:location ,(cdr (assoc :location headers))))
232
-                content))))))
233
-
234
-
235
-    (def-route ("/login/facebook" (params) :app app)
236
-      (let ((session (ningle:context :session)))
237
-        (let ((state (gen-state 36)))
238
-          (setf (gethash :state session) state)
239
-          (with-endpoints *fbook-endpoint-schema*
240
-            (setf (gethash :endpoint-schema session) *fbook-endpoint-schema*)
241
-            (multiple-value-bind (content rcode headers uri) (do-auth-request *fbook-endpoint-schema* state)
242
-              (declare (ignore headers))
243
-              (if (< rcode 400)
244
-                `(302 (:location ,(format nil "~a" uri)))
245
-                content))))))
246
-
247
-    (def-route ("/oidc_callback/google" (params) :app app)
248
-      (let ((received-state (cdr (string-assoc "state" params)))
249
-            (code (cdr (string-assoc "code" params))))
279
+(defun facebook-callback (login-callback)
280
+  (lambda (params)
281
+    (let ((received-state (cdr (string-assoc "state" params)))
282
+          (code (cdr (string-assoc "code" params))))
283
+      (with-endpoints *fbook-endpoint-schema*
250 284
         (check-state received-state
251
-                     (with-context-variables (session)
252
-                       (with-endpoints *goog-endpoint-schema*
253
-                         (let* ((a-t (get-access-token *goog-endpoint-schema* code))
254
-                                (access-token (assoc-cdr :access--token a-t)) ;; Argh
255
-                                (id-token (assoc-cdr :id--token a-t))
256
-                                (decoded (cljwt:decode id-token :fail-if-unsupported nil))
257
-                                (user-info (get-user-info *goog-endpoint-schema* access-token)))
258
-                           (setf (gethash :idtoken session) id-token)
259
-                           (setf (gethash :accesstoken session) access-token)
260
-                           (setf (gethash :userinfo session) user-info)
261
-                           (setf (gethash :app-user session)
262
-                                 (funcall login-callback user-info decoded access-token))
263
-                           '(302 (:location "/"))
264
-                           )))
265
-                     '(403 '() "Out, vile imposter!"))))
266
-
267
-
268
-    (def-route ("/oidc_callback/facebook" (params) :app app)
269
-      (let ((received-state (cdr (string-assoc "state" params)))
270
-            (code (cdr (string-assoc "code" params))))
271
-        (with-endpoints *fbook-endpoint-schema*
272
-          (check-state received-state
273
-                       (with-context-variables (session)
274
-                         (let* ((a-t (get-access-token *fbook-endpoint-schema* code))
275
-                                (id-token (assoc-cdr :access--token a-t))
276
-                                (user-info (get-user-info *fbook-endpoint-schema* id-token)))
277
-                           (setf (gethash :accesstoken session) a-t)
278
-                           (setf (gethash :userinfo session) user-info)
279
-                           (setf (gethash :idtoken session) id-token)
280
-                           (setf (gethash :app-user session) (funcall login-callback user-info id-token a-t))
281
-
282
-                           '(302 (:location "/"))))
283
-                       '(403 '() "Out, vile imposter!")))))
284
-
285
-    (def-route ("/userinfo.json" (params) :app app)
286
-      (with-context-variables (session)
287
-        (require-login
288
-          (with-endpoints  (gethash :endpoint-schema session)
289
-            (cl-json:encode-json-to-string (gethash :userinfo session))))))
290
-
291
-    (def-route ("/logout" (params) :app app)
292
-      (with-context-variables (session)
293
-        (setf (gethash :userinfo session) nil)
294
-        '(302 (:location "/"))))
295
-
296
-    app))
297
-
285
+                     (let* ((a-t (get-access-token *fbook-endpoint-schema* code))
286
+                            (id-token (assoc-cdr :access--token a-t))
287
+                            (user-info (get-user-info *fbook-endpoint-schema* id-token)))
288
+                       (with-session-values (accesstoken userinfo idtoken app-user) (context :session)
289
+                                            (setf accesstoken a-t
290
+                                                  userinfo user-info
291
+                                                  idtoken id-token
292
+                                                  app-user (funcall login-callback user-info id-token a-t)))
293
+
294
+                         '(302 (:location "/")))
295
+                     '(403 '() "Out, vile imposter!"))))))
296
+
297
+(defun userinfo-route (params)
298
+  (declare (ignore params))
299
+  (with-context-variables (session)
300
+    (require-login
301
+      (with-endpoints  (gethash :endpoint-schema session)
302
+        (cl-json:encode-json-to-string (gethash :userinfo session))))))
303
+
304
+(defun logout-route (params)
305
+  (declare (ignore params))
306
+  (with-context-variables (session)
307
+    (setf (gethash :userinfo session) nil)
308
+    '(302 (:location "/"))))
309
+
310
+(defun oauth2-login-middleware (app &key google-info facebook-info (login-callback #'identity))
311
+  (load-facebook-info facebook-info)
312
+  (load-goog-endpoint-schema)
313
+  (load-google-info google-info)
314
+  (setf (route app "/userinfo.json" :method :get) #'userinfo-route
315
+        (route app "/logout"  :method :get) #'logout-route
316
+        (route app "/login/google" :method :get) #'google-login-entry
317
+        (route app "/login/facebook" :method :get) #'facebook-login-entry 
318
+        (route app "/oidc_callback/google" :method :get) (google-callback login-callback)
319
+        (route app "/oidc_callback/facebook" :method :get) (facebook-callback login-callback))
320
+  (lambda (app) (lambda (env) (funcall app env))))
298 321
 
299 322
 (defmacro redirect-if-necessary (sessionvar &body body)
300 323
   (with-gensyms (session)
... ...
@@ -23,13 +23,6 @@
23 23
   (:use #:cl))
24 24
 (load "utils.lisp")
25 25
 
26
-(defpackage :whitespace.tables
27
-  (:use #:cl #:alexandria #:postmodern #:annot.class))
28
-(load "tables.lisp")
29
-
30
-(defpackage :whitespace.rss
31
-  (:use #:cl #:alexandria #:postmodern #:lquery #:cl-syntax #:cl-annot.syntax #:cl-annot.class
32
-        #:whitespace.tables #:iterate))
33 26
 (load "rss.lisp")
34 27
 
35 28
 (defpackage :whitespace
... ...
@@ -124,56 +117,7 @@
124 117
                              :href (format nil "#feed-~a" feed-count)
125 118
                              (rss-feed-title feed))))))))
126 119
 
127
-(defun base-template-f (&optional demo)
128
-  (cl-markup:xhtml5
129
-    (:head
130
-      (:title "Whitespace")
131
-      (:script :src "https://code.jquery.com/jquery-2.1.4.min.js" :type "text/javascript" "")
132
-      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular.js" :type "text/javascript" "")
133
-      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular-resource.js" :type "text/javascript" "")
134
-      (:script :src "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular-sanitize.js" :type "text/javascript" "")
135
-      (:script :src "/static/js/fold.js" :type "text/javascript" "")
136
-      (:script :src "/static/js/whitespace-angular.js" :type "text/javascript" "")
137
-      (:link :rel "stylesheet" :href "/static/css/reset.css")
138
-      (:link :rel "stylesheet" :href "/static/css/baseline_post.css")
139
-      (:link :rel "stylesheet" :href "/static/css/formalize.css")
140
-      (:link :rel "stylesheet" :href "/static/css/main.css")
141
-      (:link :rel "stylesheet" :href "/static/css/content.css")
142
-      (:link :rel "stylesheet" :href "/theme/light.css")
143
-      (:link :rel "icon" :href "/static/images/Whitespace_favicon.png" :type "image/x-icon")
144
-      (:link :rel "shortcut icon" :href "/static/images/Whitespace_favicon.png" :type "image/x-icon"))
145
-
146
-    (:body :ng-app "whitespace" :ng-controller "MainCtrl"
147
-     (:header
148
-       (:button :class "flip-button" "…")
149
-       (:h1 "Whitespace")
150
-       )
151
-     (:section :id "content"
152
-      (:section :id "sidebar"
153
-       (cl-markup:raw (feedlist-markup *feeds*)))
154
-      (:main
155
-        (cl-markup:raw
156
-          (unless demo
157
-            (cl-markup:markup
158
-              (:form :name "add-form" :id "add-form" :ng-submit "addFeed()"
159
-               (:input :type "text" :name "url" :class "urltext" :ng-model "addForm.url" :placeholder "http://example.com/feed.rss . . ." "")
160
-               (:input :type "hidden" :name "api" :value "yes" "")
161
-               (:button :type "submit" :class "fsub" "+")))))
162
-        (:img :ng-class "{spinner: true, hide: feeds.result !== undefined}" :src "/static/images/spinner.gif" "")
163
-        (:section :ng-class "{feed: true, closed: !feed.closed}" :ng-repeat "feed in feeds.result"
164
-         (:section :class "feed-header" :ng-click "toggleClosed(feed)"
165
-          (:h2 "{{ feed.title }}")
166
-          (:h3 "{{ feed.description }}"))
167
-         (:ul :class "post-list"
168
-          (:li :ng-class "{link: true, closed: !item.closed}" :ng-repeat "item in feed.items"
169
-           (:section :class "link-header" :ng-click "toggleClosed(item)"
170
-            (:h4 "{{item.title}}")
171
-            (:p :class "link-info"
172
-             (:a :target "_blank" :ng-href "{{item.link}}" :class "link-url" "{{item.link}}")
173
-             (:span :class "link-date" "{{item.date}}")))
174
-           (:section :class "link-content"
175
-            (:div :ng-bind-html "renderHtml(item.description)" "")))))))
176
-      (:footer))))
120
+(load "base-template.lisp")
177 121
 
178 122
 (defmacro defun-from-value (name value)
179 123
   `(setf (symbol-function ',name) ,value))
... ...
@@ -190,7 +134,7 @@
190 134
 ; ; ;  Ultimately, this will only serialize the feed if the client
191 135
 (cl-oid-connect:def-route ("/feeds/add" (params) :method :post :app *app*)
192 136
   (ningle.context:with-context-variables (session) 
193
-    (let ((user-info (car (gethash :app-user session)))
137
+    (let ((user-info (gethash :app-user session))
194 138
           (result '(302 (:location "/")))
195 139
           (api (string= (cl-oid-connect:assoc-cdr "api" params 'string=) "yes")) 
196 140
           (url (cl-oid-connect:assoc-cdr "url" params 'string=)) 
... ...
@@ -199,10 +143,11 @@
199 143
         (when (neither-null params user-info)
200 144
           (handler-case
201 145
             (let* ((doc (plump:parse (drakma:http-request url)))
202
-                   (uid (slot-value user-info 'id))
203
-                   (added-feed (store-feed doc uid)))
204
-              (when api
205
-                (setf result `(200 (:Content-Type "application/json") ,(jsonapi-encoder t added-feed)))))
146
+                   (uid (slot-value user-info 'id)))
147
+              (multiple-value-bind (added-feed dao-feed) (store-feed doc) 
148
+                (subscribe-to-feed uid (slot-value dao-feed 'id))
149
+                (when api
150
+                  (setf result `(200 (:Content-Type "application/json") ,(jsonapi-encoder t added-feed))))))
206 151
             (cl-postgres-error:unique-violation
207 152
               ()
208 153
               (when api
... ...
@@ -235,8 +180,7 @@
235 180
     (let* ((received-id (anaphora:aif (cl-oid-connect:assoc-cdr :id userinfo)
236 181
                           anaphora:it
237 182
                           (cl-oid-connect:assoc-cdr :sub userinfo)))
238
-           (db-user (postmodern:query-dao 'reader_user (:select :* :from 'reader_user
239
-                                                        :where (:= :foreign-id received-id)))))
183
+           (db-user (car (postmodern:select-dao 'reader_user (:= :foreign-id received-id)))))
240 184
       (if (not (null db-user))
241 185
         db-user
242 186
         (progn
... ...
@@ -290,7 +234,6 @@
290 234
                    `(* :color ,(colors:colorscheme-fg *colorscheme*))
291 235
 
292 236
                    `(body
293
-                      :transition "background-color 0.25s ease"
294 237
                       :background-color ,(colors:colorscheme-bg *colorscheme*))
295 238
 
296 239
                    `((:or h1 h2 h3)
... ...
@@ -314,12 +257,13 @@
314 257
                        :font-size ,(combine-unit-q (* 0.25 header-height) height-units)))
315 258
 
316 259
                    `(main
317
-                      :border-left thin solid ,(colors:colorscheme-accent *colorscheme*)
260
+                      :border-left medium solid ,(colors:colorscheme-accent *colorscheme*)
318 261
                       :height ,(combine-unit-q (- 100 header-height) height-units)
319
-                      (.add-form
262
+                      ("#add-form"
263
+                        :box-shadow "0em" "0em" "0.2em" "0.2em" ,(colors:colorscheme-accent *colorscheme*)
320 264
                         ((:or input button)
321
-                          :background-color ,(colors:colorscheme-fg *colorscheme*)
322
-                          :color ,(colors:colorscheme-bg *colorscheme*))
265
+                         :background-color ,(colors:colorscheme-bg *colorscheme*)
266
+                         :color ,(colors:colorscheme-fg *colorscheme*))
323 267
                         )
324 268
                       )
325 269
 
... ...
@@ -379,6 +323,13 @@
379 323
 
380 324
 (ql:quickload :clack-middleware-postmodern)
381 325
 
326
+(defparameter oid-mw
327
+  (cl-oid-connect:oauth2-login-middleware
328
+    *app*
329
+    :facebook-info (truename "~/github_repos/cl-oid-connect/facebook-secrets.json")
330
+    :google-info (truename "~/github_repos/cl-oid-connect/google-secrets.json")
331
+    :login-callback #'login-callback))
332
+
382 333
 (defun start (&optional tmp)
383 334
   (let ((server (if (> (length tmp) 1)
384 335
                   (intern (string-upcase (elt tmp 1)) 'keyword)
... ...
@@ -392,12 +343,13 @@
392 343
                               (postmodern:with-connection *db-connection-info*
393 344
                                 (funcall app env))))
394 345
               (:static :path "/static/" :root #p"./static/")
395
-              (funcall
396
-                (cl-oid-connect:oauth2-login-middleware
397
-                  :facebook-info (truename "~/github_repos/cl-oid-connect/facebook-secrets.json")
398
-                  :google-info (truename "~/github_repos/cl-oid-connect/google-secrets.json")
399
-                  :login-callback #'login-callback)
400
-                *app*)) :port 9090 :server server)
346
+              *app*) :port 9090 :server server)
401 347
           *handler*)))
402 348
 
349
+
350
+(defun restart-clack ()
351
+  (do () ((null *handler*))
352
+    (stop))
353
+  (start))
354
+
403 355
 ; vim: foldmethod=marker foldmarker=(,) foldminlines=3 :
... ...
@@ -1,9 +1,9 @@
1 1
 (:import "url(https://fonts.googleapis.com/css?family=Lato:400,100,300,400italic,300italic,700,700italic,900&subset=latin,latin-ext)")
2 2
 (:import "url(https://fonts.googleapis.com/css?family=Caudex)")
3 3
 
4
-((:or .feed main)
5
-  :-webkit-backface-visibility hidden;
6
-  :-webkit-transform "translateZ(0)")
4
+;((:or .feed main)
5
+;  :-webkit-backface-visibility hidden;
6
+;  :-webkit-transform "translateZ(0)")
7 7
 
8 8
 (*
9 9
   :box-sizing "border-box"
... ...
@@ -11,6 +11,7 @@
11 11
   :padding "0px")
12 12
 
13 13
 (body
14
+  :transition "background-color .5s ease"
14 15
   :font-family "Lato")
15 16
 
16 17
 (((:or main "#sidebar" .feed) > (:or ul ol))
... ...
@@ -51,9 +52,8 @@
51 52
   (ul.menu
52 53
     :text-align right
53 54
     :font-variant small-caps
54
-    :opacity 0
55 55
     (li
56
-      :transition "all 0.25s ease"
56
+      :transition "all .5s ease"
57 57
       (a
58 58
         :color inherit
59 59
         :display block
... ...
@@ -62,17 +62,21 @@
62 62
         :padding "0.5em"
63 63
         :text-decoration none
64 64
         :width "100%"
65
-        )))
66
-  (ul.menu.open
67
-    :opacity 1))
65
+        ))))
68 66
 
69 67
 (main
70 68
   :width "62vw"
71 69
   :float "right"
72 70
   :clear "right"
73
-  :overflow-x "hidden"
74
-  :overflow-y "scroll"
75
-  :position "relative")
71
+  :overflow "hidden"
72
+  :position "relative"
73
+  ((> div)
74
+   :position "absolute"
75
+   :top "2em"
76
+   :bottom "0em"
77
+   :width "100%"
78
+   :overflow-y "scroll")
79
+  )
76 80
 
77 81
 (img.spinner
78 82
   :position "absolute"
... ...
@@ -89,12 +93,12 @@
89 93
   ((:or h2 h3) :padding "0.62em")
90 94
   (h2 :padding-bottom "0.38em")
91 95
   (h3 :padding-top "0.38em")
92
-  :transition "background-color 0.25s ease"
96
+  :transition "background-color .5s ease"
93 97
   :padding-bottom "0em")
94 98
 
95 99
 ((.link.closed .link-content) 
96
-  :max-height "0px"
97
-  :padding "0em")  
100
+ :max-height "0px"
101
+ :padding "0em")  
98 102
 
99 103
 (.link.closed
100 104
   :padding-bottom "0em")
... ...
@@ -108,7 +112,7 @@
108 112
     :padding "1em"
109 113
     :padding-bottom "0em"
110 114
     :cursor pointer
111
-    :transition "background-color 0.25s ease"
115
+    :transition "background-color .5s ease"
112 116
     (h4
113 117
       :margin-bottom "0.5em"
114 118
       :display "inline-block")) 
... ...
@@ -129,7 +133,7 @@
129 133
    :clear both)
130 134
   (.link-content
131 135
     :overflow hidden
132
-    
136
+
133 137
     (img
134 138
       :margin "1em" "0%" "1em" "10%"
135 139
       :max-width "80%"
... ...
@@ -157,27 +161,34 @@
157 161
     :transition "max-height 0.5s ease"))
158 162
 
159 163
 ("#add-form"
160
-  :font-size "1em"
161
-  :height "1.8em"
162
-  :display block
163
-  :overflow hidden
164
-  :width "100%"
165
-
166
-  (input
167
-    :border "none"
168
-    :width "95%"
169
-    :height "100%"
170
-    :font-size "0.8em"
171
-    :line-height "1.8em"
172
-    :padding "1em" "0.5em"
173
-    )
174
-  (button
175
-    :border none
176
-    :border-radius 0px
177
-    :font-size inherit
178
-    :width "5%" 
179
-    :height "100%"
180
-    ))
164
+ :transition "background-color .5s ease"
165
+ :font-size "1em"
166
+ :height "1.8em"
167
+ :display block
168
+ :overflow hidden
169
+ :width "100%"
170
+ :position absolute
171
+ :z-index 3
172
+
173
+ ((:or input button)
174
+  :transition "background-color .5s ease"
175
+  )
176
+
177
+ (input
178
+   :border "none"
179
+   :width "95%"
180
+   :height "100%"
181
+   :font-size "0.8em"
182
+   :line-height "1.8em"
183
+   :padding "1em" "0.5em"
184
+   )
185
+ (button
186
+   :border none
187
+   :border-radius 0px
188
+   :font-size inherit
189
+   :width "5%" 
190
+   :height "100%"
191
+   ))
181 192
 
182 193
 
183 194
 (.flip-button
... ...
@@ -1,7 +1,7 @@
1 1
 (in-package :angular)
2 2
 
3 3
 ;:action "/feeds/add?api=yes" :name "add-form" :id "add-form" :method "post" 
4
-(def-module whitespace '(ng-resource ng-sanitize)
4
+(defmodule whitespace '(ng-resource ng-sanitize)
5 5
   (defcontroller -Main-Ctrl ($http $resource $sce)
6 6
     (resource feeds "/feeds" nil
7 7
               (json (method "GET" url "/feeds/json"))
... ...
@@ -1,12 +1,48 @@
1
-;(in-package :cl-user)
2
-;(defpackage :whitespace.rss
3
-;  (:shadow "to-json")
4
-;  (:use #:cl #:alexandria #:postmodern #:lquery #:cl-syntax #:cl-annot.syntax #:cl-annot.class
5
-;        #:whitespace.tables #:iterate))
1
+(in-package :cl-user)
2
+
3
+(load "tables.lisp")
4
+
5
+(defpackage :whitespace.rss
6
+  (:use #:cl #:alexandria #:postmodern #:lquery #:cl-syntax #:cl-annot.syntax #:cl-annot.class
7
+        #:whitespace.tables #:iterate)
8
+  (:import-from anaphora it))
9
+
6 10
 
7 11
 (in-package :whitespace.rss)
8 12
 (cl-annot.syntax:enable-annot-syntax)
9 13
 
14
+(defun ensure-mapping (list)
15
+  "Make sure that each item of the list is a pair of symbols"
16
+  (mapcar (lambda (x) (if (symbolp x) (list x x) x)) list))
17
+
18
+(defun alist-string-hash-table (alist)
19
+  (alexandria:alist-hash-table alist :test #'string=))
20
+
21
+(defun transform-alist (pair-transform alist)
22
+  (iterate (for (k . v) in-sequence alist)
23
+           (collect
24
+             (funcall pair-transform k v))))
25
+
26
+(defun %json-pair-transform (k v)
27
+  (cons (make-keyword (string-downcase k))
28
+        (typecase v
29
+          (string (coerce v 'simple-string))
30
+          (t v))))
31
+
32
+(defun %default-pair-transform (k v)
33
+  (cons (make-keyword (string-upcase k)) v))
34
+
35
+(defun make-pairs (symbols)
36
+  (cons 'list (iterate (for (key value) in symbols)
37
+                       (collect (list 'list* (symbol-name key) value)))))
38
+
39
+@export
40
+(defmacro copy-slots (slots from-v to-v)
41
+  (with-gensyms (from to)
42
+    `(let ((,from ,from-v) (,to ,to-v))
43
+       ,@(iterate (for (fro-slot to-slot) in (ensure-mapping slots))
44
+                  (collect `(setf (slot-value ,to ',to-slot) (slot-value ,from ',fro-slot))))
45
+       ,to)))
10 46
 
11 47
 @export
12 48
 (defmacro default-when (default test &body body)
... ...
@@ -15,6 +51,54 @@
15 51
            ,@body)
16 52
          ,default)))
17 53
 
54
+@export
55
+(defmacro get-elements (feed &optional (filter nil))
56
+  (let ((feed-sym (gensym))
57
+        (filter-lis `(lambda (x) (and (plump-dom:element-p x) ,@(loop for x in filter
58
+                                                                      collect `(funcall ,x x))))))
59
+    `(let ((,feed-sym ,feed))
60
+       (remove-if-not ,filter-lis (plump:children ,feed-sym)))))
61
+
62
+@export
63
+(defmacro get-elements-by-tagname (feed tagname)
64
+  `(get-elements ,feed ((lambda (x) (string= ,tagname (plump:tag-name x))))))
65
+
66
+@export
67
+(defmacro extract-text (selector &optional (default ""))
68
+  `(or (lquery:$ ,selector (text) (node)) ,default))
69
+
70
+(defmacro transform-result ((list-transform pair-transform) &body alist)
71
+    `(funcall ,list-transform
72
+              (transform-alist ,pair-transform
73
+                               ,@alist)))
74
+
75
+
76
+(defmacro slots-to-pairs (obj (&rest slots))
77
+  (alexandria:once-only (obj)
78
+    (let ((slots (ensure-mapping slots)))
79
+      `(with-slots ,(mapcar #'cadr slots) ,obj
80
+         ,(make-pairs slots)))))
81
+
82
+(defmacro defserializer ((specializes) &body slots)
83
+  (with-gensyms (obj o-t p-t)
84
+    `(defmethod serialize ((,obj ,specializes) &optional (,o-t #'identity) (,p-t #'%default-pair-transform))
85
+      (transform-result (,o-t ,p-t)
86
+        (slots-to-pairs ,obj ,slots)))))
87
+
88
+@export
89
+(defmacro xml-text-bind (syms &body body)
90
+  "Bind the symbols passed in the second arg to the text of the matching
91
+   elements in the document lquery has been initialized with and then run the
92
+   body in the resulting lexical scope.  This assumes that lquery:initialize
93
+   has already been passed the proper xml document"
94
+  `(let* ,(loop for sym in syms
95
+           collect `(,sym (or (lquery:$ ,(symbol-name sym) (text) (node)) "")))
96
+     ,@body))
97
+
98
+(defmacro make-instance-from-symbols (class &rest initargs)
99
+  `(make-instance ,class ,@(iterate (for (to from) in (ensure-mapping initargs))
100
+                                    (appending (list (make-keyword (symbol-name to)) from)))))
101
+
18 102
 @export-class
19 103
 (defclass rss-feed ()
20 104
   ((feed :accessor rss-feed-feed :initarg :feed)
... ...
@@ -22,7 +106,8 @@
22 106
    (title :accessor rss-feed-title :initarg :title)
23 107
    (link :accessor rss-feed-link :initarg :link)
24 108
    (description :accessor rss-feed-description :initarg :description)
25
-   (items :accessor rss-feed-items :initarg :items)))
109
+   (items :accessor rss-feed-items :initarg :items)
110
+   (fetch-url :accessor fetch-url :initarg :fetch-url)))
26 111
 
27 112
 @export-class
28 113
 (defclass rss-item ()
... ...
@@ -38,79 +123,131 @@
38 123
    (pub-date :accessor rss-item-pub-date :initarg :pub-date)
39 124
    (source :accessor rss-item-source  :initarg :source)))
40 125
 
41
-(load "tables.lisp")
126
+@export
127
+(defun make-rss-feed (feed)
128
+  (lquery:initialize feed)
129
+  (let* ((channel (lquery:$ "channel" (node)))
130
+         (fetch-url (lquery:$ "channel" (children) (tag-name "atom:link") (filter "[rel=self]") (attr :href) (node)))
131
+         (link (lquery:$ "channel > link" (text) (node)))
132
+         (link (if (string= link "") (lquery:$ "channel" (children) (tag-name "atom:link") (attr :href) (node)) link))
133
+         (items (lquery:$ "item")))
134
+    (xml-text-bind (title description)
135
+      (make-instance-from-symbols 'rss-feed
136
+                                  feed title link description channel fetch-url
137
+                                  (items (iterate (for it in-sequence items)
138
+                                                  (collecting (make-rss-item it))))))))
139
+
140
+@export
141
+(defgeneric serialize (cls &optional output-transform pair-transform))
42 142
 
43
-(defmethod jonathan:%to-json ((obj rss-feed)) (jonathan:%to-json (serialize obj
44
-                                                                            #'alexandria:alist-hash-table
45
-                                                                            #'%json-pair-transform)))
46
-(defmethod jonathan:%to-json ((obj rss-item)) (jonathan:%to-json (serialize obj
47
-                                                                            #'alexandria:alist-hash-table
48
-                                                                            #'%json-pair-transform)))
49 143
 
50
-(defun alist-string-hash-table (alist)
51
-  (alexandria:alist-hash-table alist :test #'string=))
144
+(defmethod serialize ((obj sequence) &optional (o-t #'identity) (p-t #'%default-pair-transform))
145
+  (iterate (for item in-sequence obj)
146
+           (collect (serialize item o-t p-t))))
52 147
 
53
-(defun transform-alist (pair-transform alist)
54
-  (iterate (for (k . v) in-sequence alist)
55
-           (collect
56
-             (funcall pair-transform k v))))
148
+; These are the interface I'm planning to remove as duplicate
149
+(defserializer (rss-feed)
150
+  title link description fetch-url
151
+  (items (iterate (for item in items)
152
+                  (collect item))))
57 153
 
58
-(defun %json-pair-transform (k v)
59
-  (cons (make-keyword k)
60
-        (typecase v
61
-          (string (coerce v 'simple-string))
62
-          (t v))))
154
+(defserializer (rss-item)
155
+  title link (description description-raw) guid pub-date source)
63 156
 
64
-(defun %default-pair-transform (k v)
65
-  (cons (make-keyword (string-upcase k)) v))
157
+; this is the interface to be used
158
+(defserializer (rss_feed_store)
159
+  title link description fetch-url)
160
+
161
+(defserializer (rss_item_store)
162
+  title link description fetch-url)
163
+
164
+(defmethod jonathan:%to-json ((obj rss-feed))
165
+  (jonathan:%to-json (serialize obj #'alexandria:alist-hash-table #'%json-pair-transform)))
166
+
167
+(defmethod jonathan:%to-json ((obj rss-item))
168
+  (jonathan:%to-json (serialize obj #'alexandria:alist-hash-table #'%json-pair-transform)))
169
+
170
+(defmacro get-id-for-object ((table key-column &optional (id-column :id)) key &body body)
171
+  "Anaphoric macro: binds id to the id it retrieves!"
172
+  (once-only (id-column key)
173
+    `(let ((id (anaphora:awhen (postmodern:query (:select ,id-column :from ',table :where (:= ',key-column ,key)))
174
+                 (caar it))))
175
+       ,@body)))
176
+
177
+(defgeneric get-dao-for (obj &optional link)
178
+  ; NOTE: this won't make dao objects for the _items_ when called on the feed!
179
+  ; also NOTE: this _prefers_ the passed object
180
+  (:method ((obj rss-feed) &optional linked-objects)
181
+   (declare (ignore linked-objects))
182
+   (with-slots (title link description fetch-url) obj
183
+     (get-id-for-object (rss_feed_store link) link
184
+       (make-instance-from-symbols 'rss_feed_store id title link description fetch-url (fetch-defaults t))) ))
185
+
186
+  (:method ((obj rss-item) &optional feed)
187
+   (with-slots (title link description-raw guid pub-date source) obj
188
+     (get-id-for-object (rss_item_store guid) guid
189
+       (make-instance-from-symbols 'rss_item_store id title link (description description-raw)
190
+                                   guid pub-date source feed (fetch-defaults t))))))
66 191
 
67 192
 @export
68
-(defgeneric serialize (cls &optional output-transform pair-transform)
69
-  (:method ((obj sequence) &optional (output-transform #'identity) (pair-transform #'%default-pair-transform))
70
-   (iterate (for item in-sequence obj)
71
-            (collect (serialize item output-transform pair-transform))))
193
+(defun get-feed-from-dao (rss-feed)
194
+  (let ((feed-dao (get-dao-for rss-feed)))
195
+    (list feed-dao
196
+          (with-slots (items) rss-feed
197
+            (iterate (for item in items)
198
+                     (collect (get-dao-for item (slot-value feed-dao 'id))))))))
72 199
 
73
-  (:method ((obj rss-feed) &optional (output-transform #'identity) (pair-transform #'%default-pair-transform))
74
-   (funcall output-transform
75
-     (transform-alist pair-transform
76
-                      `(("title" . ,(rss-feed-title obj))
77
-                        ("link" . ,(rss-feed-link obj))
78
-                        ("description" . ,(rss-feed-description obj))
79
-                        ("items" . ,(iterate (for item in-sequence (rss-feed-items obj))
80
-                                           (collect (serialize item output-transform pair-transform))))))))
81 200
 
82
-  (:method ((obj rss-item) &optional (output-transform #'identity) (pair-transform #'%default-pair-transform))
83
-   (funcall output-transform
84
-     (transform-alist pair-transform
85
-                      `(("title" . ,(rss-item-title obj))
86
-                        ("link" . ,(rss-item-link obj))
87
-                        ("description" . ,(rss-item-description-raw obj))
88
-                        ("guid" . ,(rss-item-guid obj))
89
-                        ("pub-date" . ,(rss-item-pub-date obj))
90
-                        ("source" . ,(rss-item-source obj)))))))
201
+@export
202
+(defun upsert-feed (rss-feed)
203
+  (postmodern:ensure-transaction
204
+    (destructuring-bind (feed items) (get-feed-from-dao rss-feed)
205
+      (postmodern:upsert-dao feed)
206
+      (mapcar #'postmodern:upsert-dao items))))
91 207
 
208
+; TODO: get rid of eval
92 209
 @export
93 210
 (defun store-feed-dao (serialized-rss-feed &optional link)
94 211
   (declare (ignore link))
95 212
   (let* ((items nil)
96
-         (rss_feed (eval `(postmodern:make-dao
97
-                            'rss_feed_store
98
-                            ,@(iterate (for (k . v) in-sequence serialized-rss-feed)
99
-                                       (if (eql k :items)
100
-                                         (setf items v)
101
-                                         (appending (list k v))))))))
213
+         (rss_feed (apply #'postmodern:make-dao
214
+                          (cons 'rss_feed_store
215
+                                (iterate (for (k . v) in-sequence serialized-rss-feed)
216
+                                         (if (eql k :items)
217
+                                           (setf items v)
218
+                                           (appending (list k v))))))))
102 219
     (iterate (for item in items)
103
-             (store-item-dao item (slot-value rss_feed 'id)))
220
+             (store-item-dao (serialize item)
221
+                             (slot-value rss_feed 'id)))
104 222
     rss_feed))
105 223
 
106 224
 @export
107
-(defun store-item-dao (rss-item link)
225
+(defun store-item-dao (serialized-rss-item link)
108 226
  (eval `(postmodern:make-dao
109 227
           'rss_item_store
110 228
           :feed ,link
111
-          ,@(iterate (for (k . v) in-sequence rss-item)
229
+          ,@(iterate (for (k . v) in-sequence serialized-rss-item)
112 230
                      (appending (list k v))))))
113 231
 
232
+(defun get-and-possibly-store-feed (rss-feed)
233
+  "Given an rss-feed, return the db's feed-id, persisting it if it doesn't already exist."
234
+  (postmodern:ensure-transaction
235
+    (anaphora:aif (postmodern:select-dao 'rss_feed_store (:= 'link (rss-feed-link rss-feed)))
236
+      (car anaphora:it) ;; The postmodern query returns a nested list
237
+      (store-feed-dao (serialize rss-feed)))))
238
+
239
+@export
240
+(defun store-feed (doc)
241
+  (postmodern:with-transaction ()
242
+    (let ((rss-feed (make-rss-feed doc)))
243
+      (values rss-feed
244
+              (get-and-possibly-store-feed rss-feed)))))
245
+
246
+@export ; TODO: this should eventually take a username/userobject rather than ids . . .
247
+(defun subscribe-to-feed (uid feedid)
248
+  (postmodern:query
249
+    (:insert-into 'subscriptions :set 'uid uid 'feedid feedid)))
250
+
114 251
 #|
115 252
 (:documentation
116 253
   "Store a serialized rss object into rhe database: the basic idea here is
... ...
@@ -118,14 +255,6 @@
118 255
    item and then we eval it.")
119 256
 |#
120 257
 
121
-@export
122
-(defmacro copy-slots (slots from-v to-v)
123
-   (with-gensyms (from to)
124
-     `(let ((,from ,from-v) (,to ,to-v))
125
-        ,@(loop for (fro-slot to-slot)
126
-                in (mapcar (lambda (x) (if (symbolp x) (list x x) x)) slots)
127
-                collect `(setf (slot-value ,to ',to-slot) (slot-value ,from ',fro-slot))))))
128
-
129 258
 @export
130 259
 (defun deserialize-item (item)
131 260
   (let ((result (make-instance 'rss-item)))
... ...
@@ -143,39 +272,23 @@
143 272
 @export
144 273
 (defun deserialize-feed (feed)
145 274
   (let ((result (make-instance 'rss-feed)))
146
-    (copy-slots (title link description) feed result)
275
+    (copy-slots (title link description fetch-url) feed result)
147 276
     (setf (rss-feed-items result) (deserialize-items (rfs-id feed)))
148 277
     result))
149 278
 
150 279
 @export
151 280
 (defun deserialize (&optional user-info)
152
-  (default-when #() (not (emptyp user-info))
281
+  (default-when #() (not (null user-info))
153 282
     (let ((feeds
154 283
             (postmodern:query-dao 'rss_feed_store
155
-                                  (:select 'rssfeed.*
156
-                                   :from 'rssfeed
157
-                                   :inner-join  'subscriptions :on (:= 'rssfeed.id  'subscriptions.feedid)
284
+                                  (:select 'rss_feed_store.*
285
+                                   :from 'rss_feed_store
286
+                                   :inner-join  'subscriptions :on (:= 'rss_feed_store.id  'subscriptions.feedid)
158 287
                                    :inner-join  'reader_user :on (:= 'reader_user.id  'subscriptions.uid)
159
-                                   :where (:= 'reader_user.foreign_id (user-foreign-id (car user-info)))))))
288
+                                   :where (:= 'reader_user.foreign_id (user-foreign-id user-info))))))
160 289
       (apply #'vector (loop for feed in feeds collect (deserialize-feed feed))))))
161 290
 
162 291
 
163
-@export
164
-(defmacro get-elements (feed &optional (filter nil))
165
-  (let ((feed-sym (gensym))
166
-        (filter-lis `(lambda (x) (and (plump-dom:element-p x) ,@(loop for x in filter
167
-                                                                      collect `(funcall ,x x))))))
168
-    `(let ((,feed-sym ,feed))
169
-       (remove-if-not ,filter-lis (plump:children ,feed-sym)))))
170
-
171
-@export
172
-(defmacro get-elements-by-tagname (feed tagname)
173
-  `(get-elements ,feed ((lambda (x) (string= ,tagname (plump:tag-name x))))))
174
-
175
-@export
176
-(defmacro extract-text (selector &optional (default ""))
177
-  `(or (lquery:$ ,selector (text) (node)) ,default))
178
-
179 292
 @export
180 293
 (defun normalize-html (html)
181 294
   (let ((plump-parser:*tag-dispatchers* plump:*html-tags*))
... ...
@@ -185,16 +298,6 @@
185 298
            html)
186 299
       ss)))
187 300
 
188
-@export
189
-(defmacro xml-text-bind (syms &body body)
190
-  "Bind the symbols passed in the second arg to the text of the matching
191
-   elements in the document lquery has been initialized with and then run the
192
-   body in the resulting lexical scope.  This assumes that lquery:initialize
193
-   has already been passed the proper xml document"
194
-  `(let* ,(loop for sym in syms
195
-           collect `(,sym (or (lquery:$ ,(symbol-name sym) (text) (node)) "")))
196
-     ,@body))
197
-
198 301
 @export
199 302
 (defun make-rss-item (item)
200 303
   (lquery:initialize item)
... ...
@@ -219,31 +322,12 @@
219 322
            ;(enclosure) --- TODO: implement comment / enclosure handling
220 323
 
221 324
       (xml-text-bind (title link guid pub-date source comments)
222
-        (make-instance 'rss-item :item item
223
-                       :title title :link link :description-raw description-raw :description description-munged
224
-                       :category category :guid guid :pub-date pub-date :source source :comments comments)))))
325
+        (make-instance-from-symbols 'rss-item
326
+                                    item title link description-raw (description description-munged)
327
+                                    category guid pub-date source comments)))))
225 328
       ;(setf (rss-item-enclosure result) enclosure)      -- TODO: comment/enclosure . . .
226 329
 
227
-@export
228
-(defun make-rss-feed (feed)
229
-  (lquery:initialize feed)
230
-  (let* ((channel (lquery:$ "channel" (node)))
231
-         (link (extract-text "link"))
232
-         (link (if (string= link "") (lquery:$ "channel" (children) (tag-name "atom:link") ()) link))
233
-         (items (lquery:$ "item")))
234
-    (xml-text-bind (title description)
235
-      (make-instance 'rss-feed :feed feed
236
-                     :title title :link link :description description
237
-                     :channel channel :items (loop for it across items
238
-                                                   collect (make-rss-item it))))))
239
-@export
240
-(defun store-feed (doc uid)
241
-  (postmodern:with-transaction ()
242
-    (let* ((rss-feed- (make-rss-feed doc))
243
-           (feedid (anaphora:aif (postmodern:query (:select 'id :from 'rssfeed
244
-                                                    :where (:= 'link (rss-feed-link rss-feed-))))
245
-                     (caar anaphora:it) ;; The postmodern query returns a nested list
246
-                     (slot-value (store-feed-dao (serialize rss-feed-)) 'id))))
247
-      (postmodern:query
248
-        (:insert-into 'subscriptions :set 'uid uid 'feedid feedid))
249
-      rss-feed-)))
330
+
331
+; \o/
332
+;  | Arrr
333
+; / \
... ...
@@ -1 +1 @@
1
-@import url(https://fonts.googleapis.com/css?family=Lato:400,100,300,400italic,300italic,700,700italic,900&subset=latin,latin-ext);@import url(https://fonts.googleapis.com/css?family=Caudex);.feed,main{-webkit-backface-visibility:hidden;-webkit-transform:translateZ(0);}*{box-sizing:border-box;margin:0px;padding:0px;}body{font-family:Lato;}main > ul,main > ol,#sidebar > ul,#sidebar > ol,.feed > ul,.feed > ol{list-style:none;margin:0px;}ul{margin:1em;}h1,h2{font-size:153.9%;}:h3{font-size:146.5%;}h4,h5,h6{font-size:138.5%;}ul + h1,ul + h2,ul + h3,ul + h4,ul + h5,ul + h6{width:initial;}header{color:white;}header h1{font-family:Caudex;font-size:inherit;margin-top:0em;padding-left:10vw;font-weight:200;}#sidebar,main{border-top:none;}section#sidebar{width:38vw;height:90vh;position:fixed;overflow:auto;}section#sidebar ul.menu{text-align:right;font-variant:small-caps;opacity:0;}section#sidebar ul.menu li{-moz-transition:all 0.25s ease;-o-transition:all 0.25s ease;-webkit-transition:all 0.25s ease;-ms-transition:all 0.25s ease;transition:all 0.25s ease;}section#sidebar ul.menu li a{color:inherit;display:block;font-size:125%;font-weight:700;padding:0.5em;text-decoration:none;width:100%;}section#sidebar ul.menu.open{opacity:1;}main{width:62vw;float:right;clear:right;overflow-x:hidden;overflow-y:scroll;position:relative;}img.spinner{position:absolute;top:50%;left:50%;-moz-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);}.hide{display:none;}.feed-header{-moz-transition:background-color 0.25s ease;-o-transition:background-color 0.25s ease;-webkit-transition:background-color 0.25s ease;-ms-transition:background-color 0.25s ease;transition:background-color 0.25s ease;padding-bottom:0em;}.feed-header h2,.feed-header h3{padding:0.62em;}.feed-header h2{padding-bottom:0.38em;}.feed-header h3{padding-top:0.38em;}.link.closed .link-content{max-height:0px;padding:0em;}.link.closed{padding-bottom:0em;}.link{text-decoration:none;display:block;overflow:hidden;font-size:0.8em;}.link .link-header{padding:1em;padding-bottom:0em;cursor:pointer;-moz-transition:background-color 0.25s ease;-o-transition:background-color 0.25s ease;-webkit-transition:background-color 0.25s ease;-ms-transition:background-color 0.25s ease;transition:background-color 0.25s ease;}.link .link-header h4{margin-bottom:0.5em;display:inline-block;}.link .link-info{margin-left:-1em;margin-right:-1em;padding-left:1em;padding-right:1em;padding-bottom:0.32em;}.link .link-info .link-url{float:left;}.link .link-info .link-date{float:right;display:block;}.link .link-info:after{content:" ";display:block;clear:both;}.link .link-content{overflow:hidden;-moz-transition:max-height 0.5s ease;-o-transition:max-height 0.5s ease;-webkit-transition:max-height 0.5s ease;-ms-transition:max-height 0.5s ease;transition:max-height 0.5s ease;}.link .link-content img{margin:1em 0% 1em 10%;max-width:80%;max-height:70vh;}.link .link-content > div{padding:1em;font-size:12pt;}.feed.closed{border-bottom-width:thin;}.feed.closed .post-list{max-height:0px;padding:0em;}.feed.closed h3{display:none;}.feed:first-child{border-top:none;}.feed{overflow:hidden;}.feed .post-list{-moz-transition:max-height 0.5s ease;-o-transition:max-height 0.5s ease;-webkit-transition:max-height 0.5s ease;-ms-transition:max-height 0.5s ease;transition:max-height 0.5s ease;}#add-form{font-size:1em;height:1.8em;display:block;overflow:hidden;width:100%;}#add-form input{border:none;width:95%;height:100%;font-size:0.8em;line-height:1.8em;padding:1em 0.5em;}#add-form button{border:none;border-radius:0px;font-size:inherit;width:5%;height:100%;}.flip-button{position:absolute;right:0em;top:0em;z-index:1000;width:3em;height:3em;padding-left:1em;padding-bottom:1em;border-bottom-left-radius:100%;border:none;-moz-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);-o-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);-webkit-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);-ms-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);background-image:none;}
2 1
\ No newline at end of file
2
+@import url(https://fonts.googleapis.com/css?family=Lato:400,100,300,400italic,300italic,700,700italic,900&subset=latin,latin-ext);@import url(https://fonts.googleapis.com/css?family=Caudex);*{box-sizing:border-box;margin:0px;padding:0px;}body{-moz-transition:background-color .5s ease;-o-transition:background-color .5s ease;-webkit-transition:background-color .5s ease;-ms-transition:background-color .5s ease;transition:background-color .5s ease;font-family:Lato;}main > ul,main > ol,#sidebar > ul,#sidebar > ol,.feed > ul,.feed > ol{list-style:none;margin:0px;}ul{margin:1em;}h1,h2{font-size:153.9%;}:h3{font-size:146.5%;}h4,h5,h6{font-size:138.5%;}ul + h1,ul + h2,ul + h3,ul + h4,ul + h5,ul + h6{width:initial;}header{color:white;}header h1{font-family:Caudex;font-size:inherit;margin-top:0em;padding-left:10vw;font-weight:200;}#sidebar,main{border-top:none;}section#sidebar{width:38vw;height:90vh;position:fixed;overflow:auto;}section#sidebar ul.menu{text-align:right;font-variant:small-caps;}section#sidebar ul.menu li{-moz-transition:all .5s ease;-o-transition:all .5s ease;-webkit-transition:all .5s ease;-ms-transition:all .5s ease;transition:all .5s ease;}section#sidebar ul.menu li a{color:inherit;display:block;font-size:125%;font-weight:700;padding:0.5em;text-decoration:none;width:100%;}main{width:62vw;float:right;clear:right;overflow:hidden;position:relative;}main > div{position:absolute;top:2em;bottom:0em;width:100%;overflow-y:scroll;}img.spinner{position:absolute;top:50%;left:50%;-moz-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);}.hide{display:none;}.feed-header{-moz-transition:background-color .5s ease;-o-transition:background-color .5s ease;-webkit-transition:background-color .5s ease;-ms-transition:background-color .5s ease;transition:background-color .5s ease;padding-bottom:0em;}.feed-header h2,.feed-header h3{padding:0.62em;}.feed-header h2{padding-bottom:0.38em;}.feed-header h3{padding-top:0.38em;}.link.closed .link-content{max-height:0px;padding:0em;}.link.closed{padding-bottom:0em;}.link{text-decoration:none;display:block;overflow:hidden;font-size:0.8em;}.link .link-header{padding:1em;padding-bottom:0em;cursor:pointer;-moz-transition:background-color .5s ease;-o-transition:background-color .5s ease;-webkit-transition:background-color .5s ease;-ms-transition:background-color .5s ease;transition:background-color .5s ease;}.link .link-header h4{margin-bottom:0.5em;display:inline-block;}.link .link-info{margin-left:-1em;margin-right:-1em;padding-left:1em;padding-right:1em;padding-bottom:0.32em;}.link .link-info .link-url{float:left;}.link .link-info .link-date{float:right;display:block;}.link .link-info:after{content:" ";display:block;clear:both;}.link .link-content{overflow:hidden;-moz-transition:max-height 0.5s ease;-o-transition:max-height 0.5s ease;-webkit-transition:max-height 0.5s ease;-ms-transition:max-height 0.5s ease;transition:max-height 0.5s ease;}.link .link-content img{margin:1em 0% 1em 10%;max-width:80%;max-height:70vh;}.link .link-content > div{padding:1em;font-size:12pt;}.feed.closed{border-bottom-width:thin;}.feed.closed .post-list{max-height:0px;padding:0em;}.feed.closed h3{display:none;}.feed:first-child{border-top:none;}.feed{overflow:hidden;}.feed .post-list{-moz-transition:max-height 0.5s ease;-o-transition:max-height 0.5s ease;-webkit-transition:max-height 0.5s ease;-ms-transition:max-height 0.5s ease;transition:max-height 0.5s ease;}#add-form{-moz-transition:background-color .5s ease;-o-transition:background-color .5s ease;-webkit-transition:background-color .5s ease;-ms-transition:background-color .5s ease;transition:background-color .5s ease;font-size:1em;height:1.8em;display:block;overflow:hidden;width:100%;position:absolute;z-index:3;}#add-form input,#add-form button{-moz-transition:background-color .5s ease;-o-transition:background-color .5s ease;-webkit-transition:background-color .5s ease;-ms-transition:background-color .5s ease;transition:background-color .5s ease;}#add-form input{border:none;width:95%;height:100%;font-size:0.8em;line-height:1.8em;padding:1em 0.5em;}#add-form button{border:none;border-radius:0px;font-size:inherit;width:5%;height:100%;}.flip-button{position:absolute;right:0em;top:0em;z-index:1000;width:3em;height:3em;padding-left:1em;padding-bottom:1em;border-bottom-left-radius:100%;border:none;-moz-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);-o-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);-webkit-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);-ms-transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);transition:all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);background-image:none;}
3 3
\ No newline at end of file
... ...
@@ -10,11 +10,10 @@ whitespace.controller('MainCtrl', ['$scope', '$http', '$resource', '$sce', funct
10 10
     $scope.toggleClosed = function (ent) {
11 11
         return ent.closed = !ent.closed;
12 12
     };
13
-    $scope.addFeed = function () {
13
+    return $scope.addFeed = function () {
14 14
         return feeds.add({ 'url' : $scope.addForm.url, 'api' : 'yes' }).$promise.then(function (feed) {
15 15
             return $scope.feeds.result.unshift(feed.result);
16 16
         });
17 17
     };
18
-    return null;
19 18
 }]);
20 19
 
... ...
@@ -1,3 +1,6 @@
1
+(in-package :cl-user)
2
+(defpackage :whitespace.tables
3
+  (:use #:cl #:alexandria #:postmodern #:annot.class))
1 4
 (in-package :whitespace.tables)
2 5
 (cl-annot.syntax:enable-annot-syntax)
3 6
 
... ...
@@ -6,16 +9,14 @@
6 9
   ((id          :col-type serial :initarg :id          :accessor rfs-id)
7 10
    (title       :col-type text   :initarg :title       :accessor rfs-title       :col-default "")
8 11
    (link        :col-type text   :initarg :link        :accessor rfs-link        :col-default "")
9
-   (description :col-type text   :initarg :description :accessor rfs-description :col-default ""))
10
-  (:metaclass postmodern:dao-class)
11
-  (:table-name "rssFeed")
12
-  (:unique link)
12
+   (description :col-type text   :initarg :description :accessor rfs-description :col-default "")
13
+   (fetch-url   :col-type text   :initarg :fetch-url :accessor rfs-fetch-url   :col-default ""))
14
+  (:metaclass dao-class)
13 15
   (:keys id))
14 16
 
15
-(postmodern:deftable rss_feed_store
16
-  (postmodern:!dao-def)
17
-  (postmodern:!unique "link")
18
-  )
17
+(deftable rss_feed_store
18
+  (!dao-def)
19
+  (!unique "link"))
19 20
 
20 21
 
21 22
 @export-class
... ...
@@ -30,12 +31,13 @@
30 31
    (pub-date    :col-type text    :initarg :pub-date    :accessor ris-pub-date    :col-default "")
31 32
    (source      :col-type text    :initarg :source      :accessor ris-source      :col-default "")
32 33
    (feed        :col-type integer :initarg :feed        :accessor ris-feed))
33
-  (:metaclass postmodern:dao-class)
34
+  (:metaclass dao-class)
34 35
   (:keys id))
35 36
 
36
-(postmodern:deftable rss_item_store
37
-  (postmodern:!dao-def)
38
-  (postmodern:!foreign "rssfeed" "feed" "id" :on-delete :cascade :on-update :cascade))
37
+(deftable rss_item_store
38
+  (!dao-def)
39
+  (!foreign "rss_feed_store" "feed" "id" :on-delete :cascade :on-update :cascade)
40
+  (!unique "guid"))
39 41
 
40 42
 
41 43
 @export-class
... ...
@@ -50,11 +52,11 @@
50 52
    (last-name :col-type (or string s-sql:db-null) :initarg :last-name :accessor user-last-name)
51 53
    (link :col-type (or string s-sql:db-null) :initarg :link :accessor user-link)
52 54
    (locale :col-type (or string s-sql:db-null) :initarg :locale :accessor user-locale))
53
-  (:metaclass postmodern:dao-class)
55
+  (:metaclass dao-class)
54 56
   (:keys id))
55 57
 
56
-(postmodern:deftable reader_user
57
-  (postmodern:!dao-def))
58
+(deftable reader_user
59
+  (!dao-def))
58 60
 
59 61
 @export-class
60 62
 (defclass subscriptions ()
... ...
@@ -62,16 +64,23 @@
62 64
    (uid :col-type integer :initarg :uid :accessor subscription-uid)
63 65
    (feedid :col-type integer :initarg :feedid :accessor subscription-feedid))
64 66
   (:unique (uid feedid))
65
-  (:metaclass postmodern:dao-class)
67
+  (!foreign "rss_feed_store" "feedid" "id" :on-delete :cascade :on-update :cascade)
68
+  (:metaclass dao-class)
66 69
   (:keys id))
67 70
 
68
-(postmodern:deftable subscriptions
69
-  (postmodern:!dao-def)
70
-  (postmodern:!unique '(uid feedid)))
71
+(deftable subscriptions
72
+  (!dao-def)
73
+  (!foreign "rss_feed_store" "feedid" "id" :on-delete :cascade :on-update :cascade)
74
+  (!foreign "reader_user" "uid" "id" :on-delete :cascade :on-update :cascade)
75
+  (!unique '(uid feedid)))
71 76
 
72
-; (postmodern:create-table 'rss_feed_store)
73
-; (postmodern:create-table 'rss_item_store)
74
-; (postmodern:create-table 'reader_user)
75
-; (postmodern:create-table 'subscriptions)
77
+#|
76 78
 
79
+(with-connection whitespace::*db-connection-info* 
80
+  (with-transaction ()
81
+    (create-table 'rss_feed_store)
82
+    (create-table 'rss_item_store)
83
+    (create-table 'reader_user)
84
+    (create-table 'subscriptions)))
77 85
 
86
+|#