git.fiddlerwoaroof.com
Browse code

Basic functionality down.

Working prototype stage

fiddlerwoaroof authored on 23/01/2016 01:26:20
Showing 9 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+This is a simple web service for managing a moderated RSS feed of interesting links.
0 2
new file mode 100644
... ...
@@ -0,0 +1,20 @@
1
+;;;; lyangulus.asd
2
+
3
+(asdf:defsystem #:lyangulus
4
+  :description "A simple link-submission-to-rss service"
5
+  :author "socraticum"
6
+  :license "MIT"
7
+  :depends-on (#:alimenta
8
+               #:cl-oid-connect
9
+               #:cl-mustache
10
+               #:parenscript
11
+               #:alexandria
12
+               #:anaphora
13
+               #:ironclad
14
+               #:ubiquitous
15
+               #:ningle)
16
+  :serial t
17
+  :components ((:file "package")
18
+               (:file "lyangulus")))
19
+
20
+;; vim: set ft=lisp:
0 21
new file mode 100644
... ...
@@ -0,0 +1,214 @@
1
+(declaim (optimize (debug 3) (speed 0 ) (safety 3)) )
2
+;;;; lyangulus.lisp
3
+
4
+(in-package #:lyangulus)
5
+(defparameter *submissions* nil)
6
+(defparameter *by-distinct* (make-hash-table :test #'equalp))
7
+(defparameter *users* (make-hash-table :test #'equalp))
8
+
9
+;;; "lyangulus" goes here. Hacks and glory await!
10
+
11
+(defun current-date-string ()
12
+  "Returns current date as a string."
13
+  (multiple-value-bind (sec min hr day mon yr dow dst-p tz)
14
+    (get-decoded-time)
15
+    (declare (ignore dow dst-p))
16
+    (format nil "~4,'0d-~2,'0d-~2,'0d ~2,'0d:~2,'0d:~2,'0d ~2,'0d" yr mon day hr min sec tz)))
17
+
18
+
19
+(defclass user ()
20
+  ((uid :initarg :uid)
21
+   (email :initarg :email)
22
+   (name :initarg :name)
23
+   (moderator :initarg :moderator :initform nil)))
24
+
25
+(defun user-alist (user)
26
+  (with-slots (email name moderator) user
27
+    `(("name" . ,name)
28
+      ("email" . ,email)
29
+      ("moderator" . ,moderator))))
30
+
31
+(defclass submission ()
32
+  ((headline :initarg :headline :initform ""  :accessor s-headline)
33
+   (url      :initarg :url      :initform ""  :accessor s-url)
34
+   (date     :initarg :date     :initform ""  :accessor s-date)
35
+   (approved :initarg :approved :initform "" :accessor s-approved)))
36
+
37
+(defmethod print-object ((obj user) s)
38
+  (print-unreadable-object (obj s :type t :identity t)
39
+    (with-slots (uid email name moderator) obj
40
+      (format s "U: ~s E: ~s N: ~s M: ~s" uid email name moderator))))
41
+
42
+(defmethod print-object ((obj submission) s)
43
+  (print-unreadable-object (obj s :type t :identity t)
44
+    (with-slots (headline url approved) obj
45
+      (format s "H: ~s U: ~s A: ~s" headline url approved))))
46
+
47
+(defun make-submission (headline url &key (approved ""))
48
+  (make-instance 'submission :headline headline :url url :approved approved
49
+                 :date ))
50
+
51
+(defun submission-alist (submission)
52
+  `(("headline". ,(s-headline submission))
53
+    ("url" . ,(s-url submission))
54
+    ("date" . ,(s-url submission))
55
+    ("approved" . ,(s-approved submission))))
56
+
57
+(defun alist-submission (alist &key nil-if-exists (modify t))
58
+  (let* ((result (make-submission (cdr (assoc :headline alist :test #'string-equal))
59
+                                  (cdr (assoc :url alist :test #'string-equal))
60
+                                  :approved (aif (cdr (assoc :approved alist :test #'string-equal)) it "")))
61
+         (key (cons (s-headline result) (s-url result))))
62
+    (aif (gethash key *by-distinct*)
63
+      (progn
64
+        (when modify
65
+          (setf (s-url it) (s-url result)
66
+                (s-headline it) (s-headline result)))
67
+        (if nil-if-exists nil it))
68
+      (progn
69
+        (setf (gethash key *by-distinct*) result)
70
+        result))))
71
+
72
+
73
+(defun get-moderated (feeds)
74
+  (remove-if (lambda (x) (not (equal x "approved"))) feeds :key #'s-approved))
75
+
76
+(defparameter *app* (make-instance 'ningle:<app>))
77
+
78
+(defmacro i-lambda ((&rest args) &body body)
79
+  `(lambda (,@args)
80
+     (declare (ignorable ,@args))
81
+     ,@body))
82
+
83
+(defun render-mustache (fn data)
84
+  (with-open-file (s (truename fn))
85
+    (let ((template (make-string (file-length s))))
86
+      (read-sequence template s)
87
+      (mustache:render* template data))))
88
+
89
+(setf (ningle:route *app* "/")
90
+      (i-lambda (params)
91
+        (ningle.context:with-context-variables (session)
92
+           (handler-case
93
+             (cl-oid-connect.utils:ensure-logged-in
94
+               (cl-oid-connect.utils:redirect-if-necessary session
95
+                 (render-mustache #p"static/index.mustache.html"
96
+                                  `((:links . ,(mapcar #'submission-alist *submissions*))
97
+                                    (:user . ,(user-alist (gethash :app-user session)))))))
98
+             (cl-oid-connect.utils:user-not-logged-in (c) (render-mustache #p"static/index.mustache.html"
99
+                                                                           nil))))))
100
+
101
+(defun submit (params)
102
+  (awhen (alist-submission params :nil-if-exists t)
103
+    (format t "~s <<<" it)
104
+    (push it *submissions*)))
105
+
106
+(defun get-by-key (headline url)
107
+  (gethash (cons headline url) *by-distinct*))
108
+
109
+(setf (ningle:route *app* "/murmuro" :method :POST)
110
+      (i-lambda (params)
111
+        (sleep 0.01)
112
+        (submit params)
113
+        '(302 (:location "/") ("Done"))))
114
+
115
+(setf (ningle:route *app* "/curo" :method :POST)
116
+      (i-lambda (params)
117
+        (cl-oid-connect.utils:require-login
118
+          (alet (alist-submission params :modify nil)
119
+            (let ((approval (string-downcase (cdr (assoc "approved" params :test #'equalp)))))
120
+              (setf (s-approved it)
121
+                    (if (equal approval "+") "approved"
122
+                      (if (equal approval "-") "rejected"))))))
123
+        '(302 (:location "/") ("Done"))))
124
+
125
+(setf (ningle:route *app* "/login" :method :GET)
126
+      (i-lambda (params)
127
+        `(200 ()
128
+          (,(cl-who:with-html-output-to-string (s)
129
+             (:html
130
+               (:head
131
+                 (:title "Login")
132
+                 (:link :rel "stylesheet" :href "/static/css/login.css"))
133
+              (:body
134
+                (:h1 "In Angulis")
135
+                (:div :class "login-buttons"
136
+                 (:a :class "facebook" :href "/login/facebook" "Login With Facebook")))))))))
137
+
138
+(cl-oid-connect:def-route ("/logout" (params) :app *app*)
139
+  (declare (ignore params))
140
+  (ningle:with-context-variables (session)
141
+    (setf (gethash :userinfo session) nil)
142
+    '(302 (:location "/"))))
143
+
144
+(defun get-feed-guid (item)
145
+  (with-slots (alimenta:title alimenta:link) item
146
+    (let ((hasher (ironclad:make-digest 'ironclad:sha256)))
147
+      (ironclad:update-digest hasher (ironclad:ascii-string-to-byte-array alimenta:title))
148
+      (ironclad:update-digest hasher (ironclad:ascii-string-to-byte-array alimenta:link))
149
+      (ironclad:byte-array-to-hex-string (ironclad:produce-digest hasher)))))
150
+
151
+(setf (ningle:route *app* "/feed" :method :GET)
152
+      (i-lambda (params)
153
+        (let ((feed (alimenta::make-feed :title "In Angulis" :link "http://srv2.elangley.org:9090/feed"
154
+                                         :description "Locus in quo sunt illi qui murmurant in angulis")))
155
+          (loop for submission in (reverse (get-moderated *submissions*))
156
+                do (alimenta::add-item-to-feed feed
157
+                                               :title (s-headline submission)
158
+                                               :link (s-url submission)
159
+                                               :date (current-date-string)
160
+                                               :next-id #'get-feed-guid
161
+                                               :content ""))
162
+          `(200 (:content-type "application/rss+xml") (,(plump:serialize (alimenta:generate-xml feed) nil))))))
163
+
164
+(setf (ningle:route *app* "/firehose" :method :GET)
165
+      (i-lambda (params)
166
+        (let ((feed (alimenta::make-feed :title "In Angulis" :link "http://srv2.elangley.org:9090/feed"
167
+                                         :description "Locus in quo sunt illi qui murmurant in angulis")))
168
+          (loop for submission in (reverse *submissions*)
169
+                do (alimenta::add-item-to-feed feed
170
+                                               :title (s-headline submission)
171
+                                               :link (s-url submission)
172
+                                               :date (current-date-string)
173
+                                               :next-id #'get-feed-guid
174
+                                               :content ""))
175
+          `(200 (:content-type "application/rss+xml") (,(plump:serialize (alimenta:generate-xml feed) nil))))))
176
+
177
+(cl-oid-connect::setup-oid-connect *app* (userinfo &rest args)
178
+  (declare (ignore args))
179
+  (let ((id (cdr (assoc :id userinfo))))
180
+    (unless (gethash id *users*)
181
+      (setf (gethash id *users*)
182
+            (alet (make-instance 'user)
183
+              (with-slots (uid name email) it
184
+                (prog1 it
185
+                  (setf uid id
186
+                        name (cdr (assoc :name userinfo))
187
+                        email (cdr (assoc :email userinfo))))))))
188
+    (gethash id *users*)))
189
+
190
+(let ((handler nil))
191
+  (ubiquitous:restore :whitespace)
192
+  (defun stop () (clack:stop (pop handler)))
193
+
194
+  (defun start (&optional tmp)
195
+    (cl-oid-connect:initialize-oid-connect
196
+      (ubiquitous:value 'facebook 'secrets)
197
+      (ubiquitous:value 'google 'secrets))
198
+    (let ((server (if (> (length tmp) 1)
199
+                    (intern (string-upcase (elt tmp 1)) 'keyword)
200
+                    :hunchentoot)))
201
+      (push (clack:clackup
202
+              (lack.builder:builder
203
+                :session
204
+                (:static :path "/static/" :root #p"./static/")
205
+                :backtrace
206
+                *app*
207
+                )
208
+              :port 9090
209
+              :server server)
210
+            handler)))
211
+
212
+  (defun restart-clack ()
213
+    (do () ((null handler)) (stop))
214
+    (start)))
0 215
new file mode 100644
... ...
@@ -0,0 +1,5 @@
1
+;;;; package.lisp
2
+
3
+(defpackage #:lyangulus
4
+  (:use #:cl #:anaphora))
5
+
0 6
new file mode 100644
... ...
@@ -0,0 +1,65 @@
1
+@import url(https://fonts.googleapis.com/css?family=Alegreya+Sans+SC|MedievalSharp&subset=latin,latin-ext);
2
+
3
+* {
4
+  box-sizing: border-box;
5
+  margin: 0px;
6
+  padding: 0px;
7
+  -webkit-font-feature-settings: 'kern' 1, 'liga' 1;
8
+  -moz-font-feature-settings: 'kern' 1;
9
+  -o-font-feature-settings: 'kern' 1;
10
+  text-rendering: geometricPrecision;
11
+  transition: color 0.2s ease-in-out,
12
+              background-color 0.2s ease-in-out;
13
+}
14
+
15
+body {
16
+  position: relative;
17
+  font-size: 14px;
18
+  font-family: 'Alegreya Sans SC', sans-serif;
19
+  padding-bottom: 6em;
20
+}
21
+
22
+h1 {
23
+  letter-spacing: 6px;
24
+  padding: 0.5em;
25
+  margin: 0px;
26
+  margin-top: 0.5em;
27
+  margin-bottom: 1em;
28
+  font-size: 3em;
29
+  background: white;
30
+  width: 100%;
31
+  z-index: 100;
32
+  text-align: center;
33
+  font-variant: small-caps;
34
+  font-family: 'MedievalSharp'
35
+}
36
+
37
+
38
+.login-buttons {
39
+  text-align: center;
40
+}
41
+
42
+.login-buttons a.facebook {
43
+  border: 3px double #888;
44
+  font-size: 1.5em;
45
+  padding: 1em;
46
+  display: inline-block;
47
+  text-decoration: none;
48
+  color: white;
49
+  text-shadow: 0em 0em 0.1em black;
50
+
51
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAABUCAMAAACfvxb8AAAAeFBMVEVOaaJMZ6FIZJ89W5lBXps8WplDYJxNaKFKZqBGYp5HY54/XJpCX5xFYZ07WZg+XJpLZqBAXZtJZZ9EYZ1NaKI8Wpg9WplMaKFNaaJJZJ9LZ6FEYJ1FYp1DX5xAXptJZaBAXZo+W5k+W5pKZaBGY55LZ6A8WZg/XZpJqQD4AAAAVklEQVR4Xk3CBQoDABADwU3d3d37/x8WAqEHI/jboZV1Y2sqDuiNZtaLJ7qihe1RoxjEC/XjhMaxtCPqxBmNolVc0Nxu6IuGMbUHuqNmsbF2rO2DJhg/RwEE6AR0V3kAAAAASUVORK5CYII=);
52
+  background-repeat: repeat-x;
53
+  background-size: 1px 100%;
54
+  background-position: 0 0;
55
+  background-color: #3a5795;
56
+
57
+  letter-spacing: 2px;
58
+  font-variant: small-caps;
59
+  font-weight: bold;
60
+}
61
+
62
+.login-button + * {
63
+  margin-top: 1em;
64
+}
65
+
... ...
@@ -1,6 +1,6 @@
1 1
 /*@import(url(/static/css/baseline.css));*/
2 2
 /*@import url("/css/formalize.css");*/
3
-@import url(https://fonts.googleapis.com/css?family=Alegreya|MedievalSharp&subset=latin,latin-ext);
3
+@import url(https://fonts.googleapis.com/css?family=Alegreya+Sans|MedievalSharp&subset=latin,latin-ext);
4 4
 
5 5
 * {
6 6
   box-sizing: border-box;
... ...
@@ -10,8 +10,7 @@
10 10
   -moz-font-feature-settings: 'kern' 1;
11 11
   -o-font-feature-settings: 'kern' 1;
12 12
   text-rendering: geometricPrecision;
13
-  transition: color 0.2s ease-in-out,
14
-              background-color 0.2s ease-in-out;
13
+  transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
15 14
 }
16 15
 
17 16
 body {
... ...
@@ -22,6 +21,7 @@ body {
22 21
 }
23 22
 
24 23
 h1 {
24
+  letter-spacing: 6px;
25 25
   padding: 0.5em;
26 26
   margin: 0px;
27 27
   margin-top: 0.5em;
... ...
@@ -151,6 +151,8 @@ ul#submissions li + li {
151 151
 
152 152
 ul#submissions a {
153 153
   display: block;
154
+  padding: 0.75em;
155
+  border-radius: 0.25em;
154 156
 }
155 157
 
156 158
 ul#submissions li {
... ...
@@ -174,6 +176,63 @@ ul#submissions li.submission:first-child {
174 176
   margin-top: 3.33em;
175 177
 }
176 178
 
179
+li.submission a.rejected {
180
+  background-color: hsl(0,25%,75%);
181
+}
182
+
183
+li.submission a.approved {
184
+  background-color: hsl(120,25%,75%);
185
+}
186
+
177 187
 a { color: inherit; text-decoration: none;}
178 188
 a:visited { color: hsl(0, 50%, 25%); }
179 189
 a:active, a:hover { text-decoration: underline; }
190
+
191
+.moderation {
192
+  position: absolute;
193
+  bottom: 50%;
194
+  transform: translateY(50%);
195
+  left: -5em;
196
+  width: 4em;
197
+  background: #ddd;
198
+  padding: 0.2em;
199
+  text-align: center;
200
+  border-radius: 0.2em;
201
+}
202
+
203
+.moderation button {
204
+  display: inline-block;
205
+  width: 1.5em;
206
+  height: 1.5em;
207
+  line-height: 1.5em;
208
+
209
+}
210
+
211
+.moderation button + button {
212
+  margin-left: 0.2em;
213
+}
214
+
215
+.userinfo {
216
+  background-color: #eee;
217
+  padding: 0.2em;
218
+  border-radius: 0.2em;
219
+  border: 3px double #888;
220
+  position: absolute;
221
+  font-size: 0.9em;
222
+  right: 1em;
223
+  top: 1em;
224
+  text-align: center;
225
+}
226
+
227
+.userinfo .user {
228
+  font-size: 1.5em;
229
+}
230
+.userinfo .email {
231
+  color: #888;
232
+}
233
+body > div.userinfo > a#logout {
234
+  width: 100%;
235
+  display: block;
236
+  margin-top: 0.5em;
237
+  font-size: 1.25em;
238
+}
... ...
@@ -16,14 +16,20 @@
16 16
   </form>
17 17
 
18 18
   <ul id="submissions">
19
-    <li class="in-progress" v-if="newLink.headline !== undefined || newLink.url !== undefined">
19
+    <li class="in-progress " v-if="newLink.headline !== undefined || newLink.url !== undefined">
20 20
       <a href="#">
21 21
         <h2>{{ newLink.headline }}</h2>
22 22
         <p>{{ newLink.url }}</p>
23 23
       </a>
24 24
     </li>
25 25
     <li class="submission" v-for="link in links">
26
-      <a href="{{ link.url }}">
26
+      <div class="moderation">
27
+        <form action="/curo" method="POST" v-on:submit="moderate">
28
+          <button name="curo" value="+" type="submit">+</button>
29
+          <button name="curo"  value="-"    type="submit">-</button>
30
+        </form>
31
+      </div >
32
+      <a href="{{ link.url }}" class="{{ link.approved || '' }}">
27 33
         <h2>{{ link.headline }}</h2>
28 34
         <p>{{ link.url }}</p>
29 35
       </a>
... ...
@@ -52,11 +58,13 @@ new Vue({
52 58
     },
53 59
     links: [
54 60
     { headline: "Against the axiom of nature’s infinite precision",
55
-      url: "https://thomism.wordpress.com/2016/01/19/against-the-axiom-of-natures-infinite-precision/"},
61
+      url: "https://thomism.wordpress.com/2016/01/19/against-the-axiom-of-natures-infinite-precision/",
62
+      approved: "approved"},
56 63
     { headline: "The opposition between \"strongly held intuitions\" and science",
57 64
       url: "https://thomism.wordpress.com/2010/07/10/the-battle-between-strongly-held-intuitions-and-science/"},
58 65
     { headline: "Against the axiom of nature’s infinite precision",
59
-      url: "https://thomism.wordpress.com/2016/01/19/against-the-axiom-of-natures-infinite-precision/"},
66
+      url: "https://thomism.wordpress.com/2016/01/19/against-the-axiom-of-natures-infinite-precision/",
67
+      approved: "rejected"},
60 68
     { headline: "The opposition between \"strongly held intuitions\" and science",
61 69
       url: "https://thomism.wordpress.com/2010/07/10/the-battle-between-strongly-held-intuitions-and-science/"},
62 70
     { headline: "Against the axiom of nature’s infinite precision",
63 71
new file mode 100644
... ...
@@ -0,0 +1,77 @@
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
+
26
+    <ul id="submissions">
27
+      {{#links}}
28
+      <li class="submission" v-for="link in links">
29
+        {{#user}}
30
+        {{#moderator}}
31
+        <div class="moderation">
32
+          <form action="/curo" method="POST">
33
+            <button name="approved" value="+" type="submit">+</button>
34
+            <button name="approved"  value="-"    type="submit">-</button>
35
+            <input type="hidden" name="headline" value="{{headline}}" />
36
+            <input type="hidden" name="url" value="{{url}}" />
37
+          </form>
38
+        </div>
39
+        {{/moderator}}
40
+        {{/user}}
41
+        <a href="{{ url }}" class="{{ approved }}">
42
+          <h2>{{ headline }}</h2>
43
+          <p>{{ url }}</p>
44
+        </a>
45
+      </li>
46
+      {{/links}}
47
+    </ul>
48
+    </main>
49
+
50
+    <script src="/static/js/jquery.js"></script>
51
+    <script src="/static/js/jquery.formalize.js"></script>
52
+<!--
53
+   -    <script src="/static/js/vue.js"></script>
54
+   -    <script type="text/javascript">
55
+   -new Vue({
56
+   -  el: '#app',
57
+   -
58
+   -  methods: {
59
+   -    submit: function(e) {
60
+   -      e.preventDefault();
61
+   -      this.links.unshift(0,Object.create(this.newLink));
62
+   -      this.newLink = {};
63
+   -      return false;
64
+   -    }
65
+   -  },
66
+   -
67
+   -  data: {
68
+   -    newLink: {
69
+   -    },
70
+   -    links: [ {{ jsondata }} ]
71
+   -  }
72
+   -});
73
+   -    </script>
74
+   -->
75
+  </body>
76
+</html>
77
+
0 78
new file mode 100644
... ...
@@ -0,0 +1,10 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+  <meta charset="UTF-8">
5
+  <title></title>
6
+</head>
7
+<body>
8
+  
9
+</body>
10
+</html>