Browse code
Simplifying architecture and rewriting
fiddlerwoaroof authored on 25/09/2015 21:29:39
Showing 10 changed files
Showing 10 changed files
- angular.lisp
- base-template.lisp
- cl-oid-connect.lisp
- demo.lisp
- lass/main.lass
- parenscript/angular-framework.ps
- rss.lisp
- static/css/main.css
- static/js/whitespace-angular.js
- tables.lisp
... | ... |
@@ -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 |
+|# |