Browse code
Merge branch 'production'
fiddlerwoaroof authored on 02/02/2016 05:16:05
Showing 57 changed files
Showing 57 changed files
- db/functions.sql
- db/schema.sql
- requirements.txt
- sample_data/db_creds
- src/main.py
- src/marrow/bone.py
- src/marrow/database.py
- src/marrow/titlegetter/default.py
- src/marrow/titlegetter/nytimes.py
- src/marrow/titlegetter/titlegetter.py
- src/marrow/user.py
- static/css/main.css
- static/images/icons/android-chrome-144x144.png
- static/images/icons/android-chrome-192x192.png
- static/images/icons/android-chrome-36x36.png
- static/images/icons/android-chrome-48x48.png
- static/images/icons/android-chrome-72x72.png
- static/images/icons/android-chrome-96x96.png
- static/images/icons/apple-touch-icon-114x114.png
- static/images/icons/apple-touch-icon-120x120.png
- static/images/icons/apple-touch-icon-144x144.png
- static/images/icons/apple-touch-icon-152x152.png
- static/images/icons/apple-touch-icon-180x180.png
- static/images/icons/apple-touch-icon-57x57.png
- static/images/icons/apple-touch-icon-60x60.png
- static/images/icons/apple-touch-icon-72x72.png
- static/images/icons/apple-touch-icon-76x76.png
- static/images/icons/apple-touch-icon-precomposed.png
- static/images/icons/apple-touch-icon.png
- static/images/icons/browserconfig.xml
- static/images/icons/favicon-16x16.png
- static/images/icons/favicon-194x194.png
- static/images/icons/favicon-32x32.png
- static/images/icons/favicon-96x96.png
- static/images/icons/favicon.ico
- static/images/icons/manifest.json
- static/images/icons/mstile-144x144.png
- static/images/icons/mstile-150x150.png
- static/images/icons/mstile-310x150.png
- static/images/icons/mstile-310x310.png
- static/images/icons/mstile-70x70.png
- static/images/icons/safari-pinned-tab.svg
- static/index.html
- static/js/directives/bone-list/bone-list.html
- static/js/directives/bone-list/bone-list.js
- static/js/directives/user-badge/user-badge.html
- static/js/directives/user-badge/user-badge.js
- static/js/new/controller.js
- static/js/new/controller.js.orig
- static/js/new/login.js
- static/js/new/login.js.orig
- static/js/new/services.js
- static/login.html
- static/login.html.orig
- static/partials/default.html
- static/partials/random.html
- static/partials/subscription.html
... | ... |
@@ -14,6 +14,24 @@ END |
14 | 14 |
$$ LANGUAGE plpgsql; |
15 | 15 |
|
16 | 16 |
|
17 |
+DROP FUNCTION IF EXISTS subscribe_link(text,int); |
|
18 |
+CREATE OR REPLACE FUNCTION subscribe_link(username text, linkid int) |
|
19 |
+ RETURNS bool AS $$ |
|
20 |
+DECLARE |
|
21 |
+ uid int; |
|
22 |
+ result bool; |
|
23 |
+ n_title text; |
|
24 |
+ n_url text; |
|
25 |
+BEGIN |
|
26 |
+ SELECT INTO result NOT exists(SELECT * FROM user_links WHERE user_id=uid AND link_id=linkid); |
|
27 |
+ IF result THEN |
|
28 |
+ SELECT title,url INTO n_title,n_url FROM links WHERE links.id=linkid; |
|
29 |
+ PERFORM put_link(username, n_url, n_title); |
|
30 |
+ END IF; |
|
31 |
+ RETURN result; |
|
32 |
+END |
|
33 |
+$$ LANGUAGE plpgsql; |
|
34 |
+ |
|
17 | 35 |
DROP FUNCTION IF EXISTS delete_link(text,int); |
18 | 36 |
CREATE OR REPLACE FUNCTION delete_link(username text, linkid int) |
19 | 37 |
RETURNS bool AS $$ |
... | ... |
@@ -30,10 +48,25 @@ BEGIN |
30 | 48 |
END |
31 | 49 |
$$ LANGUAGE plpgsql; |
32 | 50 |
|
51 |
+DROP FUNCTION IF EXIST has_user_shared(text, text); |
|
52 |
+CREATE OR REPLACE FUNCTION has_user_shared(username text, linkurl text) RETURNS bool AS $$ |
|
53 |
+DECLARE |
|
54 |
+ result bool; |
|
55 |
+BEGIN |
|
56 |
+ SELECT INTO result EXISTS( |
|
57 |
+ SELECT 1 FROM user_links ul |
|
58 |
+ LEFT JOIN users u ON user_id=u.id |
|
59 |
+ LEFT JOIN links l ON link_id=l.id |
|
60 |
+ WHERE linkurl=l.url AND username=u.name |
|
61 |
+ ); |
|
62 |
+ RETURN result; |
|
63 |
+END |
|
64 |
+$$ LANGUAGE plpgsql; |
|
65 |
+ |
|
33 | 66 |
DROP FUNCTION IF EXISTS get_bones(text, timestamp, int); |
34 | 67 |
DROP FUNCTION IF EXISTS get_bones(text, int, timestamp); |
35 | 68 |
CREATE OR REPLACE FUNCTION get_bones(username text, lim int, before timestamp) |
36 |
- RETURNS TABLE(url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int) AS $$ |
|
69 |
+ RETURNS TABLE(linkid int, url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int, shared bool) AS $$ |
|
37 | 70 |
DECLARE |
38 | 71 |
subscriber_id int; |
39 | 72 |
BEGIN |
... | ... |
@@ -43,9 +76,12 @@ BEGIN |
43 | 76 |
AS |
44 | 77 |
SELECT |
45 | 78 |
DISTINCT ON (links.url) |
46 |
- links.url,links.title,links.posted,users1.name,total_votes(links.id),user_vote(subscriber_id,links.id) |
|
79 |
+ links.id,links.url,links.title,links.posted,users1.name, |
|
80 |
+ total_votes(links.id),user_vote(subscriber_id,links.id), |
|
81 |
+ has_user_shared(username, links.url) |
|
47 | 82 |
FROM user_subscriptions |
48 | 83 |
RIGHT JOIN user_links ON user_subscriptions.to_id=user_links.user_id |
84 |
+ INNER JOIN links ON link_id=links.id |
|
49 | 85 |
INNER JOIN users ON users.id=fro_id |
50 | 86 |
LEFT JOIN users as users1 ON users1.id=to_id |
51 | 87 |
WHERE fro_id = subscriber_id |
... | ... |
@@ -57,7 +93,8 @@ $$ LANGUAGE plpgsql; |
57 | 93 |
|
58 | 94 |
DROP FUNCTION IF EXISTS get_bones(text, int); |
59 | 95 |
CREATE OR REPLACE FUNCTION get_bones(username text, lim int) |
60 |
- RETURNS TABLE(url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int) AS $$ |
|
96 |
+ RETURNS TABLE(linkid int, url text, title text, posted timestamp, poster text, total_votes bigint, |
|
97 |
+ subscriber_vote int, shared bool) AS $$ |
|
61 | 98 |
DECLARE |
62 | 99 |
subscriber_id int; |
63 | 100 |
BEGIN |
... | ... |
@@ -67,7 +104,9 @@ BEGIN |
67 | 104 |
AS |
68 | 105 |
SELECT |
69 | 106 |
DISTINCT ON (links.url) |
70 |
- links.url,links.title,links.posted,users1.name,total_votes(links.id),user_vote(subscriber_id,links.id) |
|
107 |
+ links.id,links.url,links.title,links.posted,users1.name, |
|
108 |
+ total_votes(links.id),user_vote(subscriber_id,links.id), |
|
109 |
+ has_user_shared(username, links.url) |
|
71 | 110 |
FROM user_subscriptions |
72 | 111 |
RIGHT JOIN user_links ON user_subscriptions.to_id=user_links.user_id |
73 | 112 |
INNER JOIN links ON link_id=links.id |
... | ... |
@@ -80,7 +119,8 @@ $$ LANGUAGE plpgsql; |
80 | 119 |
|
81 | 120 |
DROP FUNCTION IF EXISTS get_bones(text); |
82 | 121 |
CREATE OR REPLACE FUNCTION get_bones(username text) |
83 |
- RETURNS TABLE(url text, title text, posted timestamp, poster text, total_votes bigint, subscriber_vote int) AS $$ |
|
122 |
+ RETURNS TABLE(linkid int, url text, title text, posted timestamp, poster text, total_votes bigint, |
|
123 |
+ subscriber_vote int, shared bool) AS $$ |
|
84 | 124 |
DECLARE |
85 | 125 |
subscriber_id int; |
86 | 126 |
BEGIN |
... | ... |
@@ -90,7 +130,9 @@ BEGIN |
90 | 130 |
AS |
91 | 131 |
SELECT |
92 | 132 |
DISTINCT ON (links.url) |
93 |
- links.url,links.title,links.posted,users1.name,total_votes(links.id),user_vote(subscriber_id,links.id) |
|
133 |
+ links.id,links.url,links.title,links.posted,users1.name, |
|
134 |
+ total_votes(links.id),user_vote(subscriber_id,links.id), |
|
135 |
+ has_user_shared(username, links.url) |
|
94 | 136 |
FROM user_subscriptions |
95 | 137 |
RIGHT JOIN user_links ON user_subscriptions.to_id=user_links.user_id |
96 | 138 |
INNER JOIN links ON link_id=links.id |
... | ... |
@@ -103,7 +145,7 @@ $$ LANGUAGE plpgsql; |
103 | 145 |
|
104 | 146 |
DROP FUNCTION IF EXISTS get_bone(text); |
105 | 147 |
CREATE OR REPLACE FUNCTION get_bone(username text) |
106 |
- RETURNS TABLE(name text, url text, title text, posted timestamp, linkid int, votes bigint) AS $$ |
|
148 |
+ RETURNS TABLE(name text, url text, title text, posted timestamp, linkid int, votes bigint, shared bool) AS $$ |
|
107 | 149 |
BEGIN |
108 | 150 |
RETURN QUERY SELECT users.name, links.url, links.title, links.posted, links.id, total_votes(links.id) |
109 | 151 |
FROM users |
... | ... |
@@ -380,3 +422,4 @@ BEGIN |
380 | 422 |
RETURN result; |
381 | 423 |
END |
382 | 424 |
$$ LANGUAGE plpgsql; |
425 |
+ |
... | ... |
@@ -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 (user_id) user_id,name,posted FROM recent_users; |
... | ... |
@@ -1,12 +1,13 @@ |
1 | 1 |
Flask==0.10.1 |
2 |
-Flask-Cors==2.1.0 |
|
3 |
-Flask-Limiter==0.8.1 |
|
4 |
-Flask-Login==0.3.0 |
|
2 |
+Flask-Cors==2.1.2 |
|
3 |
+Flask-Limiter==0.9.1 |
|
4 |
+Flask-Login==0.3.2 |
|
5 | 5 |
Flask-OAuth==0.12 |
6 |
-Flask-Security==1.7.4 |
|
7 |
-Flask-WTF==0.11 |
|
8 |
-lxml==3.4.4 |
|
6 |
+Flask-Security==1.7.5 |
|
7 |
+Flask-WTF==0.12 |
|
8 |
+lxml==3.5.0 |
|
9 | 9 |
psycopg2==2.6.1 |
10 | 10 |
python-dateutil==2.4.2 |
11 |
-textblob==0.9.1 |
|
12 |
-uWSGI==2.0.11 |
|
11 |
+requests==2.9.1 |
|
12 |
+textblob==0.11.0 |
|
13 |
+uWSGI==2.0.12 |
... | ... |
@@ -19,9 +19,11 @@ except ImportError: |
19 | 19 |
secret_key = base64.b64encode(os.urandom(24)) |
20 | 20 |
debug = False |
21 | 21 |
static_root = os.path.join(os.path.dirname(__file__), os.path.pardir, 'static') |
22 |
+ server_name = "localhost" |
|
22 | 23 |
|
23 | 24 |
app.secret_key = config.secret_key |
24 | 25 |
app.debug = config.debug |
26 |
+app.config["SERVER_NAME"] = config.server_name |
|
25 | 27 |
|
26 | 28 |
limiter = Limiter(app) |
27 | 29 |
limiter.limit("60/hour 3/second", key_func=lambda: request.host)(user.user_blueprint) |
... | ... |
@@ -1,5 +1,6 @@ |
1 | 1 |
import flask |
2 | 2 |
from flask import Blueprint, session, redirect, url_for, escape, request, abort, g |
3 |
+import flask_login; |
|
3 | 4 |
from flask.ext.cors import cross_origin |
4 | 5 |
from flask.ext.login import login_required, current_user |
5 | 6 |
import urllib2 |
... | ... |
@@ -31,7 +32,7 @@ def as_json(f): |
31 | 32 |
return res |
32 | 33 |
return _inner |
33 | 34 |
|
34 |
-@bone_blueprint.route('/link/<linkid>', methods=['GET','DELETE']) |
|
35 |
+@bone_blueprint.route('/link/<linkid>', methods=['GET','POST','DELETE']) |
|
35 | 36 |
@login_required |
36 | 37 |
def delete_link(linkid): |
37 | 38 |
db = database.get_db() |
... | ... |
@@ -42,6 +43,11 @@ def delete_link(linkid): |
42 | 43 |
cur.execute('SELECT id,url,title,posted FROM links WHERE id=%s', (linkid,)) |
43 | 44 |
nid,url,title,posted = cur.fetchone() |
44 | 45 |
result = dict(id=nid,url=url,title=title,posted=posted.isoformat()) |
46 |
+ elif request.method == 'POST': |
|
47 |
+ result = False |
|
48 |
+ if 'username' in session: |
|
49 |
+ cur.execute('SELECT subscribe_link(%s,%s)', (current_user.id,linkid)) |
|
50 |
+ result = cur.fetchone()[0] |
|
45 | 51 |
elif request.method == 'DELETE': |
46 | 52 |
result = False |
47 | 53 |
if 'username' in session: |
... | ... |
@@ -56,7 +62,7 @@ def clean_url(url): |
56 | 62 |
netloc, path = path, netloc |
57 | 63 |
return urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) |
58 | 64 |
|
59 |
-def get_title(url): |
|
65 |
+def get_siteinfo(url): |
|
60 | 66 |
return config.titlegetter.get_title(url) |
61 | 67 |
|
62 | 68 |
@bone_blueprint.route('/vote/total') |
... | ... |
@@ -135,7 +141,8 @@ def submit_link(): |
135 | 141 |
if username is not None: |
136 | 142 |
url, title = obj['url'],obj['title'] |
137 | 143 |
url = clean_url(url) |
138 |
- title = get_title(url) |
|
144 |
+ title, url = get_siteinfo(url) # this makes sure that the url is the site's preferred URL |
|
145 |
+ # TODO: this might need sanity checks . . . like make sure same site? |
|
139 | 146 |
with db.cursor() as cur: |
140 | 147 |
cur.callproc('put_link', (username, url, title)) |
141 | 148 |
## This returns (link_id, user_id) |
... | ... |
@@ -151,9 +158,9 @@ def submit_link(): |
151 | 158 |
@bone_blueprint.route('', methods=['GET']) |
152 | 159 |
@login_required |
153 | 160 |
def default_data(): |
154 |
- result = '', 401, {} |
|
155 |
- if 'username' in session: |
|
156 |
- result = data(current_user.id) |
|
161 |
+ print current_user.id |
|
162 |
+ print 'username' in session |
|
163 |
+ result = data(current_user.id) |
|
157 | 164 |
return result |
158 | 165 |
|
159 | 166 |
@bone_blueprint.route('/u/<username>', methods=['GET']) |
... | ... |
@@ -162,10 +169,14 @@ def data(username): |
162 | 169 |
|
163 | 170 |
result = {'marrow':[], 'sectionTitle': sectionTitle} |
164 | 171 |
with database.get_db().cursor() as cur: |
165 |
- cur.execute("SELECT url, title, posted, linkid, votes from get_bone(%s);", (username,)) |
|
172 |
+ cur_username = 'anonymous' |
|
173 |
+ if current_user.is_authenticated: |
|
174 |
+ cur_username = current_user.id |
|
175 |
+ cur.execute("SELECT url, title, posted, linkid, votes, has_user_shared(%s, url) from get_bone(%s);", |
|
176 |
+ (cur_username, username,)) |
|
166 | 177 |
result['marrow'] = [ |
167 |
- dict(id=linkid, url=url,title=title,posted=posted.isoformat(),votes=votes) |
|
168 |
- for url,title,posted,linkid,votes |
|
178 |
+ dict(id=linkid, url=url,title=title,posted=posted.isoformat(),votes=votes,shared=shared) |
|
179 |
+ for url,title,posted,linkid,votes,shared |
|
169 | 180 |
in cur.fetchall() |
170 | 181 |
] |
171 | 182 |
return json.dumps(result) |
... | ... |
@@ -229,8 +240,9 @@ def subscriptions(before, count): |
229 | 240 |
args = args + (before,) |
230 | 241 |
cur.callproc("get_bones", args) |
231 | 242 |
result['marrow'] = [ |
232 |
- dict(poster=poster, url=url,title=title,posted=posted.isoformat(), votes=votes, myVote=myvote) |
|
233 |
- for url,title,posted,poster,votes,myvote |
|
243 |
+ dict(id=id,poster=poster, url=url,title=title,posted=posted.isoformat(), votes=votes, |
|
244 |
+ myVote=myvote, shared=shared) |
|
245 |
+ for id,url,title,posted,poster,votes,myvote,shared |
|
234 | 246 |
in cur.fetchall() |
235 | 247 |
] |
236 | 248 |
return (json.dumps(result), 200, {'Content-Type': 'application/json'}) |
... | ... |
@@ -1,28 +1,31 @@ |
1 | 1 |
from flask import g |
2 | 2 |
import psycopg2 |
3 |
+ |
|
3 | 4 |
try: from marrow_config import config |
4 | 5 |
except ImportError: |
5 | 6 |
class config: |
6 |
- db = "marrow" |
|
7 |
- user = "marrow" |
|
8 |
- password = "marrowpass" |
|
9 |
- host = "pgsqlserver.elangley.org" |
|
7 |
+ class db: |
|
8 |
+ db = "marrow" |
|
9 |
+ user = "marrow" |
|
10 |
+ password = "marrowpass" |
|
11 |
+ host = "pgsqlserver.elangley.org" |
|
10 | 12 |
|
11 |
-def get_db(): |
|
13 |
+def get_db(close=True): |
|
12 | 14 |
db = getattr(g, '_database', None) |
15 |
+ _config = config.db |
|
13 | 16 |
if db is None: |
14 |
- db = g._database = psycopg2.connect( |
|
15 |
- database=config.db, |
|
16 |
- user=config.user, |
|
17 |
- password=config.password, |
|
18 |
- host=config.host |
|
19 |
- ); |
|
20 |
- return db |
|
17 |
+ db = g._database = [psycopg2.connect( |
|
18 |
+ database=_config.db, |
|
19 |
+ user=_config.user, |
|
20 |
+ password=_config.password, |
|
21 |
+ host=_config.host |
|
22 |
+ ),close]; |
|
23 |
+ return db[0] |
|
21 | 24 |
|
22 | 25 |
def close_connection(exception): |
23 | 26 |
db = getattr(g, '_database', None) |
24 |
- if db is not None: |
|
25 |
- db.close() |
|
27 |
+ if db is not None and db[1]: |
|
28 |
+ db[0].close() |
|
26 | 29 |
|
27 | 30 |
def check_ak(db, username, ak): |
28 | 31 |
with db.cursor() as cur: |
... | ... |
@@ -1,7 +1,8 @@ |
1 |
-import lxml.html |
|
2 |
-import urllib2 |
|
3 |
-import urlparse |
|
4 | 1 |
import re |
2 |
+import urlparse |
|
3 |
+ |
|
4 |
+import lxml.html |
|
5 |
+import requests |
|
5 | 6 |
|
6 | 7 |
from textblob import TextBlob |
7 | 8 |
from textblob_aptagger import PerceptronTagger |
... | ... |
@@ -12,21 +13,41 @@ def titlecase(line): |
12 | 13 |
|
13 | 14 |
class DefaultTitleGetter(object): |
14 | 15 |
url_cleaner = re.compile('[+\-_]') |
16 |
+ user_agent = {'User-Agent': 'Marrow Title Getter: https://joinmarrow.com'} |
|
15 | 17 |
|
16 | 18 |
def get_title(self, url): |
19 |
+ s = requests.session() |
|
17 | 20 |
scheme, netloc, path, params, query, fragment = urlparse.urlparse(url, 'http') |
18 |
- data = urllib2.urlopen(url) |
|
19 |
- content_type = data.headers['content-type'].lower() |
|
20 |
- charset = 'utf-8' |
|
21 |
- if 'charset' in content_type: |
|
22 |
- charset = content_type.partition('charset=')[-1] |
|
23 |
- data = data.read() |
|
24 |
- data = data.decode(charset) |
|
25 |
- etree = lxml.html.fromstring(data) |
|
26 |
- titleElems = etree.xpath('//title') |
|
27 |
- title = url |
|
28 |
- if titleElems != []: |
|
29 |
- title = titleElems[0].text |
|
21 |
+ data = s.get(url, headers=self.user_agent) |
|
22 |
+ etree = lxml.html.fromstring(data.content.decode(data.encoding)) |
|
23 |
+ |
|
24 |
+ canonicalLink = etree.xpath('//link[@rel="canonical"]/@href') |
|
25 |
+ oetree = etree |
|
26 |
+ if canonicalLink != []: |
|
27 |
+ canonicalLink = canonicalLink[0] |
|
28 |
+ try: |
|
29 |
+ data = s.get(canonicalLink, headers=self.user_agent) |
|
30 |
+ etree = lxml.html.fromstring(data.content.decode(data.encoding)) |
|
31 |
+ except requests.exceptions.MissingSchema: |
|
32 |
+ nscheme, nnetloc, npath, nparams, nquery, nfragment = urlparse.urlparse(canonicalLink) |
|
33 |
+ if nscheme == '': |
|
34 |
+ nscheme = scheme |
|
35 |
+ if nnetloc == '': |
|
36 |
+ nnetloc = netloc |
|
37 |
+ canonicalLink = urlparse.urlunparse((nscheme, nnetloc, npath, nparams, nquery, nfragment)) |
|
38 |
+ try: |
|
39 |
+ data = s.get(canonicalLink, headers=self.user_agent) |
|
40 |
+ etree = lxml.html.fromstring(data.content.decode(data.encoding)) |
|
41 |
+ except IOError: |
|
42 |
+ etree = oetree |
|
43 |
+ except IOError: |
|
44 |
+ etree = oetree |
|
45 |
+ else: |
|
46 |
+ canonicalLink = url |
|
47 |
+ |
|
48 |
+ title = etree.xpath('//title/text()') |
|
49 |
+ if title != []: |
|
50 |
+ title = title[0] |
|
30 | 51 |
elif path: |
31 | 52 |
# hacky way to make a title |
32 | 53 |
path = urlparse.unquote(path) |
... | ... |
@@ -37,4 +58,4 @@ class DefaultTitleGetter(object): |
37 | 58 |
title = map(titlecase, path) |
38 | 59 |
title = u' \u2014 '.join(title) |
39 | 60 |
title = u' \u2014 '.join([title, netloc]) |
40 |
- return title |
|
61 |
+ return title, canonicalLink |
... | ... |
@@ -2,6 +2,7 @@ import urlparse |
2 | 2 |
import urllib2 |
3 | 3 |
import json |
4 | 4 |
|
5 |
+# TODO: this should use the articlesearch API, if this is actually necessary |
|
5 | 6 |
class TimesTitleGetter(object): |
6 | 7 |
api_url='http://api.nytimes.com/svc/news/v3/content.json?url=%(url)s&api-key=%(api_key)s' |
7 | 8 |
site='nytimes.com' |
... | ... |
@@ -13,4 +14,4 @@ class TimesTitleGetter(object): |
13 | 14 |
info = json.load(urllib2.urlopen(api_url)) |
14 | 15 |
title = info['results'][0]['title'] |
15 | 16 |
source = info['results'][0]['source'] |
16 |
- return u'%s \u2014 %s' % (title, source) |
|
17 |
+ return u'%s \u2014 %s' % (title, source), url |
... | ... |
@@ -1,4 +1,5 @@ |
1 | 1 |
import urllib2 |
2 |
+import requests |
|
2 | 3 |
import urlparse |
3 | 4 |
|
4 | 5 |
from .utils import memoize |
... | ... |
@@ -34,11 +35,10 @@ class TitleGetter(object): |
34 | 35 |
handler = self.getters[site] |
35 | 36 |
break |
36 | 37 |
|
37 |
- title = None |
|
38 | 38 |
try: |
39 |
- title = handler.get_title(url) |
|
40 |
- except urllib2.HTTPError: |
|
41 |
- title = self.default_handler.get_title(url) |
|
39 |
+ title, canonicalUrl = handler.get_title(url) |
|
40 |
+ except requests.exceptions.RequestException: |
|
41 |
+ title, canonicalUrl = self.default_handler.get_title(url) |
|
42 | 42 |
|
43 |
- return title.encode('utf-8') |
|
43 |
+ return title.encode('utf-8'), canonicalUrl |
|
44 | 44 |
|
... | ... |
@@ -111,6 +111,7 @@ def adduser(): |
111 | 111 |
session['username'] = username |
112 | 112 |
result['status'] = True |
113 | 113 |
_get_users() |
114 |
+ login_user(User.get_user(username)) |
|
114 | 115 |
except psycopg2.IntegrityError as e: |
115 | 116 |
db.rollback() |
116 | 117 |
if e.pgcode == '23505': #username not unique |
... | ... |
@@ -121,6 +122,23 @@ def adduser(): |
121 | 122 |
else: db.commit() |
122 | 123 |
return json.dumps(result) |
123 | 124 |
|
125 |
+@user_blueprint.route('/active') |
|
126 |
+def active(): |
|
127 |
+ result = dict(status=False, data=[]) |
|
128 |
+ with database.get_db() as db: |
|
129 |
+ with db.cursor() as cur: |
|
130 |
+ cur.execute("SELECT * FROM recently_active_users ORDER BY posted DESC LIMIT 10") |
|
131 |
+ store = result['data'] |
|
132 |
+ for id,name,last_posted in cur.fetchall(): |
|
133 |
+ store.append( |
|
134 |
+ dict( |
|
135 |
+ id=id, |
|
136 |
+ name=name, |
|
137 |
+ last_posted=last_posted.isoformat() |
|
138 |
+ ) |
|
139 |
+ ) |
|
140 |
+ return (json.dumps(result), 200, {'Content-Type': 'application/json'}) |
|
141 |
+ |
|
124 | 142 |
@user_blueprint.route('/following') |
125 | 143 |
@login_required |
126 | 144 |
def following(): |
... | ... |
@@ -142,7 +160,7 @@ import os, base64 |
142 | 160 |
def gen_ak(db): |
143 | 161 |
return ak |
144 | 162 |
|
145 |
-@user_blueprint.route('/<user>/env', methods=['POST']) |
|
163 |
+@user_blueprint.route('/env/<user>', methods=['POST']) |
|
146 | 164 |
def getenv(user): pass |
147 | 165 |
|
148 | 166 |
@user_blueprint.route('/change-password', methods=['POST']) |
... | ... |
@@ -48,6 +48,10 @@ html { |
48 | 48 |
|
49 | 49 |
* { |
50 | 50 |
box-sizing: border-box; |
51 |
+ -webkit-font-feature-settings: 'kern' 1, 'liga' 1; |
|
52 |
+ -moz-font-feature-settings: 'kern' 1; |
|
53 |
+ -o-font-feature-settings: 'kern' 1; |
|
54 |
+ text-rendering: geometricPrecision; |
|
51 | 55 |
} |
52 | 56 |
|
53 | 57 |
body { |
... | ... |
@@ -115,6 +119,8 @@ img { |
115 | 119 |
|
116 | 120 |
.identicon { |
117 | 121 |
border-radius: 50%; |
122 |
+ /*border: thin solid #888;*/ |
|
123 |
+ box-shadow: 0em 0em 0.1em black; |
|
118 | 124 |
vertical-align: middle; |
119 | 125 |
} |
120 | 126 |
|
... | ... |
@@ -134,9 +140,41 @@ img { |
134 | 140 |
width: 9em; |
135 | 141 |
} |
136 | 142 |
|
143 |
+.filter-form { |
|
144 |
+ padding-bottom: 0px; |
|
145 |
+} |
|
146 |
+ |
|
137 | 147 |
ul.subscription-list { |
138 | 148 |
text-align: center; |
139 |
- /*background: #eee;*/ |
|
149 |
+ position: relative; |
|
150 |
+ background: #888; |
|
151 |
+ /*text-shadow: 0em 0em 0.1em black;*/ |
|
152 |
+ /*color: white; */ |
|
153 |
+ border: thin solid black; |
|
154 |
+ padding: 0.75em 1.5em; |
|
155 |
+ max-height: 1.5em; |
|
156 |
+ overflow: hidden; |
|
157 |
+ transition: max-height 1.5s ease, padding 1.5s ease; |
|
158 |
+} |
|
159 |
+ |
|
160 |
+ul.subscription-list.opened { |
|
161 |
+ max-height: 12em; |
|
162 |
+ padding-bottom: 2.5em; |
|
163 |
+} |
|
164 |
+ |
|
165 |
+ul.subscription-list > .list-control { |
|
166 |
+ position: absolute; |
|
167 |
+ width: 100%; |
|
168 |
+ bottom: 0px; |
|
169 |
+ left: 0px; |
|
170 |
+ height: 1.5em; |
|
171 |
+ background: #888; |
|
172 |
+ color: white; |
|
173 |
+ cursor: pointer; |
|
174 |
+} |
|
175 |
+ul.subscription-list > .list-control:hover { |
|
176 |
+ background-color: #ccc; |
|
177 |
+ /*color: black;*/ |
|
140 | 178 |
} |
141 | 179 |
|
142 | 180 |
ul.subscription-list::after { |
... | ... |
@@ -260,6 +298,11 @@ footer a.facebook-link:hover { |
260 | 298 |
margin-top: 10px; |
261 | 299 |
} |
262 | 300 |
|
301 |
+#nav-main h3 { |
|
302 |
+ text-align: right; |
|
303 |
+ margin-top: 20px; |
|
304 |
+} |
|
305 |
+ |
|
263 | 306 |
/* @end */ |
264 | 307 |
|
265 | 308 |
/* @group List Module */ |
... | ... |
@@ -413,6 +456,19 @@ div.marrow { |
413 | 456 |
font-size: 80%; |
414 | 457 |
color: #aaa; |
415 | 458 |
} |
459 |
+a.add-link { |
|
460 |
+ color: green; |
|
461 |
+ display: inline-block; |
|
462 |
+ vertical-align: top; |
|
463 |
+ position: relative; |
|
464 |
+ top: 0.2em; |
|
465 |
+ /*position: absolute;*/ |
|
466 |
+ /*margin-left: -1em;*/ |
|
467 |
+} |
|
468 |
+a.add-link:hover { |
|
469 |
+ text-decoration: none; |
|
470 |
+ color: olive; |
|
471 |
+} |
|
416 | 472 |
a.delete-link { |
417 | 473 |
color: red; |
418 | 474 |
display: inline-block; |
... | ... |
@@ -2,11 +2,11 @@ |
2 | 2 |
<browserconfig> |
3 | 3 |
<msapplication> |
4 | 4 |
<tile> |
5 |
- <square70x70logo src="/images/icons/mstile-70x70.png"/> |
|
6 |
- <square150x150logo src="/images/icons/mstile-150x150.png"/> |
|
7 |
- <square310x310logo src="/images/icons/mstile-310x310.png"/> |
|
8 |
- <wide310x150logo src="/images/icons/mstile-310x150.png"/> |
|
9 |
- <TileColor>#000000</TileColor> |
|
5 |
+ <square70x70logo src="/images/icons/mstile-70x70.png?v=all2jv2L7Q"/> |
|
6 |
+ <square150x150logo src="/images/icons/mstile-150x150.png?v=all2jv2L7Q"/> |
|
7 |
+ <square310x310logo src="/images/icons/mstile-310x310.png?v=all2jv2L7Q"/> |
|
8 |
+ <wide310x150logo src="/images/icons/mstile-310x150.png?v=all2jv2L7Q"/> |
|
9 |
+ <TileColor>#da532c</TileColor> |
|
10 | 10 |
</tile> |
11 | 11 |
</msapplication> |
12 | 12 |
</browserconfig> |
... | ... |
@@ -2,40 +2,41 @@ |
2 | 2 |
"name": "Marrow", |
3 | 3 |
"icons": [ |
4 | 4 |
{ |
5 |
- "src": "\/images\/icons\/android-chrome-36x36.png", |
|
5 |
+ "src": "\/images\/icons\/android-chrome-36x36.png?v=all2jv2L7Q", |
|
6 | 6 |
"sizes": "36x36", |
7 | 7 |
"type": "image\/png", |
8 | 8 |
"density": "0.75" |
9 | 9 |
}, |
10 | 10 |
{ |
11 |
- "src": "\/images\/icons\/android-chrome-48x48.png", |
|
11 |
+ "src": "\/images\/icons\/android-chrome-48x48.png?v=all2jv2L7Q", |
|
12 | 12 |
"sizes": "48x48", |
13 | 13 |
"type": "image\/png", |
14 | 14 |
"density": "1.0" |
15 | 15 |
}, |
16 | 16 |
{ |
17 |
- "src": "\/images\/icons\/android-chrome-72x72.png", |
|
17 |
+ "src": "\/images\/icons\/android-chrome-72x72.png?v=all2jv2L7Q", |
|
18 | 18 |
"sizes": "72x72", |
19 | 19 |
"type": "image\/png", |
20 | 20 |
"density": "1.5" |
21 | 21 |
}, |
22 | 22 |
{ |
23 |
- "src": "\/images\/icons\/android-chrome-96x96.png", |
|
23 |
+ "src": "\/images\/icons\/android-chrome-96x96.png?v=all2jv2L7Q", |
|
24 | 24 |
"sizes": "96x96", |
25 | 25 |
"type": "image\/png", |
26 | 26 |
"density": "2.0" |
27 | 27 |
}, |
28 | 28 |
{ |
29 |
- "src": "\/images\/icons\/android-chrome-144x144.png", |
|
29 |
+ "src": "\/images\/icons\/android-chrome-144x144.png?v=all2jv2L7Q", |
|
30 | 30 |
"sizes": "144x144", |
31 | 31 |
"type": "image\/png", |
32 | 32 |
"density": "3.0" |
33 | 33 |
}, |
34 | 34 |
{ |
35 |
- "src": "\/images\/icons\/android-chrome-192x192.png", |
|
35 |
+ "src": "\/images\/icons\/android-chrome-192x192.png?v=all2jv2L7Q", |
|
36 | 36 |
"sizes": "192x192", |
37 | 37 |
"type": "image\/png", |
38 | 38 |
"density": "4.0" |
39 | 39 |
} |
40 |
- ] |
|
40 |
+ ], |
|
41 |
+ "display": "standalone" |
|
41 | 42 |
} |
47 | 48 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,49 @@ |
1 |
+<?xml version="1.0" standalone="no"?> |
|
2 |
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" |
|
3 |
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> |
|
4 |
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg" |
|
5 |
+ width="360.000000pt" height="337.000000pt" viewBox="0 0 360.000000 337.000000" |
|
6 |
+ preserveAspectRatio="xMidYMid meet"> |
|
7 |
+<metadata> |
|
8 |
+Created by potrace 1.11, written by Peter Selinger 2001-2013 |
|
9 |
+</metadata> |
|
10 |
+<g transform="translate(0.000000,337.000000) scale(0.100000,-0.100000)" |
|
11 |
+fill="#000000" stroke="none"> |
|
12 |
+<path d="M309 3354 c-21 -26 8 -65 75 -99 106 -54 97 57 94 -1162 -3 -1060 -3 |
|
13 |
+-1062 -24 -1090 -11 -15 -46 -40 -78 -55 -63 -31 -88 -66 -66 -93 11 -13 47 |
|
14 |
+-15 264 -13 l251 3 3 23 c5 32 -12 49 -81 83 -105 53 -97 -33 -97 1053 0 517 |
|
15 |
+2 937 6 934 3 -3 234 -487 514 -1074 l508 -1069 38 -3 39 -2 470 1017 c258 |
|
16 |
+560 491 1063 517 1118 l47 100 1 -991 0 -991 -23 -34 c-14 -21 -44 -44 -80 |
|
17 |
+-62 -64 -30 -89 -65 -67 -92 11 -13 58 -15 335 -15 351 0 356 1 336 53 -6 16 |
|
18 |
+-29 34 -64 51 -30 14 -67 40 -81 57 l-26 31 0 1066 c0 1225 -10 1106 100 1162 |
|
19 |
+67 34 92 68 70 95 -11 13 -48 15 -273 13 l-261 -3 -475 -1007 c-262 -554 -479 |
|
20 |
+-1004 -482 -1000 -4 4 -226 458 -495 1010 l-488 1002 -247 0 c-213 0 -248 -2 |
|
21 |
+-260 -16z"/> |
|
22 |
+<path d="M158 461 c-144 -46 -196 -218 -100 -333 91 -108 265 -80 334 56 l15 |
|
23 |
+28 13 -33 c40 -102 161 -148 265 -100 52 24 74 50 95 112 l17 50 7 -33 c16 |
|
24 |
+-83 110 -158 196 -158 72 1 170 71 186 135 8 32 20 32 28 1 3 -14 18 -39 32 |
|
25 |
+-56 105 -124 308 -74 342 85 l8 40 7 -41 c10 -61 45 -105 106 -136 63 -32 110 |
|
26 |
+-35 166 -9 53 24 90 66 108 120 l14 44 13 -37 c16 -48 66 -105 107 -122 42 |
|
27 |
+-18 115 -18 156 0 42 18 82 61 102 110 l17 39 19 -44 c27 -58 55 -87 107 -110 |
|
28 |
+98 -43 203 -4 253 93 10 21 19 50 20 65 1 23 3 20 10 -13 12 -50 71 -121 116 |
|
29 |
+-140 41 -18 114 -18 156 0 38 15 94 76 103 111 8 33 20 32 32 -3 24 -70 109 |
|
30 |
+-131 182 -132 40 0 127 38 152 66 62 71 62 207 0 278 -25 28 -112 66 -152 66 |
|
31 |
+-73 -1 -158 -62 -182 -132 -12 -35 -24 -36 -32 -3 -9 35 -65 96 -103 111 -42 |
|
32 |
+18 -115 18 -156 0 -45 -19 -104 -90 -116 -140 -7 -33 -9 -36 -10 -13 -1 35 |
|
33 |
+-32 95 -63 124 -31 28 -93 53 -133 53 -43 0 -114 -31 -142 -61 -12 -13 -31 |
|
34 |
+-44 -42 -68 l-19 -44 -17 39 c-20 49 -60 92 -102 110 -41 18 -114 18 -156 0 |
|
35 |
+-41 -17 -91 -74 -107 -122 l-13 -37 -14 44 c-18 54 -55 96 -108 120 -56 26 |
|
36 |
+-103 23 -166 -8 -61 -32 -96 -76 -106 -137 l-7 -41 -8 40 c-34 159 -237 209 |
|
37 |
+-342 85 -14 -17 -29 -42 -32 -56 -8 -31 -20 -31 -28 1 -16 64 -114 134 -186 |
|
38 |
+135 -86 0 -180 -75 -196 -158 l-7 -33 -17 50 c-21 62 -43 88 -95 112 -100 46 |
|
39 |
+-221 4 -262 -91 l-15 -35 -20 40 c-11 22 -28 48 -37 59 -39 44 -139 74 -193 |
|
40 |
+57z m529 -79 c91 -65 79 -218 -21 -266 -105 -50 -216 21 -216 139 0 59 25 106 |
|
41 |
+72 136 43 26 122 22 165 -9z m800 0 c91 -65 79 -218 -21 -266 -49 -23 -107 |
|
42 |
+-20 -146 7 -56 37 -73 69 -73 132 0 63 17 95 74 133 43 30 120 27 166 -6z |
|
43 |
+m796 2 c37 -27 67 -85 67 -129 0 -44 -30 -102 -67 -129 -15 -11 -48 -21 -77 |
|
44 |
+-24 -41 -3 -57 1 -85 20 -57 38 -75 72 -75 133 0 61 18 95 75 133 28 19 44 23 |
|
45 |
+85 20 29 -3 62 -13 77 -24z m787 4 c55 -38 75 -73 75 -133 0 -60 -19 -95 -76 |
|
46 |
+-133 -28 -19 -44 -23 -85 -20 -28 3 -62 13 -77 24 -32 23 -67 90 -67 127 0 57 |
|
47 |
+45 126 95 147 39 17 101 11 135 -12z"/> |
|
48 |
+</g> |
|
49 |
+</svg> |
... | ... |
@@ -4,28 +4,31 @@ |
4 | 4 |
<title>Marrow</title> |
5 | 5 |
<base href="/" /> |
6 | 6 |
<meta charset="utf-8"> |
7 |
- <meta name="msapplication-TileColor" content="#000000"> |
|
8 |
- <meta name="msapplication-TileImage" content="/images/icons/mstile-144x144.png"> |
|
9 |
- <meta name="msapplication-config" content="/images/icons/browserconfig.xml"> |
|
10 |
- <meta name="theme-color" content="#000000"> |
|
11 | 7 |
<meta name="viewport" content="width=device-width, initial-scale=1"> |
12 | 8 |
|
13 | 9 |
<!-- Favicons/App Icons --> |
14 |
- <link rel="apple-touch-icon" sizes="57x57" href="/images/icons/apple-touch-icon-57x57.png"> |
|
15 |
- <link rel="apple-touch-icon" sizes="60x60" href="/images/icons/apple-touch-icon-60x60.png"> |
|
16 |
- <link rel="apple-touch-icon" sizes="72x72" href="/images/icons/apple-touch-icon-72x72.png"> |
|
17 |
- <link rel="apple-touch-icon" sizes="76x76" href="/images/icons/apple-touch-icon-76x76.png"> |
|
18 |
- <link rel="apple-touch-icon" sizes="114x114" href="/images/icons/apple-touch-icon-114x114.png"> |
|
19 |
- <link rel="apple-touch-icon" sizes="120x120" href="/images/icons/apple-touch-icon-120x120.png"> |
|
20 |
- <link rel="apple-touch-icon" sizes="144x144" href="/images/icons/apple-touch-icon-144x144.png"> |
|
21 |
- <link rel="apple-touch-icon" sizes="152x152" href="/images/icons/apple-touch-icon-152x152.png"> |
|
22 |
- <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon-180x180.png"> |
|
23 |
- <link rel="icon" type="image/png" href="/images/icons/favicon-32x32.png" sizes="32x32"> |
|
24 |
- <link rel="icon" type="image/png" href="/images/icons/android-chrome-192x192.png" sizes="192x192"> |
|
25 |
- <link rel="icon" type="image/png" href="/images/icons/favicon-96x96.png" sizes="96x96"> |
|
26 |
- <link rel="icon" type="image/png" href="/images/icons/favicon-16x16.png" sizes="16x16"> |
|
27 |
- <link rel="manifest" href="/images/icons/manifest.json"> |
|
28 |
- <link rel="shortcut icon" href="/images/icons/favicon.ico"> |
|
10 |
+ <link rel="apple-touch-icon" sizes="57x57" href="/images/icons/apple-touch-icon-57x57.png?v=all2jv2L7Q"> |
|
11 |
+ <link rel="apple-touch-icon" sizes="60x60" href="/images/icons/apple-touch-icon-60x60.png?v=all2jv2L7Q"> |
|
12 |
+ <link rel="apple-touch-icon" sizes="72x72" href="/images/icons/apple-touch-icon-72x72.png?v=all2jv2L7Q"> |
|
13 |
+ <link rel="apple-touch-icon" sizes="76x76" href="/images/icons/apple-touch-icon-76x76.png?v=all2jv2L7Q"> |
|
14 |
+ <link rel="apple-touch-icon" sizes="114x114" href="/images/icons/apple-touch-icon-114x114.png?v=all2jv2L7Q"> |
|
15 |
+ <link rel="apple-touch-icon" sizes="120x120" href="/images/icons/apple-touch-icon-120x120.png?v=all2jv2L7Q"> |
|
16 |
+ <link rel="apple-touch-icon" sizes="144x144" href="/images/icons/apple-touch-icon-144x144.png?v=all2jv2L7Q"> |
|
17 |
+ <link rel="apple-touch-icon" sizes="152x152" href="/images/icons/apple-touch-icon-152x152.png?v=all2jv2L7Q"> |
|
18 |
+ <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon-180x180.png?v=all2jv2L7Q"> |
|
19 |
+ <link rel="icon" type="image/png" href="/images/icons/favicon-32x32.png?v=all2jv2L7Q" sizes="32x32"> |
|
20 |
+ <link rel="icon" type="image/png" href="/images/icons/favicon-194x194.png?v=all2jv2L7Q" sizes="194x194"> |
|
21 |
+ <link rel="icon" type="image/png" href="/images/icons/favicon-96x96.png?v=all2jv2L7Q" sizes="96x96"> |
|
22 |
+ <link rel="icon" type="image/png" href="/images/icons/android-chrome-192x192.png?v=all2jv2L7Q" sizes="192x192"> |
|
23 |
+ <link rel="icon" type="image/png" href="/images/icons/favicon-16x16.png?v=all2jv2L7Q" sizes="16x16"> |
|
24 |
+ <link rel="manifest" href="/images/icons/manifest.json?v=all2jv2L7Q"> |
|
25 |
+ <link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#8eb7cf"> |
|
26 |
+ <link rel="shortcut icon" href="/images/icons/favicon.ico?v=all2jv2L7Q"> |
|
27 |
+ <meta name="msapplication-TileColor" content="#da532c"> |
|
28 |
+ <meta name="msapplication-TileImage" content="/images/icons/mstile-144x144.png?v=all2jv2L7Q"> |
|
29 |
+ <meta name="msapplication-config" content="/images/icons/browserconfig.xml?v=all2jv2L7Q"> |
|
30 |
+ <meta name="theme-color" content="#8eb7cf"> |
|
31 |
+ |
|
29 | 32 |
<!-- JS --> |
30 | 33 |
<script src="//crypto-js.googlecode.com/svn/tags/3.0.2/build/rollups/md5.js"></script> |
31 | 34 |
<script src="/lib/angular.min.js"></script> |
... | ... |
@@ -46,64 +49,70 @@ |
46 | 49 |
|
47 | 50 |
</head> |
48 | 51 |
<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'); |
|
52 |
+ <!-- |
|
53 |
+ - <script> |
|
54 |
+- window.fbAsyncInit = function() {FB.init({ |
|
55 |
+ - appId : '1420153671647757', |
|
56 |
+ - xfbml : true, |
|
57 |
+ - version : 'v2.3' |
|
58 |
+ - }); |
|
59 |
+- }; |
|
60 |
+- |
|
61 |
+- (function(d, s, id){ |
|
62 |
+ - var js, fjs = d.getElementsByTagName(s)[0]; |
|
63 |
+ - if (d.getElementById(id)) {return;} |
|
64 |
+ - js = d.createElement(s); js.id = id; |
|
65 |
+ - js.src = "//connect.facebook.net/en_US/sdk.js"; |
|
66 |
+ - fjs.parentNode.insertBefore(js, fjs); |
|
67 |
+ - }(document, 'script', 'facebook-jssdk')); |
|
68 |
+- </script> |
|
69 |
+ - |
|
70 |
+ --> |
|
71 |
+ <div id="page-container"> |
|
72 |
+ <main ng-view></main> |
|
73 |
+ <header> |
|
74 |
+ <h1 class="site-logo">Marrow</h1> |
|
75 |
+ </header> |
|
76 |
+ <nav id="nav-main" ng-controller="SidebarCtrl"> |
|
77 |
+ <ul> |
|
78 |
+ <li><a href="/">Home</a></li> |
|
79 |
+ <li><a href ng-click="subscriptions()">My Lists</a></li> |
|
80 |
+ <li><a href ng-click="random()">Random</a></li> |
|
81 |
+ <li class="logout-link"><a href ng-click="logout()">Log Out</a></li> |
|
82 |
+ </ul> |
|
83 |
+ <h3>Active Users</h3> |
|
84 |
+ <ul> |
|
85 |
+ <li ng-repeat="user in activeUsers.data"> |
|
86 |
+ <user-badge poster="{{user.name}}" no-image></user-badge> |
|
87 |
+ </li> |
|
88 |
+ </ul> |
|
89 |
+ <!-- |
|
90 |
+ -<div class="appstore-links"> |
|
91 |
+ - <a href="https://play.google.com/store/apps/details?id=com.joinmarrow.marrow"> |
|
92 |
+ - <img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" /> |
|
93 |
+ - </a> |
|
94 |
+ -</div> |
|
95 |
+ --> |
|
96 |
+ </nav> |
|
97 |
+ <footer> |
|
98 |
+ <span class="beta-message"> |
|
99 |
+ This service is currently in beta. Like us on |
|
100 |
+ <a class="facebook-link" href="https://www.facebook.com/join.marrow">Facebook.</a> |
|
101 |
+ Try the |
|
102 |
+ <a style="color: blue" |
|
103 |
+ href="https://chrome.google.com/webstore/detail/add-to-marrow/pcgflajngpeopkemlijnnggfchoglpad"> |
|
104 |
+ Chrome Extension |
|
105 |
+ </a> |
|
106 |
+ to get the newest links delivered straight to your browser. |
|
107 |
+ <!--or report problems on <a class="facebook-link" href="https://bugs.joinmarrow.com">bugs.joinmarrow.com</a>.--> |
|
108 |
+ </span> |
|
109 |
+ </footer> |
|
110 |
+ </div> |
|
111 |
+ <script> |
|
112 |
+(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ |
|
113 |
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), |
|
114 |
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) |
|
115 |
+})(window,document,'script','//www.google-analytics.com/analytics.js','ga'); |
|
107 | 116 |
|
108 | 117 |
ga('create', 'UA-61547817-1', 'auto'); |
109 | 118 |
//ga('send', 'pageview'); |
... | ... |
@@ -7,6 +7,9 @@ |
7 | 7 |
data-title="{{marrow.title}}" |
8 | 8 |
data-poster="{{marrow.poster}}" |
9 | 9 |
data-votes="{{marrow.votes}}"> |
10 |
+ <a href class="add-link" ng-click="reshare({item:marrow})" title="Reshare Link" |
|
11 |
+ analytics-on="click" analytics-category="link" analytics-label="reshare" |
|
12 |
+ analytics-event="{{marrow.url}}">{{!marrow.shared? '(+)': '✓'}}</a> |
|
10 | 13 |
<span class="vote-disp">{{marrow.votes}}</span> |
11 | 14 |
<span ng-if="marrow.title"> |
12 | 15 |
<a href="{{marrow.url}}" class="list-item" >{{marrow.title}}</a> |
... | ... |
@@ -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 |
- <span class="poster-handle de-emphasize">{{poster}}{{rep === undefined || rep === null? '' : ' ('+rep+')'}}</span> |
|
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: '@', rep: '@', |
|
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'; |
... | ... |
@@ -24,7 +24,7 @@ marrowApp.config(['$routeProvider', |
24 | 24 |
responseError: function(rejection) { |
25 | 25 |
if (rejection.status === 401) { |
26 | 26 |
console.log("Response Error 401",rejection); |
27 |
- $window.location.href = '/login.html#' + encodeURIComponent($location.path()); |
|
27 |
+ $window.location.href = '/login.html'; |
|
28 | 28 |
} |
29 | 29 |
return $q.reject(rejection); |
30 | 30 |
} |
... | ... |
@@ -38,25 +38,23 @@ marrowApp.config(['$routeProvider', |
38 | 38 |
marrowApp.config(['$locationProvider', function($locationProvider) { $locationProvider.html5Mode(true); }]); |
39 | 39 |
|
40 | 40 |
//marrowApp.controller('LoginCtrl', function ($scope,$http,$route,$location) { |
41 |
-// $scope.tab = 'login'; |
|
42 |
- |
|
43 | 41 |
// $scope.message = ''; |
44 |
- |
|
42 |
+// |
|
45 | 43 |
// var check_login = function () { |
46 |
-// var injector = angular.injector(['ng']); |
|
47 |
-// var $http = injector.get('$http'); |
|
44 |
+// injector = angular.injector(['ng']); |
|
45 |
+// $http = injector.get('$http'); |
|
48 | 46 |
// return $http.get("/api/user/check").success(function(is_loggedon) { |
49 | 47 |
// if (is_loggedon.result === true) { |
50 | 48 |
// angular.element(document.body).addClass('is-logged-on'); |
51 | 49 |
// } |
52 | 50 |
// }); |
53 | 51 |
// }; |
54 |
- |
|
52 |
+// |
|
55 | 53 |
// check_login().success( |
56 | 54 |
// function(is_loggedon) { |
57 | 55 |
// if (is_loggedon.result) { $location.url('/');} |
58 | 56 |
// }); |
59 |
- |
|
57 |
+// |
|
60 | 58 |
// $scope.newuser = function () { |
61 | 59 |
// var username = $scope.username; |
62 | 60 |
// var password = $scope.password; |
... | ... |
@@ -67,11 +65,11 @@ marrowApp.config(['$locationProvider', function($locationProvider) { $locationPr |
67 | 65 |
// else {$scope.message = added_user.message;} |
68 | 66 |
// }); |
69 | 67 |
// }; |
70 |
- |
|
68 |
+// |
|
71 | 69 |
// $scope.login = function () { |
72 | 70 |
// var username = $scope.username; |
73 | 71 |
// var password = $scope.password; |
74 |
- |
|
72 |
+// |
|
75 | 73 |
// $http.post("/api/user/login", {"username":username, "password":password}) |
76 | 74 |
// .success( |
77 | 75 |
// function (login_succeeded) { |
... | ... |
@@ -82,10 +80,19 @@ marrowApp.config(['$locationProvider', function($locationProvider) { $locationPr |
82 | 80 |
// }; |
83 | 81 |
//}); |
84 | 82 |
|
85 |
-marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService) { |
|
83 |
+marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService, $window) { |
|
86 | 84 |
$scope.url = ""; |
87 | 85 |
$scope.title = ""; |
88 | 86 |
|
87 |
+ $scope.reshare = function (marrow) { |
|
88 |
+ if (!marrow.shared ) { |
|
89 |
+ $http.post('/api/bones/link/'+marrow.id).success(function(shared) { |
|
90 |
+ marrow.shared = true; |
|
91 |
+ if (shared === true) { $scope.update(); } |
|
92 |
+ }); |
|
93 |
+ }; |
|
94 |
+ }; |
|
95 |
+ |
|
89 | 96 |
$scope.toggleSubscribe = function (txt) { |
90 | 97 |
var postObj = {"from":$scope.bone.sectionTitle, "to":$scope.bone.sectionTitle}; |
91 | 98 |
var promise = null; |
... | ... |
@@ -266,7 +273,16 @@ marrowApp.controller('UserSettingCtrl', function ($scope,$http,$location) { |
266 | 273 |
}; |
267 | 274 |
}); |
268 | 275 |
|
269 |
-marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window) { |
|
276 |
+marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window, UserService) { |
|
277 |
+ //eventSource = new EventSource("/api/user/active"); |
|
278 |
+ $scope.activeUsers = UserService.active(); |
|
279 |
+ //$scope.activeUsers = Object.create(null); |
|
280 |
+ //$scope.activeUsers.users = [] |
|
281 |
+ //eventSource.addEventListener("active", function(event) { |
|
282 |
+ // var users = $scope.activeUsers.users; |
|
283 |
+ // Array.prototype.splice.apply(users, [0, users.length].concat(JSON.parse(event.data).data)); |
|
284 |
+ //}); |
|
285 |
+ |
|
270 | 286 |
$scope.subscriptions = function() { |
271 | 287 |
if ($location.url() !== '/subscriptions') { $location.url('/subscriptions'); } |
272 | 288 |
else { $route.reload(); } |
273 | 289 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,322 @@ |
1 |
+window.URL = window.URL || window.webkitURL; |
|
2 |
+var marrowApp = angular.module('marrowApp', ['ngRoute', 'marrowApp.services', 'marrowApp.directives', 'marrowApp.utils', |
|
3 |
+ 'marrowApp.directives.boneList', 'marrowApp.directives.userBadge', |
|
4 |
+ 'angulartics', 'angulartics.google.analytics', 'angulartics.piwik']); |
|
5 |
+ |
|
6 |
+marrowApp.config(['$routeProvider', |
|
7 |
+ function($routeProvider) { |
|
8 |
+ $routeProvider. |
|
9 |
+ when('/random', {templateUrl: 'partials/random.html', controller: 'RandomMarrowCtrl'}). |
|
10 |
+ when('/settings', {templateUrl: 'partials/user-settings.html', controller: 'UserSettingCtrl'}). |
|
11 |
+ when('/subscriptions', {templateUrl: 'partials/subscription.html', controller: 'SubscriptionCtrl'}). |
|
12 |
+ when('/', {templateUrl: 'partials/default.html', controller: 'MarrowCtrl'}). |
|
13 |
+ when('/user/:user', {template: '<div ng-include="templateUrl">Loading...</div>', controller: 'UserCtrl'}); |
|
14 |
+ } |
|
15 |
+]) |
|
16 |
+.factory('authHttpResponseInterceptor',['$q','$location', '$window',function($q,$location,$window){ |
|
17 |
+ return { |
|
18 |
+ response: function(response){ |
|
19 |
+ if (response.status === 401) { |
|
20 |
+ console.log("Response 401"); |
|
21 |
+ } |
|
22 |
+ return response || $q.when(response); |
|
23 |
+ }, |
|
24 |
+ responseError: function(rejection) { |
|
25 |
+ if (rejection.status === 401) { |
|
26 |
+ console.log("Response Error 401",rejection); |
|
27 |
+ $window.location.href = '/login.html#' + encodeURIComponent($location.path()); |
|
28 |
+ } |
|
29 |
+ return $q.reject(rejection); |
|
30 |
+ } |
|
31 |
+ }; |
|
32 |
+}]) |
|
33 |
+.config(['$httpProvider',function($httpProvider) { |
|
34 |
+ //Http Intercpetor to check auth failures for xhr requests |
|
35 |
+ $httpProvider.interceptors.push('authHttpResponseInterceptor'); |
|
36 |
+}]); |
|
37 |
+ |
|
38 |
+marrowApp.config(['$locationProvider', function($locationProvider) { $locationProvider.html5Mode(true); }]); |
|
39 |
+ |
|
40 |
+//marrowApp.controller('LoginCtrl', function ($scope,$http,$route,$location) { |
|
41 |
+<<<<<<< HEAD |
|
42 |
+// $scope.tab = 'login'; |
|
43 |
+ |
|
44 |
+// $scope.message = ''; |
|
45 |
+ |
|
46 |
+// var check_login = function () { |
|
47 |
+// var injector = angular.injector(['ng']); |
|
48 |
+// var $http = injector.get('$http'); |
|
49 |
+======= |
|
50 |
+// $scope.message = ''; |
|
51 |
+// |
|
52 |
+// var check_login = function () { |
|
53 |
+// injector = angular.injector(['ng']); |
|
54 |
+// $http = injector.get('$http'); |
|
55 |
+>>>>>>> Split login into its own html file |
|
56 |
+// return $http.get("/api/user/check").success(function(is_loggedon) { |
|
57 |
+// if (is_loggedon.result === true) { |
|
58 |
+// angular.element(document.body).addClass('is-logged-on'); |
|
59 |
+// } |
|
60 |
+// }); |
|
61 |
+// }; |
|
62 |
+<<<<<<< HEAD |
|
63 |
+ |
|
64 |
+======= |
|
65 |
+// |
|
66 |
+>>>>>>> Split login into its own html file |
|
67 |
+// check_login().success( |
|
68 |
+// function(is_loggedon) { |
|
69 |
+// if (is_loggedon.result) { $location.url('/');} |
|
70 |
+// }); |
|
71 |
+<<<<<<< HEAD |
|
72 |
+ |
|
73 |
+======= |
|
74 |
+// |
|
75 |
+>>>>>>> Split login into its own html file |
|
76 |
+// $scope.newuser = function () { |
|
77 |
+// var username = $scope.username; |
|
78 |
+// var password = $scope.password; |
|
79 |
+// var postObj = {"username":username, "password": password}; |
|
80 |
+// $http.post("/api/user/add", postObj) |
|
81 |
+// .success(function(added_user) { |
|
82 |
+// if (added_user.status === true) {$location.url('/');} |
|
83 |
+// else {$scope.message = added_user.message;} |
|
84 |
+// }); |
|
85 |
+// }; |
|
86 |
+<<<<<<< HEAD |
|
87 |
+ |
|
88 |
+// $scope.login = function () { |
|
89 |
+// var username = $scope.username; |
|
90 |
+// var password = $scope.password; |
|
91 |
+ |
|
92 |
+======= |
|
93 |
+// |
|
94 |
+// $scope.login = function () { |
|
95 |
+// var username = $scope.username; |
|
96 |
+// var password = $scope.password; |
|
97 |
+// |
|
98 |
+>>>>>>> Split login into its own html file |
|
99 |
+// $http.post("/api/user/login", {"username":username, "password":password}) |
|
100 |
+// .success( |
|
101 |
+// function (login_succeeded) { |
|
102 |
+// var el = angular.element(document.querySelector('#login_form')); |
|
103 |
+// if (login_succeeded.status === true) {$location.url('/');} |
|
104 |
+// else {$scope.message = login_succeeded.message;} |
|
105 |
+// }); |
|
106 |
+// }; |
|
107 |
+//}); |
|
108 |
+ |
|
109 |
+<<<<<<< HEAD |
|
110 |
+marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService) { |
|
111 |
+======= |
|
112 |
+marrowApp.controller('RootCtrl', function ($scope,$http,$location,$route, SubscribedTo, BoneService, UserService, $window) { |
|
113 |
+>>>>>>> Split login into its own html file |
|
114 |
+ $scope.url = ""; |
|
115 |
+ $scope.title = ""; |
|
116 |
+ |
|
117 |
+ $scope.toggleSubscribe = function (txt) { |
|
118 |
+ var postObj = {"from":$scope.bone.sectionTitle, "to":$scope.bone.sectionTitle}; |
|
119 |
+ var promise = null; |
|
120 |
+ |
|
121 |
+ if ($scope.iFollow.follows) { |
|
122 |
+ promise = $http.post('/api/bones/unsubscribe', postObj); |
|
123 |
+ } else { |
|
124 |
+ promise = $http.post('/api/bones/subscribe', postObj); |
|
125 |
+ } |
|
126 |
+ |
|
127 |
+ return promise.success(function(result) { |
|
128 |
+ result = JSON.parse(result); |
|
129 |
+ if (result) { |
|
130 |
+ $scope.iFollow.follows = ! $scope.iFollow.follows; |
|
131 |
+ } |
|
132 |
+ }); |
|
133 |
+ }; |
|
134 |
+ |
|
135 |
+ $scope.bone = {sectionTitle: "", marrow: []}; |
|
136 |
+ $scope.friends = {data: []}; |
|
137 |
+ |
|
138 |
+ $scope.update = function() { |
|
139 |
+ var config = {params: $scope.args? $scope.args: {}}; |
|
140 |
+ return $scope.getendpoint($scope.serviceParams, function(data) { |
|
141 |
+ $scope.bone.sectionTitle = data.sectionTitle; |
|
142 |
+ $scope.bone.marrow = data.marrow; |
|
143 |
+ $scope.iFollow = UserService.follows({user:$scope.bone.sectionTitle}); |
|
144 |
+ }).$promise.then($scope._update); |
|
145 |
+ }; |
|
146 |
+ |
|
147 |
+ UserService.check(function(is_loggedon) { |
|
148 |
+ if (is_loggedon.result === true) { |
|
149 |
+ angular.element(document.body).addClass('is-logged-on'); |
|
150 |
+ } else { |
|
151 |
+<<<<<<< HEAD |
|
152 |
+ //$window.location.href = '/login.html'; |
|
153 |
+======= |
|
154 |
+ $window.location.href = '/login.html'; |
|
155 |
+>>>>>>> Split login into its own html file |
|
156 |
+ } |
|
157 |
+ |
|
158 |
+ $scope.update(); |
|
159 |
+ }); |
|
160 |
+ |
|
161 |
+}); |
|
162 |
+ |
|
163 |
+marrowApp.controller('RandomMarrowCtrl', function ($controller, $scope,$http,$location,$route, SubscribedTo, BoneService, UserService) { |
|
164 |
+ $scope._update = function() {}; |
|
165 |
+ $scope.getendpoint = BoneService.random; |
|
166 |
+ angular.extend(this, $controller('RootCtrl', {$scope: $scope})); |
|
167 |
+}); |
|
168 |
+ |
|
169 |
+marrowApp.controller('SubscriptionCtrl', function ($controller,$scope,$http,$location,$route, SubscribedTo, BoneService, UserService) { |
|
170 |
+ $scope.uncheckOthers = function (list) { |
|
171 |
+ for (var n in list) { |
|
172 |
+ if (n !== 'all' && list[n] === true) { list[n] = false; } |
|
173 |
+ } |
|
174 |
+ }; |
|
175 |
+ |
|
176 |
+ $scope.friend = Object.create(null); |
|
177 |
+ $scope.friend.all = true; |
|
178 |
+ |
|
179 |
+ $scope.upVote = function(boneItem) { |
|
180 |
+ var apiCall = boneItem.myVote === 0? BoneService.vote_up: BoneService.vote_zero; |
|
181 |
+ apiCall({url: boneItem.url}).$promise.then(function(r) { |
|
182 |
+ if (r.success) { |
|
183 |
+ boneItem.votes = r.votes; |
|
184 |
+ boneItem.myVote = r.myVote; |
|
185 |
+ } |
|
186 |
+ }).then($scope._update); |
|
187 |
+ }; |
|
188 |
+ |
|
189 |
+ $scope.backAPage = function() { |
|
190 |
+ var bone = $scope.bone.marrow; |
|
191 |
+ var lastitem = bone[bone.length-1].posted; |
|
192 |
+ BoneService.subscriptions({before: lastitem}).$promise.then(function(r) { |
|
193 |
+ while (r.marrow.length) { |
|
194 |
+ $scope.bone.marrow.push(r.marrow.shift()); |
|
195 |
+ } |
|
196 |
+ }).then($scope._update); |
|
197 |
+ }; |
|
198 |
+ |
|
199 |
+ $scope.emptyOrEquals = function(actual, expected) { |
|
200 |
+ var result = false; |
|
201 |
+ if (!expected) { result = true; } |
|
202 |
+ else if (expected.all) { result = true; } |
|
203 |
+ else {result = expected[actual]; } |
|
204 |
+ return result; |
|
205 |
+ }; |
|
206 |
+ |
|
207 |
+ $scope.getBucket = function(date, buckets, classes) { |
|
208 |
+ date = Date.parse(date); |
|
209 |
+ var result = buckets.filter(function(x) { |
|
210 |
+ return x >= date; |
|
211 |
+ }); |
|
212 |
+ return classes[result[result.length-1]]; |
|
213 |
+ }; |
|
214 |
+ |
|
215 |
+ $scope.following_set = Object.create(null); |
|
216 |
+ $scope._update = function() { |
|
217 |
+ var marrow = $scope.bone.marrow; |
|
218 |
+ var first = marrow[0].posted, last = marrow[marrow.length-1].posted; |
|
219 |
+ first = Date.parse(first); |
|
220 |
+ last = Date.parse(last); |
|
221 |
+ var range = first - last; |
|
222 |
+ console.log(range); |
|
223 |
+ var bucketWidth = Math.ceil(range/20); |
|
224 |
+ var buckets = []; |
|
225 |
+ var bucketClasses= {}; |
|
226 |
+ for (var x = first; x > last; x -= bucketWidth) { |
|
227 |
+ buckets.push(x); |
|
228 |
+ } |
|
229 |
+ for (var x = 0; x < 20; x++) { // jshint ignore:line |
|
230 |
+ var bucket = x; |
|
231 |
+ bucketClasses[buckets[bucket]] = 'bucket-'+bucket; |
|
232 |
+ } |
|
233 |
+ $scope.bone.marrow.map(function(o) { |
|
234 |
+ o.colorClass = $scope.getBucket(o.posted, buckets, bucketClasses); |
|
235 |
+ if (!(o.poster in $scope.following_set)) { |
|
236 |
+ $scope.following_set[o.poster] = true; |
|
237 |
+ $scope.friends.data.push(o.poster); |
|
238 |
+ } |
|
239 |
+ }); |
|
240 |
+ $scope.friends.reps = UserService.reputations($scope.friends.data); |
|
241 |
+ }; |
|
242 |
+ |
|
243 |
+ $scope.getendpoint = BoneService.subscriptions; |
|
244 |
+ angular.extend(this, $controller('RootCtrl', {$scope: $scope})); |
|
245 |
+}); |
|
246 |
+ |
|
247 |
+marrowApp.controller('MarrowCtrl', function ($controller,$scope,$http,$location,$route, SubscribedTo, BoneService, UserService) { |
|
248 |
+ $scope.postobj = {url: "", title: ""}; |
|
249 |
+ |
|
250 |
+ $scope.delete = function (linkid) { |
|
251 |
+ $http.delete('/api/bones/link/'+linkid).success(function (deleted) { |
|
252 |
+ deleted = JSON.parse(deleted); |
|
253 |
+ if (deleted === true) { $scope.update(); } |
|
254 |
+ }); |
|
255 |
+ }; |
|
256 |
+ |
|
257 |
+ $scope.addLink = function() { |
|
258 |
+ $http.post('/api/bones/add', $scope.postobj).success(function(data) { |
|
259 |
+ if (data.success) { |
|
260 |
+ $scope.postobj.url = ""; |
|
261 |
+ $scope.update(); |
|
262 |
+ } |
|
263 |
+ }); |
|
264 |
+ }; |
|
265 |
+ |
|
266 |
+ if ($scope.getendpoint === undefined) { |
|
267 |
+ $scope.getendpoint = BoneService.get; |
|
268 |
+ } |
|
269 |
+ angular.extend(this, $controller('RootCtrl', {$scope: $scope})); |
|
270 |
+}); |
|
271 |
+ |
|
272 |
+marrowApp.controller('UserCtrl', function ($controller, $scope,$http,$routeParams, UserService, BoneService) { |
|
273 |
+ var user = $routeParams.user; |
|
274 |
+ $scope.getendpoint = BoneService.user; |
|
275 |
+ $scope.serviceParams = {user: user}; |
|
276 |
+ |
|
277 |
+ angular.extend(this, $controller('MarrowCtrl', {$scope: $scope})); |
|
278 |
+ $scope._update = function() { |
|
279 |
+ $scope.iFollow.$promise.then(function(result) { |
|
280 |
+ $scope.templateUrl = result.me === user? "/partials/default.html": "/partials/random.html"; |
|
281 |
+ }); |
|
282 |
+ }; |
|
283 |
+ |
|
284 |
+}); |
|
285 |
+ |
|
286 |
+marrowApp.controller('UserSettingCtrl', function ($scope,$http,$location) { |
|
287 |
+ $scope.oldPassword = ''; |
|
288 |
+ $scope.newPassword = ''; |
|
289 |
+ $scope.changePassword = function() { |
|
290 |
+ var postObj = {"old_password": $scope.oldPassword, "new_password": $scope.newPassword}; |
|
291 |
+ $http.post('/api/user/change-password', postObj).success(function(result) { |
|
292 |
+ if (result.status === true) { |
|
293 |
+ $location.url('/'); |
|
294 |
+ } else { |
|
295 |
+ $scope.message = result.message; |
|
296 |
+ } |
|
297 |
+ }); |
|
298 |
+ }; |
|
299 |
+}); |
|
300 |
+ |
|
301 |
+marrowApp.controller('SidebarCtrl', function ($scope,$http,$location,$route, $window) { |
|
302 |
+ $scope.subscriptions = function() { |
|
303 |
+ if ($location.url() !== '/subscriptions') { $location.url('/subscriptions'); } |
|
304 |
+ else { $route.reload(); } |
|
305 |
+ }; |
|
306 |
+ |
|
307 |
+ $scope.random = function() { |
|
308 |
+ if ($location.url() !== '/random') { $location.url('/random'); } |
|
309 |
+ else { $route.reload(); } |
|
310 |
+ }; |
|
311 |
+ |
|
312 |
+ $scope.logout = function() { |
|
313 |
+ $http.get('/api/user/logout').success(function() { |
|
314 |
+<<<<<<< HEAD |
|
315 |
+ $window.location.href = '/'; |
|
316 |
+======= |
|
317 |
+ $window.location.href = '/login.html'; |
|
318 |
+>>>>>>> Split login into its own html file |
|
319 |
+ }); |
|
320 |
+ }; |
|
321 |
+}); |
|
322 |
+ |
... | ... |
@@ -1,5 +1,5 @@ |
1 | 1 |
window.URL = window.URL || window.webkitURL; |
2 |
-var loginModule = angular.module('marrowLogin', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics']); |
|
2 |
+var loginModule = angular.module('marrowLogin', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics', 'angulartics.piwik']); |
|
3 | 3 |
|
4 | 4 |
loginModule.controller('LoginCtrl', function ($scope,$http,$route,$window) { |
5 | 5 |
$scope.message = ''; |
... | ... |
@@ -14,10 +14,10 @@ loginModule.controller('LoginCtrl', function ($scope,$http,$route,$window) { |
14 | 14 |
}); |
15 | 15 |
}; |
16 | 16 |
|
17 |
- check_login().success( |
|
18 |
- function(is_loggedon) { |
|
19 |
- if (is_loggedon.result) { $window.location.href = '/';} |
|
20 |
- }); |
|
17 |
+ //check_login().success( |
|
18 |
+ // function(is_loggedon) { |
|
19 |
+ // if (is_loggedon.result) { $window.location.href = '/';} |
|
20 |
+ //}); |
|
21 | 21 |
|
22 | 22 |
$scope.newuser = function () { |
23 | 23 |
var username = $scope.username; |
24 | 24 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,49 @@ |
1 |
+<<<<<<< HEAD |
|
2 |
+window.URL = window.URL || window.webkitURL; |
|
3 |
+var loginModule = angular.module('marrowLogin', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics']); |
|
4 |
+======= |
|
5 |
+var loginModule = angular.module('marrowApp.login', ['ngResource','ngRoute','angulartics', 'angulartics.google.analytics']); |
|
6 |
+>>>>>>> Split login into its own html file |
|
7 |
+ |
|
8 |
+loginModule.controller('LoginCtrl', function ($scope,$http,$route,$window) { |
|
9 |
+ $scope.message = ''; |
|
10 |
+ |
|
11 |
+ var check_login = function () { |
|
12 |
+ injector = angular.injector(['ng']); |
|
13 |
+ $http = injector.get('$http'); |
|
14 |
+ return $http.get("/api/user/check").success(function(is_loggedon) { |
|
15 |
+ if (is_loggedon.result === true) { |
|
16 |
+ angular.element(document.body).addClass('is-logged-on'); |
|
17 |
+ } |
|
18 |
+ }); |
|
19 |
+ }; |
|
20 |
+ |
|
21 |
+ check_login().success( |
|
22 |
+ function(is_loggedon) { |
|
23 |
+ if (is_loggedon.result) { $window.location.href = '/';} |
|
24 |
+ }); |
|
25 |
+ |
|
26 |
+ $scope.newuser = function () { |
|
27 |
+ var username = $scope.username; |
|
28 |
+ var password = $scope.password; |
|
29 |
+ var postObj = {"username":username, "password": password}; |
|
30 |
+ $http.post("/api/user/add", postObj) |
|
31 |
+ .success(function(added_user) { |
|
32 |
+ if (added_user.status === true) {$window.location.href = '/';} |
|
33 |
+ else {$scope.message = added_user.message;} |
|
34 |
+ }); |
|
35 |
+ }; |
|
36 |
+ |
|
37 |
+ $scope.login = function () { |
|
38 |
+ var username = $scope.username; |
|
39 |
+ var password = $scope.password; |
|
40 |
+ |
|
41 |
+ $http.post("/api/user/login", {"username":username, "password":password}) |
|
42 |
+ .success( |
|
43 |
+ function (login_succeeded) { |
|
44 |
+ var el = angular.element(document.querySelector('#login_form')); |
|
45 |
+ if (login_succeeded.status === true) {$window.location.href = '/';} |
|
46 |
+ else {$scope.message = login_succeeded.message;} |
|
47 |
+ }); |
|
48 |
+ }; |
|
49 |
+}); |
... | ... |
@@ -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 |
); |
... | ... |
@@ -32,6 +32,7 @@ |
32 | 32 |
<script src="/lib/angular-resource.min.js"></script> |
33 | 33 |
<script src="/lib/angulartics.min.js"></script> |
34 | 34 |
<script src="/lib/angulartics-ga.min.js"></script> |
35 |
+ <script src="/lib/angulartics-piwik.js"></script> |
|
35 | 36 |
<script src="/js/new/login.js"></script> |
36 | 37 |
<!-- CSS --> |
37 | 38 |
<link rel="stylesheet" href="/lib/formalize.css" media="screen" /> |
... | ... |
@@ -48,8 +49,8 @@ |
48 | 49 |
<div class="message">{{message}}</div> |
49 | 50 |
<input type="text" ng-model="username" placeholder="Username" /> |
50 | 51 |
<input type="password" ng-model="password" placeholder="Password" /> |
51 |
- <button ng-click="login()">Log In</button> |
|
52 |
- <button ng-click="newuser()">Add User</button> |
|
52 |
+ <button ng-click="login()" analytics-on="click" analytics-category="user" analytics-event="login">Log In</button> |
|
53 |
+ <button ng-click="newuser()" analytics-on="click" analytics-category="user" analytics-event="new" analytics-label="{{username}}">Add User</button> |
|
53 | 54 |
</form> |
54 | 55 |
</div> |
55 | 56 |
</main> |
56 | 57 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,75 @@ |
1 |
+<html lang="en"> |
|
2 |
+<head> |
|
3 |
+ <title>Marrow</title> |
|
4 |
+ <base href="/login.html" /> |
|
5 |
+ <meta charset="utf-8"> |
|
6 |
+ <meta name="msapplication-TileColor" content="#000000"> |
|
7 |
+ <meta name="msapplication-TileImage" content="/images/icons/mstile-144x144.png"> |
|
8 |
+ <meta name="msapplication-config" content="/images/icons/browserconfig.xml"> |
|
9 |
+ <meta name="theme-color" content="#000000"> |
|
10 |
+ <meta name="viewport" content="width=device-width, initial-scale=1"> |
|
11 |
+ |
|
12 |
+ <!-- Favicons/App Icons --> |
|
13 |
+ <link rel="apple-touch-icon" sizes="57x57" href="/images/icons/apple-touch-icon-57x57.png"> |
|
14 |
+ <link rel="apple-touch-icon" sizes="60x60" href="/images/icons/apple-touch-icon-60x60.png"> |
|
15 |
+ <link rel="apple-touch-icon" sizes="72x72" href="/images/icons/apple-touch-icon-72x72.png"> |
|
16 |
+ <link rel="apple-touch-icon" sizes="76x76" href="/images/icons/apple-touch-icon-76x76.png"> |
|
17 |
+ <link rel="apple-touch-icon" sizes="114x114" href="/images/icons/apple-touch-icon-114x114.png"> |
|
18 |
+ <link rel="apple-touch-icon" sizes="120x120" href="/images/icons/apple-touch-icon-120x120.png"> |
|
19 |
+ <link rel="apple-touch-icon" sizes="144x144" href="/images/icons/apple-touch-icon-144x144.png"> |
|
20 |
+ <link rel="apple-touch-icon" sizes="152x152" href="/images/icons/apple-touch-icon-152x152.png"> |
|
21 |
+ <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon-180x180.png"> |
|
22 |
+ <link rel="icon" type="image/png" href="/images/icons/favicon-32x32.png" sizes="32x32"> |
|
23 |
+ <link rel="icon" type="image/png" href="/images/icons/android-chrome-192x192.png" sizes="192x192"> |
|
24 |
+ <link rel="icon" type="image/png" href="/images/icons/favicon-96x96.png" sizes="96x96"> |
|
25 |
+ <link rel="icon" type="image/png" href="/images/icons/favicon-16x16.png" sizes="16x16"> |
|
26 |
+ <link rel="manifest" href="/images/icons/manifest.json"> |
|
27 |
+ <link rel="shortcut icon" href="/images/icons/favicon.ico"> |
|
28 |
+ <!-- JS --> |
|
29 |
+ <script src="//crypto-js.googlecode.com/svn/tags/3.0.2/build/rollups/md5.js"></script> |
|
30 |
+ <script src="/lib/angular.min.js"></script> |
|
31 |
+ <script src="/lib/angular-route.min.js"></script> |
|
32 |
+ <script src="/lib/angular-resource.min.js"></script> |
|
33 |
+ <script src="/lib/angulartics.min.js"></script> |
|
34 |
+ <script src="/lib/angulartics-ga.min.js"></script> |
|
35 |
+ <script src="/js/new/login.js"></script> |
|
36 |
+ <!-- CSS --> |
|
37 |
+ <link rel="stylesheet" href="/lib/formalize.css" media="screen" /> |
|
38 |
+ <link rel="stylesheet" href="/css/main.css" media="screen" /> |
|
39 |
+</head> |
|
40 |
+ |
|
41 |
+<body ng-app="marrowLogin"> |
|
42 |
+ <header> |
|
43 |
+ <h1 class="site-logo">Marrow</h1> |
|
44 |
+ </header> |
|
45 |
+ <main ng-controller="LoginCtrl"> |
|
46 |
+ <div id="login_form"> |
|
47 |
+ <form> |
|
48 |
+ <div class="message">{{message}}</div> |
|
49 |
+ <input type="text" ng-model="username" placeholder="Username" /> |
|
50 |
+ <input type="password" ng-model="password" placeholder="Password" /> |
|
51 |
+ <button ng-click="login()">Log In</button> |
|
52 |
+ <button ng-click="newuser()">Add User</button> |
|
53 |
+ </form> |
|
54 |
+ </div> |
|
55 |
+ </main> |
|
56 |
+<!-- Piwik --> |
|
57 |
+<script type="text/javascript"> |
|
58 |
+ var _paq = _paq || []; |
|
59 |
+ _paq.push(["setDocumentTitle", document.domain + "/" + document.title]); |
|
60 |
+ _paq.push(["setCookieDomain", "*.joinmarrow.com"]); |
|
61 |
+ _paq.push(["setDomains", ["*.joinmarrow.com"]]); |
|
62 |
+ _paq.push(['trackPageView']); |
|
63 |
+ _paq.push(['enableLinkTracking']); |
|
64 |
+ (function() { |
|
65 |
+ var u="//piwik.elangley.org/"; |
|
66 |
+ _paq.push(['setTrackerUrl', u+'piwik.php']); |
|
67 |
+ _paq.push(['setSiteId', 2]); |
|
68 |
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; |
|
69 |
+ g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); |
|
70 |
+ })(); |
|
71 |
+</script> |
|
72 |
+<noscript><p><img src="//piwik.elangley.org/piwik.php?idsite=2" style="border:0;" alt="" /></p></noscript> |
|
73 |
+<!-- End Piwik Code --> |
|
74 |
+</body> |
|
75 |
+</html> |
... | ... |
@@ -12,7 +12,7 @@ |
12 | 12 |
</h2> |
13 | 13 |
<form> |
14 | 14 |
<input type="text" ng-model="postobj.url" placeholder="http:// . . ."/> |
15 |
- <button type="submit" ng-click="addLink(postobj.url)">+</button> |
|
15 |
+ <button type="submit" ng-click="addLink(postobj.url)" analytics-on="click" analytics-category="link" analytics-label="add" analytics-event="{{postobj.url}}">+</button> |
|
16 | 16 |
</form> |
17 | 17 |
<ul class="bone-list"> |
18 | 18 |
<li ng-repeat="marrow in bone.marrow" |
... | ... |
@@ -22,7 +22,9 @@ |
22 | 22 |
data-posted="{{marrow.posted}}" |
23 | 23 |
data-title="{{marrow.title}}" |
24 | 24 |
data-votes="{{marrow.votes}}"> |
25 |
- <a href class="delete-link" ng-click="delete(marrow.id)" title="Delete Link from List">(-)</a> |
|
25 |
+ <a href class="delete-link" ng-click="delete(marrow.id)" title="Delete Link from List" |
|
26 |
+ analytics-on="click" analytics-category="link" analytics-label="delete" |
|
27 |
+ analytics-event="{{marrow.url}}">(-)</a> |
|
26 | 28 |
<div class="marrow"> |
27 | 29 |
<span class="vote-disp">{{marrow.votes}}</span> |
28 | 30 |
<span ng-if="marrow.title"> |
... | ... |
@@ -2,8 +2,8 @@ |
2 | 2 |
<h2 class="section-title"> |
3 | 3 |
{{bone.sectionTitle}} ({{filtered.length}} items) |
4 | 4 |
</h2> |
5 |
- <form> |
|
6 |
- <ul class="subscription-list"> |
|
5 |
+ <form class="filter-form"> |
|
6 |
+ <ul ng-class="{'subscription-list': true, 'opened': opened}"> |
|
7 | 7 |
<li class="sub-filter"> |
8 | 8 |
<input id="sub-all" type="checkbox" ng-model="friend.all" ng-click="uncheckOthers(friend)"></input> |
9 | 9 |
<label for="sub-all">[All]</label> |
... | ... |
@@ -15,6 +15,10 @@ |
15 | 15 |
<span class="wide">{{name}}</span> |
16 | 16 |
</label> |
17 | 17 |
</li> |
18 |
+ <div class="list-control" ng-click="opened = ! opened"> |
|
19 |
+ <span ng-if="opened">Hide . . .</span> |
|
20 |
+ <span ng-if="!opened">Filter . . .</span> |
|
21 |
+ </div> |
|
18 | 22 |
</ul> |
19 | 23 |
</form> |
20 | 24 |
<ul class="bone-list"> |
... | ... |
@@ -30,15 +34,20 @@ |
30 | 34 |
<span class="voting"> |
31 | 35 |
<span class="score">{{marrow.votes}}</span> |
32 | 36 |
<button class="upVote vote-button fa fa-plus" ng-class="{selected: marrow.myVote===1}" ng-click="upVote(marrow)" |
33 |
- analytics-on="click" analytics-event="vote" analytics-category="{{marrow.myVote===0? 'up' : 'zero'}}"></button> |
|
37 |
+ analytics-on="click" analytics-event="vote" analytics-category="{{marrow.myVote===0? 'up' : 'zero'}}"> |
|
38 |
+ </button> |
|
34 | 39 |
</span> |
35 | 40 |
<span ng-if="marrow.title"> |
36 | 41 |
<a href="{{marrow.url}}" class="list-item">{{marrow.title}}</a> |
37 | 42 |
<br /> |
38 | 43 |
</span> |
44 |
+ <a class="add-link" ng-click="reshare(marrow)" title="Reshare Link" |
|
45 |
+ analytics-on="click" analytics-category="link" analytics-label="reshare" |
|
46 |
+ analytics-event="{{marrow.url}}">{{!marrow.shared? 'Reshare': '✓'}}</a> |
|
47 |
+ — |
|
39 | 48 |
<a href="{{marrow.url}}" ng-class="{'de-emphasize':marrow.title}" >{{marrow.url}}</a> |
40 | 49 |
</div> |
41 |
- <user-badge poster="{{marrow.poster}}" rep="{{friends.reps.reputation[marrow.poster]}}"></user-badge> |
|
50 |
+ <user-badge poster="{{marrow.poster}}"></user-badge> |
|
42 | 51 |
</li> |
43 | 52 |
</ul> |
44 | 53 |
<a ng-click="backAPage()" class="more-link"/>More</a> |