Browse code
Provide live updating list of 10 most recent users
fiddlerwoaroof authored on 19/10/2015 20:16:55
Showing 10 changed files
Showing 10 changed files
- db/functions.sql
- db/schema.sql
- src/marrow/database.py
- src/marrow/user.py
- static/css/main.css
- static/index.html
- static/js/directives/user-badge/user-badge.html
- static/js/directives/user-badge/user-badge.js
- static/js/new/controller.js
- static/js/new/services.js
... | ... |
@@ -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']) |
... | ... |
@@ -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 |
); |