git.fiddlerwoaroof.com
Browse code

Provide live updating list of 10 most recent users

fiddlerwoaroof authored on 19/10/2015 20:16:55
Showing 10 changed files
... ...
@@ -381,3 +381,4 @@ BEGIN
381 381
   RETURN result;
382 382
 END
383 383
 $$ LANGUAGE plpgsql;
384
+
... ...
@@ -56,3 +56,14 @@ CREATE TABLE user_ak (
56 56
   UNIQUE (user_id, ak),
57 57
   PRIMARY KEY (id)
58 58
 );
59
+
60
+DROP VIEW IF EXISTS recently_active_users;
61
+CREATE VIEW recently_active_users AS
62
+WITH recent_users AS (
63
+  SELECT user_id,name,posted
64
+  FROM user_links
65
+  LEFT JOIN links ON link_id = links.id
66
+  RIGHT JOIN users ON user_id=users.id
67
+  WHERE posted > now() - interval '1 week'
68
+  ORDER BY posted desc, user_id)
69
+SELECT DISTINCT ON (name) user_id,name,posted FROM recent_users;
... ...
@@ -8,21 +8,21 @@ except ImportError:
8 8
         password = "marrowpass"
9 9
         host = "pgsqlserver.elangley.org"
10 10
 
11
-def get_db():
11
+def get_db(close=True):
12 12
     db = getattr(g, '_database', None)
13 13
     if db is None:
14
-        db = g._database = psycopg2.connect(
14
+        db = g._database = [psycopg2.connect(
15 15
           database=config.db,
16 16
           user=config.user,
17 17
           password=config.password,
18 18
           host=config.host
19
-        );
20
-    return db
19
+        ),close];
20
+    return db[0]
21 21
 
22 22
 def close_connection(exception):
23 23
     db = getattr(g, '_database', None)
24
-    if db is not None:
25
-        db.close()
24
+    if db is not None and db[1]:
25
+        db[0].close()
26 26
 
27 27
 def check_ak(db, username, ak):
28 28
     with db.cursor() as cur:
... ...
@@ -2,7 +2,7 @@ from __future__ import division
2 2
 import json
3 3
 
4 4
 import flask
5
-from flask import Blueprint, session, redirect, url_for, escape, request, abort, g
5
+from flask import Blueprint, session, redirect, url_for, escape, request, abort, g, current_app, stream_with_context
6 6
 from flask.ext.cors import cross_origin
7 7
 from flask.ext.login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
8 8
 import psycopg2
... ...
@@ -91,6 +91,26 @@ def follows(to):
91 91
 # def list_users():
92 92
 #     return json.dumps([_ for _ in users.keys()])
93 93
 
94
+class ServerSentEvent(object):
95
+    def __init__(self, id, event, data):
96
+        self.data = data
97
+        self.event = event
98
+        self.id = id
99
+        self.desc_map = {
100
+            self.data : "data",
101
+            self.event : "event",
102
+            self.id : "id"
103
+        }
104
+
105
+    def encode(self):
106
+        if not self.data:
107
+            return ""
108
+        lines = ["%s: %s" % (v, k) 
109
+                 for k, v in self.desc_map.iteritems() if k]
110
+        
111
+        return "%s\n\n" % "\n".join(lines)
112
+
113
+
94 114
 @user_blueprint.route('/add', methods=['POST'])
95 115
 def adduser():
96 116
     db = database.get_db()
... ...
@@ -122,6 +142,47 @@ def adduser():
122 142
             else: db.commit()
123 143
     return json.dumps(result)
124 144
 
145
+import time
146
+@user_blueprint.route('/active')
147
+def active():
148
+    def get_event():
149
+        result = dict(status=False, data=[])
150
+        with database.get_db() as db:
151
+            with db.cursor() as cur:
152
+                cur.execute("SELECT * FROM recently_active_users LIMIT 10")
153
+                result['status'] = True
154
+                store = result['data']
155
+                for id,name,last_posted in cur.fetchall():
156
+                    store.append(
157
+                        dict(
158
+                            id=id,
159
+                            name=name,
160
+                            last_posted=last_posted.isoformat()
161
+                        )
162
+                    )
163
+        return json.dumps(result)
164
+    def poll():
165
+        try:
166
+            id = 1
167
+            t0 = time.time()
168
+            data = get_event()
169
+            ev = ServerSentEvent(id, "active", data).encode()
170
+            yield ev
171
+            while True:
172
+                t1 = time.time()
173
+                if t1 - t0 > 5:
174
+                    id += 1
175
+                    data = get_event()
176
+                    ev = ServerSentEvent(id, "active", data).encode()
177
+                    yield ev
178
+                    time.sleep(0.1)
179
+                    t0 = t1
180
+        except GeneratorExit:
181
+            print 'GeneratorExit!'
182
+    response = flask.Response(stream_with_context(poll()), mimetype="text/event-stream")
183
+    response.headers['X-Accel-Buffering'] = 'no'
184
+    return response
185
+
125 186
 @user_blueprint.route('/following')
