Browse code
Fix proxied IP address . . . hopefully?
fiddlerwoaroof authored on 28/01/2016 07:46:50
Showing 7 changed files
Showing 7 changed files
- inangulis.asd
- inangulis.lisp
- package.lisp
- static/headlines.mustache.html
- static/submission.mustache.html
- tables.lisp
- web.lisp
... | ... |
@@ -14,10 +14,6 @@ |
14 | 14 |
(defun str-assoc (param params &key (test #'equal) key) |
15 | 15 |
(cdr-assoc param params :test test :key key)) |
16 | 16 |
|
17 |
-(defmacro setf1 (&body body) |
|
18 |
- "Make setf a bit nicer" |
|
19 |
- (list* 'setf (apply #'append body))) |
|
20 |
- |
|
21 | 17 |
;;; "inangulis" goes here. Hacks and glory await! |
22 | 18 |
|
23 | 19 |
(defun current-date-string () |
... | ... |
@@ -32,7 +28,9 @@ |
32 | 28 |
(moderator :initarg :moderator :initform nil))) |
33 | 29 |
|
34 | 30 |
(defun user-alist (user) |
35 |
- (with-slots (email name moderator) user |
|
31 |
+ (with-slots ((email inangulis.tables::email) |
|
32 |
+ (name inangulis.tables::name) |
|
33 |
+ (moderator inangulis.tables::moderator)) user |
|
36 | 34 |
`(("name" . ,name) |
37 | 35 |
("email" . ,email) |
38 | 36 |
("moderator" . ,moderator)))) |
... | ... |
@@ -71,6 +69,9 @@ |
71 | 69 |
(remove-if (lambda (x) (not (equal x "approved"))) feeds :key #'s-approved)) |
72 | 70 |
|
73 | 71 |
(defparameter *app* (make-instance 'ningle:<app>)) |
72 |
+(defmethod lack.component:call :before ((component (eql *app*)) env) |
|
73 |
+ (awhen (gethash "x-remote-ip" (lack.request:request-headers ningle.context:*request*)) |
|
74 |
+ (setf (lack.request:request-remote-addr ningle:*request*) it))) |
|
74 | 75 |
|
75 | 76 |
(defmacro i-defun (name (&rest args) &body body) |
76 | 77 |
`(defun ,name (,@args) |
... | ... |
@@ -84,12 +85,6 @@ |
84 | 85 |
(declare (ignorable ,@args)) |
85 | 86 |
,@body)) |
86 | 87 |
|
87 |
-(defun render-mustache (fn data) |
|
88 |
- (with-open-file (s (truename fn)) |
|
89 |
- (let ((template (make-string (file-length s)))) |
|
90 |
- (read-sequence template s) |
|
91 |
- (mustache:render* template data)))) |
|
92 |
- |
|
93 | 88 |
(defmacro with-db (&body b) |
94 | 89 |
`(postmodern:with-connection (ubiquitous:value 'db) |
95 | 90 |
,@b)) |
... | ... |
@@ -100,28 +95,12 @@ |
100 | 95 |
,@b))) |
101 | 96 |
|
102 | 97 |
|
103 |
-(setf (ningle:route *app* "/") |
|
104 |
- (flet ((render-index (&optional user) |
|
105 |
- (render-mustache #p"static/index.mustache.html" |
|
106 |
- (cons |
|
107 |
- `(:links . ,(mapcar #'submission-alist *submissions*)) |
|
108 |
- (when user |
|
109 |
- (list |
|
110 |
- `(:user . ,(user-alist user)))))))) |
|
111 |
- (i-lambda (params) |
|
112 |
- (with-submissions |
|
113 |
- (ningle.context:with-context-variables (session) |
|
114 |
- (handler-case |
|
115 |
- (cl-oid-connect.utils:ensure-logged-in |
|
116 |
- (cl-oid-connect.utils:redirect-if-necessary session |
|
117 |
- (render-index (gethash :app-user session)))) |
|
118 |
- (cl-oid-connect.utils:user-not-logged-in (c) (render-index)))))))) |
|
98 |
+ |
|
119 | 99 |
|
120 | 100 |
(defun submit (params) |
121 |
- (with-db |
|
122 |
- (awhen (alist-submission params :nil-if-exists t) |
|
123 |
- (postmodern:insert-dao it) |
|
124 |
- (push it *submissions*)))) |
|
101 |
+ (awhen (alist-submission params :nil-if-exists t) |
|
102 |
+ (postmodern:insert-dao it) |
|
103 |
+ (push it *submissions*))) |
|
125 | 104 |
|
126 | 105 |
(defun get-feed-guid (item) |
127 | 106 |
(with-slots (alimenta:title alimenta:link) item |
... | ... |
@@ -130,27 +109,37 @@ |
130 | 109 |
(ironclad:update-digest hasher (babel:string-to-octets alimenta:link)) |
131 | 110 |
(ironclad:byte-array-to-hex-string (ironclad:produce-digest hasher))))) |
132 | 111 |
|
112 |
+(defmethod run-route :around (name params &rest r) |
|
113 |
+ (declare (ignore r)) |
|
114 |
+ (with-db |
|
115 |
+ (pomo:with-transaction () |
|
116 |
+ (call-next-method)))) |
|
117 |
+ |
|
133 | 118 |
;; View Controllers |
134 |
-(i-defun murmur (params) |
|
119 |
+(define-controller murmur (params) |
|
135 | 120 |
(sleep 0.01) |
136 |
- (submit params) |
|
137 |
- '(302 (:location "/") ("Done"))) |
|
138 |
- |
|
139 |
-(i-defun curate (params) |
|
140 |
- (with-db |
|
141 |
- (let ((*submissions* (postmodern:select-dao 'inangulis.tables::submission t "date desc"))) |
|
142 |
- (cl-oid-connect.utils:require-login |
|
143 |
- (alet (alist-submission params) |
|
144 |
- (let ((approval (string-downcase (str-assoc "approved" params :test #'equalp)))) |
|
145 |
- (setf (s-approved it) |
|
146 |
- (if (equal approval "+") "approved" |
|
147 |
- (if (equal approval "-") "rejected"))) |
|
148 |
- (when *persist* |
|
149 |
- (postmodern:update-dao it))))))) |
|
121 |
+ (submit params)) |
|
122 |
+ |
|
123 |
+(define-view murmur (model) |
|
124 |
+ '(302 (:location "/") ("Done"))) |
|
125 |
+ |
|
126 |
+(define-controller curate (params) |
|
127 |
+ (let ((*submissions* (postmodern:select-dao 'inangulis.tables::submission t "date desc"))) |
|
128 |
+ (cl-oid-connect.utils:require-login |
|
129 |
+ (alet (alist-submission params) |
|
130 |
+ (let ((approval (string-downcase (str-assoc "approved" params :test #'equalp)))) |
|
131 |
+ (setf (s-approved it) |
|
132 |
+ (if (equal approval "+") "approved" |
|
133 |
+ (if (equal approval "-") "rejected"))) |
|
134 |
+ (when *persist* |
|
135 |
+ (postmodern:update-dao it))))))) |
|
136 |
+ |
|
137 |
+(define-view curate (params) |
|
150 | 138 |
'(302 (:location "/") ("Done"))) |
151 | 139 |
|
152 |
-(i-defun login-page (params) |
|
153 |
- `(200 () |
|
140 |
+(define-view login-page (model) |
|
141 |
+ `(200 |
|
142 |
+ () |
|
154 | 143 |
(,(cl-who:with-html-output-to-string (s) |
155 | 144 |
(:html |
156 | 145 |
(:head |
... | ... |
@@ -161,15 +150,17 @@ |
161 | 150 |
(:div :class "login-buttons" |
162 | 151 |
(:a :class "facebook" :href "/login/facebook" "Login With Facebook")))))))) |
163 | 152 |
|
164 |
-(i-defun logout (params) |
|
153 |
+(define-controller logout (params) |
|
165 | 154 |
(ningle:with-context-variables (session) |
166 |
- (setf (gethash :userinfo session) nil) |
|
167 |
- '(302 (:location "/")))) |
|
155 |
+ (setf (gethash :userinfo session) nil))) |
|
168 | 156 |
|
169 |
-(i-defun get-feed (params &key moderated) |
|
157 |
+(define-view logout (model) |
|
158 |
+ '(302 (:location "/"))) |
|
159 |
+ |
|
160 |
+(defmethod controller ((name (eql 'get-feed)) params &key moderated) |
|
170 | 161 |
(let ((feed (alimenta::make-feed :title "In Angulis" :link "http://in-angulis.com/feed" |
171 | 162 |
:description "Locus in quo sunt illi qui murmurant in angulis"))) |
172 |
- (with-db |
|
163 |
+ (prog1 feed |
|
173 | 164 |
(pomo:do-select-dao (('inangulis.tables::submission submission) |
174 | 165 |
(:raw (if moderated (pomo:sql (:= 'approved "approved")) "'t'")) |
175 | 166 |
(:desc 'date)) |
... | ... |
@@ -178,29 +169,88 @@ |
178 | 169 |
:link (s-url submission) |
179 | 170 |
:date (s-date submission) |
180 | 171 |
:next-id #'get-feed-guid |
181 |
- :content ""))) |
|
182 |
- `(200 (:content-type "application/rss+xml") (,(plump:serialize (alimenta:generate-xml feed) nil))))) |
|
183 |
- |
|
184 |
-(setf1 ((ningle:route *app* "/feed" :method :GET) (lambda (params) (get-feed params :moderated t))) |
|
185 |
- ((ningle:route *app* "/firehose" :method :GET) #'get-feed) |
|
186 |
- ((ningle:route *app* "/login" :method :GET) #'login-page) |
|
187 |
- ((ningle:route *app* "/curo" :method :POST) #'curate) |
|
188 |
- ((ningle:route *app* "/murmuro" :method :POST) #'murmur) |
|
189 |
- ((ningle:route *app* "/logout") #'logout)) |
|
190 |
- |
|
172 |
+ :content ""))))) |
|
173 |
+ |
|
174 |
+(defparameter *tmp* nil) |
|
175 |
+ |
|
176 |
+(define-view get-feed (feed) |
|
177 |
+ `(200 (:content-type "application/rss+xml") (,(plump:serialize (alimenta:generate-xml feed) nil)))) |
|
178 |
+ |
|
179 |
+(mustache-view root (user . links) #p"static/index.mustache.html" |
|
180 |
+ :links (mapcar #'submission-alist links) |
|
181 |
+ :user (when user (user-alist user))) |
|
182 |
+ |
|
183 |
+(define-controller root (params) |
|
184 |
+ (setf *tmp* ningle.context:*request*) |
|
185 |
+ (with-submissions |
|
186 |
+ (ningle:with-context-variables (session) |
|
187 |
+ (handler-case |
|
188 |
+ (cl-oid-connect.utils:ensure-logged-in |
|
189 |
+ (cl-oid-connect.utils:redirect-if-necessary session |
|
190 |
+ (cons (gethash :app-user session) |
|
191 |
+ *submissions*))) |
|
192 |
+ (cl-oid-connect.utils:user-not-logged-in (c) (cons nil *submissions*)))))) |
|
193 |
+ |
|
194 |
+(defmethod controller :around ((name (eql 'headlines)) params &key (moderated t moderated-p)) |
|
195 |
+ (unless moderated-p |
|
196 |
+ (awhen (str-assoc "moderated" params :test #'string-equal) |
|
197 |
+ (setf moderated (and (> (length it) 0) (char= #\t (elt it 0))) |
|
198 |
+ moderated-p t))) |
|
199 |
+ (if moderated-p |
|
200 |
+ (call-next-method name params :moderated moderated) |
|
201 |
+ (call-next-method))) |
|
202 |
+ |
|
203 |
+(defmethod controller ((name (eql 'headlines)) params &key (moderated t moderated-p)) |
|
204 |
+ (let (result) |
|
205 |
+ (ningle.context:with-context-variables (session) |
|
206 |
+ (let* ((app-user (gethash :app-user session)) |
|
207 |
+ (moderator-p (and app-user (slot-value app-user 'inangulis.tables::moderator)))) |
|
208 |
+ (pomo:do-select-dao (('inangulis.tables::submission submission) |
|
209 |
+ (:raw (cond |
|
210 |
+ ((and moderator-p (not moderated-p)) "'t'") |
|
211 |
+ (moderated (pomo:sql (:= 'approved "approved"))) |
|
212 |
+ (t "'t'"))) |
|
213 |
+ (:desc 'date)) |
|
214 |
+ (push submission result)))) |
|
215 |
+ result)) |
|
216 |
+ |
|
217 |
+(define-view headlines (columns) ;#p"static/headlines.mustache.html" |
|
218 |
+ (let ((sub-len (length columns)) |
|
219 |
+ (columns (mapcar #'submission-alist columns))) |
|
220 |
+ (render-mustache #p"static/headlines.mustache.html" |
|
221 |
+ `((:columns . (((:rows . ,(subseq columns 0 (floor sub-len 3)))) |
|
222 |
+ ((:rows . ,(subseq columns (floor sub-len 3) (* 2 (floor sub-len 3)) ))) |
|
223 |
+ ((:rows . ,(subseq columns (* 2 (floor sub-len 3)) sub-len))))))))) |
|
224 |
+ |
|
225 |
+(defroutes *app* |
|
226 |
+ (("/") (as-route 'root)) |
|
227 |
+ (("/curo" :method :POST) (as-route 'curate)) |
|
228 |
+ (("/feed") (as-route 'get-feed :moderated t)) |
|
229 |
+ (("/firehose") (as-route 'get-feed)) |
|
230 |
+ (("/headlines") (as-route 'headlines)) |
|
231 |
+ (("/login") (as-route 'login-page)) |
|
232 |
+ (("/logout") (as-route 'logout)) |
|
233 |
+ (("/murmuro" :method :POST) (as-route 'murmur))) |
|
191 | 234 |
|
192 | 235 |
(cl-oid-connect::setup-oid-connect *app* (userinfo &rest args) |
193 | 236 |
(declare (ignore args)) |
194 | 237 |
(let ((id (cdr (assoc :id userinfo)))) |
195 |
- (unless (gethash id *users*) |
|
196 |
- (setf (gethash id *users*) |
|
197 |
- (alet (make-instance 'user) |
|
198 |
- (with-slots (uid name email) it |
|
199 |
- (prog1 it |
|
200 |
- (setf uid id |
|
201 |
- name (cdr (assoc :name userinfo)) |
|
202 |
- email (cdr (assoc :email userinfo)))))))) |
|
203 |
- (gethash id *users*))) |
|
238 |
+ (with-db |
|
239 |
+ (aif (car (postmodern:select-dao 'inangulis.tables::user (:= 'uid id))) |
|
240 |
+ it |
|
241 |
+ (alet (make-instance 'inangulis.tables::user) |
|
242 |
+ (with-slots ((uid inangulis.tables::uid) |
|
243 |
+ (name inangulis.tables::name) |
|
244 |
+ (email inangulis.tables::email)) it |
|
245 |
+ (prog1 it |
|
246 |
+ (setf uid id |
|
247 |
+ name (cdr (assoc :name userinfo)) |
|
248 |
+ email (cdr (assoc :email userinfo))) |
|
249 |
+ (pomo:with-transaction () |
|
250 |
+ (postmodern:insert-dao it))))))))) |
|
251 |
+ |
|
252 |
+ |
|
253 |
+ |
|
204 | 254 |
|
205 | 255 |
(let ((handler nil)) |
206 | 256 |
(ubiquitous:restore :inangulis) |
... | ... |
@@ -222,7 +272,7 @@ |
222 | 272 |
:backtrace |
223 | 273 |
*app* |
224 | 274 |
) |
225 |
- :debug (ubiquitous:defaulted-value t 'debug)) |
|
275 |
+ :debug (ubiquitous:defaulted-value t 'debug)) |
|
226 | 276 |
:port 9090 |
227 | 277 |
:server server) |
228 | 278 |
handler))) |
... | ... |
@@ -4,6 +4,15 @@ |
4 | 4 |
(:use #:cl #:anaphora #:postmodern) |
5 | 5 |
(:export #:submission #:headline #:url #:date #:approved)) |
6 | 6 |
|
7 |
+(defpackage #:inangulis.web |
|
8 |
+ (:use #:cl) |
|
9 |
+ (:export #:defroutes #:as-route #:define-controller #:define-view |
|
10 |
+ #:controller #:view #:run-route #:mustache-view #:render-mustache |
|
11 |
+ #:setf1)) |
|
12 |
+ |
|
7 | 13 |
(defpackage #:inangulis |
8 |
- (:use #:cl #:anaphora)) |
|
14 |
+ (:use #:cl #:anaphora #:inangulis.tables #:inangulis.web)) |
|
15 |
+ |
|
16 |
+(defpackage #:inangulis-user |
|
17 |
+ (:use #:cl #:inangulis.tables #:inangulis.web #:inangulis)) |
|
9 | 18 |
|
10 | 19 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,110 @@ |
1 |
+<!DOCTYPE html> |
|
2 |
+<html lang="en"> |
|
3 |
+ <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# join-marrow: http://ogp.me/ns/fb/join-marrow#"> |
|
4 |
+ <base href="http://planet.joinmarrow.com" /> |
|
5 |
+ <meta property="og:type" content="join-marrow:headlines" /> |
|
6 |
+ <meta property="og:title" content="Current Headlines" /> |
|
7 |
+ <meta property="og:site_name" content="Catholic News" /> |
|
8 |
+ <meta property="og:url" content="http://planet.joinmarrow.com/headlines" /> |
|
9 |
+ <meta property="og:image" content="http://planet.joinmarrow.com/planet.joinmarrow.png" /> |
|
10 |
+ <meta property="og:image:width" content="512" /> |
|
11 |
+ <meta property="og:image:height" content="512" /> |
|
12 |
+ <meta property="og:description" content="Headlines from a collection of Catholic blogs. Visit us to take the pulse of the Catholic internet" /> |
|
13 |
+ <meta property="fb:app_id" content="897925460261572" /> |
|
14 |
+ <base target="_blank" /> |
|
15 |
+ <meta http-equiv="refresh" content="900"> <!-- refresh every 15 minutes --> |
|
16 |
+ |
|
17 |
+ <link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png"> |
|
18 |
+ <link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png"> |
|
19 |
+ <link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png"> |
|
20 |
+ <link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png"> |
|
21 |
+ <link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png"> |
|
22 |
+ <link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png"> |
|
23 |
+ <link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png"> |
|
24 |
+ <link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png"> |
|
25 |
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png"> |
|
26 |
+ <link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32"> |
|
27 |
+ <link rel="icon" type="image/png" href="/favicon-194x194.png" sizes="194x194"> |
|
28 |
+ <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96"> |
|
29 |
+ <link rel="icon" type="image/png" href="/android-chrome-192x192.png" sizes="192x192"> |
|
30 |
+ <link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16"> |
|
31 |
+ <link rel="manifest" href="/manifest.json"> |
|
32 |
+ <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> |
|
33 |
+ <meta name="msapplication-TileColor" content="#888888"> |
|
34 |
+ <meta name="msapplication-TileImage" content="/mstile-144x144.png"> |
|
35 |
+ <meta name="theme-color" content="#ffffff"> |
|
36 |
+ |
|
37 |
+ <meta charset="UTF-8"> |
|
38 |
+ <meta name="viewport" content="width=device-width, initial-scale=1"> |
|
39 |
+ |
|
40 |
+ <title>Catholic Feeds - Headlines</title> |
|
41 |
+ <link rel="stylesheet" href="headline-style.css"> |
|
42 |
+ <link rel="stylesheet" href="light.css"> |
|
43 |
+ <!-- Piwik --> |
|
44 |
+ <script type="text/javascript"> |
|
45 |
+ var _paq = _paq || []; |
|
46 |
+ _paq.push(['trackPageView']); |
|
47 |
+ _paq.push(['enableLinkTracking']); |
|
48 |
+ (function() { |
|
49 |
+ var u="//piwik.elangley.org/"; |
|
50 |
+ _paq.push(['setTrackerUrl', u+'piwik.php']); |
|
51 |
+ _paq.push(['setSiteId', 1]); |
|
52 |
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; |
|
53 |
+ g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); |
|
54 |
+ })(); |
|
55 |
+ |
|
56 |
+function colorflip() { |
|
57 |
+ var lightEl = document.querySelector('link[href^=light]'); |
|
58 |
+ var darkEl = document.querySelector('link[href^=dark]'); |
|
59 |
+ |
|
60 |
+ if (lightEl !== null) { |
|
61 |
+ lightEl.href = "dark.css" |
|
62 |
+ } else { |
|
63 |
+ darkEl.href = "light.css" |
|
64 |
+ } |
|
65 |
+}; |
|
66 |
+ |
|
67 |
+</script> |
|
68 |
+<noscript><p><img src="//piwik.elangley.org/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript> |
|
69 |
+<!-- End Piwik Code --> |
|
70 |
+ |
|
71 |
+ </head> |
|
72 |
+ <body> |
|
73 |
+ <script type="text/javascript"> |
|
74 |
+ //do work |
|
75 |
+ |
|
76 |
+ if (window.location.hash === "#dark") { |
|
77 |
+ console.log('hi!'); |
|
78 |
+ colorflip(); |
|
79 |
+ } |
|
80 |
+ |
|
81 |
+</script> |
|
82 |
+<div id="fb-root"></div> |
|
83 |
+<script>(function(d, s, id) { |
|
84 |
+ var js, fjs = d.getElementsByTagName(s)[0]; |
|
85 |
+ if (d.getElementById(id)) return; |
|
86 |
+ js = d.createElement(s); js.id = id; |
|
87 |
+ js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.5&appId=897925460261572"; |
|
88 |
+ fjs.parentNode.insertBefore(js, fjs); |
|
89 |
+ }(document, 'script', 'facebook-jssdk'));</script> |
|
90 |
+ |
|
91 |
+<header> |
|
92 |
+<button class="flip-button" onclick="colorflip()">…</button> |
|
93 |
+<h1>Headlines</h1> |
|
94 |
+</header> |
|
95 |
+<main> |
|
96 |
+{{#columns}} |
|
97 |
+<div class="col"> |
|
98 |
+ {{#rows}} |
|
99 |
+ <article data-date="{{date}}"> |
|
100 |
+ <a href="{{url}}" rel="nofollow"> |
|
101 |
+ <h2>{{headline}}</h2> |
|
102 |
+ <span class="source">{{url}}</span> |
|
103 |
+ </a> |
|
104 |
+ </article> |
|
105 |
+ {{/rows}} |
|
106 |
+</div> |
|
107 |
+{{/columns}} |
|
108 |
+</main> |
|
109 |
+ </body> |
|
110 |
+ </html> |
0 | 111 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,52 @@ |
1 |
+<!DOCTYPE html> |
|
2 |
+<html lang="en"> |
|
3 |
+ <head> |
|
4 |
+ <meta charset="UTF-8"> |
|
5 |
+ <title>In Angulis</title> |
|
6 |
+ <!--<link rel="stylesheet" href="/css/baseline.css">--> |
|
7 |
+ <link rel="stylesheet" href="/static/css/main.css"> |
|
8 |
+ </head> |
|
9 |
+ <body> |
|
10 |
+ <h1>In Angulis</h1> |
|
11 |
+ {{#user}} |
|
12 |
+ <div class="userinfo"> |
|
13 |
+ <div class="user">{{name}}</div> |
|
14 |
+ <div class="email">{{email}}</div> |
|
15 |
+ <div class="mod">{{moderator}}</div> |
|
16 |
+ <a id="logout" href="/logout">[Logout]</a> |
|
17 |
+ </div> |
|
18 |
+ {{/user}} |
|
19 |
+ <main id="app"> |
|
20 |
+ <form action="/murmuro" id="submission" name="submission" method="POST" v-on:submit="submit"> |
|
21 |
+ <input type="text" name="headline" placeholder="Headline" v-model="newLink.headline"> |
|
22 |
+ <input type="text" name="url" placeholder="URL" v-model="newLink.url"> |
|
23 |
+ <input type="submit" value="Go" title='Murmuro'> |
|
24 |
+ </form> |
|
25 |
+ </main> |
|
26 |
+ |
|
27 |
+ <script src="/static/js/jquery.js"></script> |
|
28 |
+ <script src="/static/js/jquery.formalize.js"></script> |
|
29 |
+ <script src="/static/js/vue.js"></script> |
|
30 |
+ <script type="text/javascript"> |
|
31 |
+ new Vue({ |
|
32 |
+ el: '#app', |
|
33 |
+ |
|
34 |
+ methods: { |
|
35 |
+ submit: function(e) { |
|
36 |
+ e.preventDefault(); |
|
37 |
+ this.links.unshift(0,Object.create(this.newLink)); |
|
38 |
+ this.newLink = {}; |
|
39 |
+ return false; |
|
40 |
+ } |
|
41 |
+ }, |
|
42 |
+ |
|
43 |
+ data: { |
|
44 |
+ newLink: { |
|
45 |
+ } |
|
46 |
+ } |
|
47 |
+ }); |
|
48 |
+ </script> |
|
49 |
+</body> |
|
50 |
+ </html> |
|
51 |
+ |
|
52 |
+ |
... | ... |
@@ -3,6 +3,18 @@ |
3 | 3 |
(eval-when (:compile-toplevel :load-toplevel :execute) |
4 | 4 |
(local-time:set-local-time-cl-postgres-readers)) |
5 | 5 |
|
6 |
+(defclass user () |
|
7 |
+ ((id :col-type serial) |
|
8 |
+ (uid :initarg :uid :col-type text) |
|
9 |
+ (email :initarg :email :col-type text) |
|
10 |
+ (name :initarg :name :col-type text) |
|
11 |
+ (moderator :initarg :moderator :initform nil :col-type bool)) |
|
12 |
+ (:metaclass dao-class) |
|
13 |
+ (:keys id)) |
|
14 |
+ |
|
15 |
+(deftable user |
|
16 |
+ (!dao-def)) |
|
17 |
+ |
|
6 | 18 |
(defclass submission () |
7 | 19 |
((headline :initarg :headline :col-type text :initform "" :accessor inangulis::s-headline) |
8 | 20 |
(url :initarg :url :col-type text :initform "" :accessor inangulis::s-url) |
... | ... |
@@ -15,6 +27,10 @@ |
15 | 27 |
(!dao-def)) |
16 | 28 |
|
17 | 29 |
;; (ubiquitous:restore :inangulis) |
30 |
+ |
|
31 |
+;; (with-connection (ubiquitous:value 'db) |
|
32 |
+;; (create-table 'user)) |
|
33 |
+ |
|
18 | 34 |
;; (with-connection (ubiquitous:value 'db) |
19 | 35 |
;; (create-table 'submission)) |
20 | 36 |
|
21 | 37 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,61 @@ |
1 |
+;; This is a minimal web-framework built on ningle that attempts to decouple |
|
2 |
+;; views from controllers and make life more interesting. |
|
3 |
+(in-package :inangulis.web) |
|
4 |
+ |
|
5 |
+(defgeneric controller (name params &key)) |
|
6 |
+(defgeneric view (name model)) |
|
7 |
+(defgeneric run-route (name params &rest r)) |
|
8 |
+ |
|
9 |
+(defmacro setf1 (&body body) |
|
10 |
+ "Make setf a bit nicer" |
|
11 |
+ (list* 'setf (apply #'append body))) |
|
12 |
+ |
|
13 |
+(defmacro defroutes (app &body routes) |
|
14 |
+ (alexandria:once-only (app) |
|
15 |
+ (list* 'setf1 |
|
16 |
+ (loop for ((target &key method) callback) in routes |
|
17 |
+ collect `((ningle:route ,app ,target :method ,(or method :GET)) ,callback))))) |
|
18 |
+ |
|
19 |
+ |
|
20 |
+(defmacro as-route (name &rest r &key &allow-other-keys) |
|
21 |
+ `(lambda (params) (run-route ,name params ,@r))) |
|
22 |
+ |
|
23 |
+ |
|
24 |
+(defmethod run-route (name params &rest r) |
|
25 |
+ (view name (apply #'controller (list* name params r)))) |
|
26 |
+ |
|
27 |
+(defmethod controller (name params &key) |
|
28 |
+ params) |
|
29 |
+ |
|
30 |
+(defmacro define-controller (name (params &rest r &key &allow-other-keys) &body body) |
|
31 |
+ `(defmethod controller ((name (eql ',name)) ,params &key ,@r) |
|
32 |
+ ,@body)) |
|
33 |
+ |
|
34 |
+(defmacro define-view (name (model) &body body) |
|
35 |
+ `(defmethod view ((name (eql ',name)) ,model) |
|
36 |
+ ,@body)) |
|
37 |
+ |
|
38 |
+(defun render-mustache (fn data) |
|
39 |
+ (with-open-file (s (truename fn)) |
|
40 |
+ (let ((template (make-string (file-length s)))) |
|
41 |
+ (read-sequence template s) |
|
42 |
+ (mustache:render* template data)))) |
|
43 |
+ |
|
44 |
+ |
|
45 |
+(defmacro mustache ((template lambda-list data) &body body) |
|
46 |
+ "Template specifies the template to be render, lambda-list is used to destructure data |
|
47 |
+ and body transforms the destructured data into an alist for use in the template" |
|
48 |
+ (alexandria:once-only (template) |
|
49 |
+ `(destructuring-bind ,lambda-list ,data |
|
50 |
+ (render-mustache ,template |
|
51 |
+ (list |
|
52 |
+ ,@(loop for (k v) on body by #'cddr |
|
53 |
+ if (or k v) |
|
54 |
+ collect `(cons ,k ,v))))))) |
|
55 |
+ |
|
56 |
+(defmacro mustache-view (name lambda-list template &body body) |
|
57 |
+ (alexandria:with-gensyms (model) |
|
58 |
+ `(define-view ,name (,model) |
|
59 |
+ (mustache (,template ,lambda-list ,model) |
|
60 |
+ ,@body)))) |
|
61 |
+ |