126 187
 @login_required
127 188
 def following():
... ...
@@ -143,7 +204,7 @@ import os, base64
143 204
 def gen_ak(db):
144 205
     return ak
145 206
 
146
-@user_blueprint.route('/<user>/env', methods=['POST'])
207
+@user_blueprint.route('/env/<user>', methods=['POST'])
147 208
 def getenv(user): pass
148 209
 
149 210
 @user_blueprint.route('/change-password', methods=['POST'])
... ...
@@ -260,6 +260,11 @@ footer a.facebook-link:hover {
260 260
   margin-top: 10px;
261 261
 }
262 262
 
263
+#nav-main h3 {
264
+  text-align: right;
265
+  margin-top: 20px;
266
+}
267
+
263 268
 /* @end */
264 269
 
265 270
 /* @group List Module */
... ...
@@ -46,64 +46,70 @@
46 46
 
47 47
 </head>
48 48
 <body>
49
-<!--
50
-   -  <script>
51
-   -    window.fbAsyncInit = function() {FB.init({
52
-   -        appId      : '1420153671647757',
53
-   -        xfbml      : true,
54
-   -        version    : 'v2.3'
55
-   -      });
56
-   -    };
57
-   -
58
-   -    (function(d, s, id){
59
-   -      var js, fjs = d.getElementsByTagName(s)[0];
60
-   -      if (d.getElementById(id)) {return;}
61
-   -      js = d.createElement(s); js.id = id;
62
-   -      js.src = "//connect.facebook.net/en_US/sdk.js";
63
-   -      fjs.parentNode.insertBefore(js, fjs);
64
-   -    }(document, 'script', 'facebook-jssdk'));
65
-   -  </script>
66
-   -
67
-   -->
68
-  <div id="page-container">
69
-    <main ng-view></main>
70
-    <header>
71
-      <h1 class="site-logo">Marrow</h1>
72
-    </header>
73
-    <nav id="nav-main" ng-controller="SidebarCtrl">
74
-      <ul>
75
-        <li><a href="/">Home</a></li>
76
-        <li><a href ng-click="subscriptions()">My Lists</a></li>
77
-        <li><a href ng-click="random()">Random</a></li>
78
-        <li class="logout-link"><a href ng-click="logout()">Log Out</a></li>
79
-      </ul>
80
-      <!--
81
-         -<div class="appstore-links">
82
-         -  <a href="https://play.google.com/store/apps/details?id=com.joinmarrow.marrow">
83
-         -    <img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" />
84
-         -  </a>
85
-         -</div>
86
-         -->
87
-    </nav>
88
-    <footer>
89
-      <span class="beta-message">
90
-        This service is currently in beta. Like us on
91
-        <a class="facebook-link" href="https://www.facebook.com/join.marrow">Facebook.</a>
92
-        Try the
93
-        <a style="color: blue"
94
-           href="https://chrome.google.com/webstore/detail/add-to-marrow/pcgflajngpeopkemlijnnggfchoglpad">
95
-          Chrome Extension
96
-        </a>
97
-        to get the newest links delivered straight to your browser.
98
-        <!--or report problems on <a class="facebook-link" href="https://bugs.joinmarrow.com">bugs.joinmarrow.com</a>.-->
99
-      </span>
100
-    </footer>
101
-  </div>
102
-  <script>
103
-    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
104
-        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
105
-        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
106
-            })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
49
+  <!--
50
+    -  <script>
51
+-    window.fbAsyncInit = function() {FB.init({
52
+  -        appId      : '1420153671647757',
53
+  -        xfbml      : true,
54
+  -        version    : 'v2.3'
55
+    -      });
56
+-    };
57
+-
58
+-    (function(d, s, id){
59
+  -      var js, fjs = d.getElementsByTagName(s)[0];
60
+  -      if (d.getElementById(id)) {return;}
61
+  -      js = d.createElement(s); js.id = id;
62
+  -      js.src = "//connect.facebook.net/en_US/sdk.js";
63
+  -      fjs.parentNode.insertBefore(js, fjs);
64
+  -    }(document, 'script', 'facebook-jssdk'));
65
+-  </script>
66
+    -
67
+    -->
68
+    <div id="page-container">
69
+      <main ng-view></main>
70
+      <header>
71
+        <h1 class="site-logo">Marrow</h1>
72
+      </header>
73
+      <nav id="nav-main" ng-controller="SidebarCtrl">
74
+        <ul>
75
+          <li><a href="/">Home</a></li>
76
+          <li><a href ng-click="subscriptions()">My Lists</a></li>
77
+          <li><a href ng-click="random()">Random</a></li>
78
+          <li class="logout-link"><a href ng-click="logout()">Log Out</a></li>
79
+        </ul>
80
+        <h3>Active Users</h3>
81
+        <ul>
82
+          <li ng-repeat="user in activeUsers.users">
83
+            <user-badge poster="{{user.name}}" no-image></user-badge>
84
+          </li>
85
+        </ul>
86
+        <!--
87
+          -<div class="appstore-links">
88
+          -  <a href="https://play.google.com/store/apps/details?id=com.joinmarrow.marrow">
89
+          -    <img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" />
90
+          -  </a>
91
+          -</div>
92
+        -->
93
+      </nav>
94
+      <footer>
95
+        <span class="beta-message">
96
+          This service is currently in beta. Like us on
97
+          <a class="facebook-link" href="https://www.facebook.com/join.marrow">Facebook.</a>
98
+          Try the
99
+          <a style="color: blue"
100
+             href="https://chrome.google.com/webstore/detail/add-to-marrow/pcgflajngpeopkemlijnnggfchoglpad">
101
+            Chrome Extension
102
+          </a>
103
+          to get the newest links delivered straight to your browser.
104
+          <!--or report problems on <a class="facebook-link" href="https://bugs.joinmarrow.com">bugs.joinmarrow.com</a>.-->
105
+        </span>
106
+      </footer>
107
+    </div>
108
+    <script>
109
+(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
110
+  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
111
+  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
112
+})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
107 113
 
108 114
     ga('create', 'UA-61547817-1', 'auto');
109 115
       //ga('send', 'pageview');
... ...
@@ -1,4 +1,5 @@
1
-<a class="avatar-image" ng-href="/user/{{ poster }}" title="{{poster}}">
1
+<a ng-if="!withoutImage" class="avatar-image" ng-href="/user/{{ poster }}" title="{{poster}}">
2 2
   <gravatar-image class="poster-avatar" user-name="{{poster}}"></gravatar-image>
3 3
   <span class="poster-handle de-emphasize">{{poster}}</span>
4 4
 </a>
5
+<a ng-if="withoutImage" ng-href="/user/{{poster}}" title="{{poster}}">{{poster}}</a>
... ...
@@ -16,10 +16,12 @@ angular.module('marrowApp.directives.userBadge', ['marrowApp.utils'])
16 16
 .directive('userBadge', function() {
17 17
   return {
18 18
     scope: {
19
-      poster: '@'
19
+      poster: '@',
20
+      noImage: '@'
20 21
     },
21 22
     templateUrl: '/js/directives/user-badge/user-badge.html',
22
-    controller: function($scope) {
23
+    controller: function($scope,$attrs) {
24
+      $scope.withoutImage = $attrs.hasOwnProperty('noImage');
23 25
       $scope.gravURL = function(uid) {
24 26
         var hash = CryptoJS.MD5(uid);
25 27
         return '//gravatar.com/avatar/'+hash+'?d=identicon&s=24';
... ...
@@ -264,7 +264,14 @@ marrowApp.controller('UserSettingCtrl', function ($scope,$http,$location) {
264 264
   };
265 265
 });
266 266
 
267
-marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window) {
267
+marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window, UserService) {
268
+  eventSource = new EventSource("/api/user/active");
269
+  $scope.activeUsers = Object.create(null);
270
+  eventSource.addEventListener("active", function(event) {
271
+    console.log(event);
272
+    $scope.activeUsers.users = JSON.parse(event.data).data;
273
+  });
274
+
268 275
   $scope.subscriptions = function() {
269 276
     if ($location.url() !== '/subscriptions') { $location.url('/subscriptions'); }
270 277
     else { $route.reload(); }
... ...
@@ -38,7 +38,8 @@ serviceModule.factory('UserService', ['$resource',
38 38
       environment: {'method': 'POST', 'url': '/api/user/environment'},
39 39
       reputation: {'method': 'POST', 'url': '/api/user/reputation/:user', params: {user: '@user'}},
40 40
       reputations: {'method': 'POST', 'url': '/api/user/reputation'},
41
-      changePassword: {'method': 'POST', 'url': '/api/user/change-password'}
41
+      changePassword: {'method': 'POST', 'url': '/api/user/change-password'},
42
+      active: {'method': 'GET', 'url': '/api/user/active'}
42 43
     });
43 44
   }]
44 45
 );