24 |
25 |
26 | {% for u in users %}
27 |
28 |
29 |
}})
30 |
31 |
32 |
{{u.name}}
33 | {%if u.is_pdf_ready()%}
34 |
[PDF]
35 | {%endif%}
36 |
N.{{u.id}}
37 |
38 |
39 | {%for ua in u.get_alias()%}
40 | {%set homepage_info = ua.get_homepage_url()%}
41 | {%if homepage_info%}
42 |
43 |
44 |
45 | {%endif%}
46 | {%endfor%}
47 |
48 |
49 |
50 | {% endfor %}
51 |
52 |
53 |
54 | {%endblock%}
55 |
56 |
--------------------------------------------------------------------------------
/past/templates/note.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% import "blocks.html" as blocks %}
4 |
5 | {% block css %}
6 | {{super()}}
7 |
8 | {% endblock %}
9 |
10 | {% block rightbar_block %}
11 | {{super()}}
12 | {{blocks.rightbar_note_block()}}
13 | {{blocks.rightbar_markdown_block()}}
14 | {{blocks.rightbar_feedback_block()}}
15 | {% endblock %}
16 |
17 | {% block middlebar_block %}
18 |
19 |
首页 >
日记
20 | {%if g.user%}
21 |
> 写日记
22 | {%endif%}
23 |
24 |
25 |
26 |
27 | {{ blocks.notification_block(g.user) }}
28 |
29 |
30 |
31 |
32 |
{{title}}
33 |
34 |
{{create_time.strftime("%Y-%m-%d %H:%M:%S")}}
35 | {%if g.user and g.user.id == note.user_id%}
36 |
编辑
37 | {%else%}
38 |
作者:{{user.name}}
39 | {%endif%}
40 |
41 |
42 |
43 | {%if note.fmt == consts.NOTE_FMT_MARKDOWN%}
44 | {{content|safe}}
45 | {%else%}
46 |
{{content}}
47 | {%endif%}
48 |
49 |
50 |
51 |
52 | {%if not fmt or fmt==consts.NOTE_FMT_PLAIN%}
53 | 格式:文本
54 | {%elif fmt==consts.NOTE_FMT_MARKDOWN %}
55 | 格式:Markdown
56 | {%endif%}
57 |
58 |
59 | {% endblock %}
60 |
--------------------------------------------------------------------------------
/past/view/utils.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | from functools import wraps
4 | from flask import g, flash, redirect, url_for, abort
5 |
6 | from past.model.user import User
7 | from past.model.note import Note
8 | from past import consts
9 | from past import config
10 |
11 | def require_login(msg="", redir=""):
12 | def _(f):
13 | @wraps(f)
14 | def __(*a, **kw):
15 | if not g.user:
16 | flash(msg and msg.decode("utf8") or u"为了保护用户的隐私,请先登录^^", "tip")
17 | return redirect(redir or "/home")
18 | return f(*a, **kw)
19 | return __
20 | return _
21 |
22 | def check_access_user(user):
23 | user_privacy = user.get_profile_item('user_privacy')
24 | if user_privacy == consts.USER_PRIVACY_PRIVATE and not (g.user and g.user.id == user.id):
25 | return (403, "由于该用户设置了仅自己可见的权限,所以,我们就看不到了")
26 | elif user_privacy == consts.USER_PRIVACY_THEPAST and not g.user:
27 | return (403, "由于用户设置了仅登录用户可见的权限,所以,需要登录后再看")
28 |
29 | def check_access_note(note):
30 | if note.privacy == consts.STATUS_PRIVACY_PRIVATE and not (g.user and g.user.id == note.user_id):
31 | return (403, "由于该日记设置了仅自己可见的权限,所以,我们就看不到了")
32 | elif note.privacy == consts.STATUS_PRIVACY_THEPAST and not g.user:
33 | return (403, "由于该日记设置了仅登录用户可见的权限,所以,需要登录后再看")
34 |
35 | ## 把status_list构造为month,day的层级结构
36 | def statuses_timelize(status_list):
37 |
38 | hashed = {}
39 | for s in status_list:
40 | hash_s = hash(s)
41 | if hash_s not in hashed:
42 | hashed[hash_s] = RepeatedStatus(s)
43 | else:
44 | hashed[hash_s].status_list.append(s)
45 |
46 | return sorted(hashed.values(), key=lambda x:x.create_time, reverse=True)
47 |
48 | class RepeatedStatus(object):
49 | def __init__(self, status):
50 | self.create_time = status.create_time
51 | self.status_list = [status]
52 |
53 | def get_sync_list(user):
54 | print '------user:',user
55 | user_binded_providers = [ua.type for ua in user.get_alias() if ua.type in config.CAN_SHARED_OPENID_TYPE]
56 |
57 | sync_list = []
58 | for t in user_binded_providers:
59 | p = user.get_thirdparty_profile(t)
60 | if p and p.get("share") == "Y":
61 | sync_list.append([t, "Y"])
62 | else:
63 | sync_list.append([t, "N"])
64 | return sync_list
65 |
--------------------------------------------------------------------------------
/past/api/error.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime
3 | from tweepy.error import TweepError
4 | from past.model.user import User
5 |
6 | class OAuthError(Exception):
7 | def __init__(self, msg_type, user_id, openid_type, msg):
8 | self.msg_type = msg_type
9 | self.user_id = user_id
10 | self.openid_type = openid_type
11 | self.msg = msg
12 |
13 | def __str__(self):
14 | return "OAuthError: user:%s, openid_type:%s, %s, %s" % \
15 | (self.user_id, self.openid_type, self.msg_type, self.msg)
16 | __repr__ = __str__
17 |
18 | def set_the_profile(self, flush=False):
19 | if self.user_id:
20 | u = User.get(self.user_id)
21 | if u:
22 | if flush:
23 | u.set_thirdparty_profile_item(self.openid_type, self.msg_type, datetime.datetime.now())
24 | else:
25 | p = u.get_thirdparty_profile(self.openid_type)
26 | t = p and p.get(self.msg_type)
27 | u.set_thirdparty_profile_item(self.openid_type, self.msg_type, t or datetime.datetime.now())
28 |
29 | def clear_the_profile(self):
30 | if self.user_id:
31 | u = User.get(self.user_id)
32 | if u:
33 | u.set_thirdparty_profile_item(self.openid_type, self.msg_type, "")
34 |
35 | def is_exception_exists(self):
36 | if self.user_id:
37 | u = User.get(self.user_id)
38 | p = u and u.get_thirdparty_profile(self.openid_type)
39 | return p and p.get(self.msg_type)
40 |
41 |
42 | class OAuthTokenExpiredError(OAuthError):
43 | TYPE = "expired"
44 | def __init__(self, user_id=None, openid_type=None, msg=""):
45 | super(OAuthTokenExpiredError, self).__init__(
46 | OAuthTokenExpiredError.TYPE, user_id, openid_type, msg)
47 |
48 | class OAuthAccessError(OAuthError):
49 | TYPE = "access_error"
50 | def __init__(self, user_id=None, openid_type=None, msg=""):
51 | super(OAuthAccessError, self).__init__(
52 | OAuthAccessError.TYPE, user_id, openid_type, msg)
53 |
54 |
55 | class OAuthLoginError(OAuthError):
56 | TYPE = "login"
57 | def __init__(self, user_id=None, openid_type=None, msg=""):
58 | if isinstance(msg, TweepError):
59 | msg = "%s:%s" %(msg.reason, msg.response)
60 | super(OAuthLoginError, self).__init__(
61 | OAuthLoginError.TYPE, user_id, openid_type, msg)
62 |
63 |
--------------------------------------------------------------------------------
/tools/move_data_from_redis_to_mongo.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | import sys
4 | sys.path.append('../')
5 | import datetime
6 |
7 | from past.store import mongo_conn, db_conn
8 | from past.utils.escape import json_decode, json_encode
9 |
10 | def move_user_profile():
11 | RAW_USER_REDIS_KEY = "/user/raw/%s"
12 |
13 | cursor = db_conn.execute("select id from user order by id")
14 | rows = cursor.fetchall()
15 | cursor and cursor.close()
16 | for row in rows:
17 | print '--------user raw id:', row[0]
18 | sys.stdout.flush()
19 | r = redis_conn.get(RAW_USER_REDIS_KEY % row[0])
20 | if r:
21 | mongo_conn.set(RAW_USER_REDIS_KEY % row[0], r)
22 | r2 = redis_conn.get("/profile/%s" % row[0])
23 | if r2:
24 | mongo_conn.set("/profile/%s" % row[0], r2)
25 |
26 | def move_status():
27 | STATUS_REDIS_KEY = "/status/text/%s"
28 | RAW_STATUS_REDIS_KEY = "/status/raw/%s"
29 |
30 | start = 318003
31 | limit = 2500
32 | r =db_conn.execute("select count(1) from status")
33 | total = r.fetchone()[0]
34 | print '----total status:', total
35 | sys.stdout.flush()
36 |
37 | while (start <= int(total)):
38 | print '-------start ', start
39 | sys.stdout.flush()
40 | cursor = db_conn.execute("select id from status order by id limit %s,%s", (start, limit))
41 | rows = cursor.fetchall()
42 | if rows:
43 | keys = [STATUS_REDIS_KEY % row[0] for row in rows]
44 | values = redis_conn.mget(*keys)
45 | print '+++ mget text:', datetime.datetime.now()
46 | docs = []
47 | for i in xrange(0, len(keys)):
48 | if values[i]:
49 | docs.append({"k":keys[i], "v":values[i]})
50 | mongo_conn.get_connection().insert(docs)
51 | ##mongo_conn.set(keys[i], values[i])
52 | print '+++ inserted text:', datetime.datetime.now()
53 |
54 | keys = [RAW_STATUS_REDIS_KEY % row[0] for row in rows]
55 | values = redis_conn.mget(*keys)
56 | print '+++ mget raw:', datetime.datetime.now()
57 | docs = []
58 | for i in xrange(0, len(keys)):
59 | if values[i]:
60 | docs.append({"k":keys[i], "v":values[i]})
61 | mongo_conn.get_connection().insert(docs)
62 | print '+++ inserted raw:', datetime.datetime.now()
63 |
64 | start += limit
65 |
66 | #move_user_profile()
67 | move_status()
68 |
--------------------------------------------------------------------------------
/past/static/js/scrollpagination.js:
--------------------------------------------------------------------------------
1 | /*
2 | ** Anderson Ferminiano
3 | ** contato@andersonferminiano.com -- feel free to contact me for bugs or new implementations.
4 | ** jQuery ScrollPagination
5 | ** 28th/March/2011
6 | ** http://andersonferminiano.com/jqueryscrollpagination/
7 | ** You may use this script for free, but keep my credits.
8 | ** Thank you.
9 | */
10 |
11 | (function( $ ){
12 |
13 |
14 | $.fn.scrollPagination = function(options) {
15 |
16 | var opts = $.extend($.fn.scrollPagination.defaults, options);
17 | var target = opts.scrollTarget;
18 | if (target == null){
19 | target = obj;
20 | }
21 | opts.scrollTarget = target;
22 |
23 | return this.each(function() {
24 | $.fn.scrollPagination.init($(this), opts);
25 | });
26 |
27 | };
28 |
29 | $.fn.stopScrollPagination = function(){
30 | return this.each(function() {
31 | $(this).attr('scrollPagination', 'disabled');
32 | });
33 |
34 | };
35 |
36 | $.fn.scrollPagination.loadContent = function(obj, opts){
37 | var target = opts.scrollTarget;
38 | var mayLoadContent = $(target).scrollTop()+opts.heightOffset >= $(document).height() - $(target).height();
39 | if (mayLoadContent){
40 | if (opts.beforeLoad != null){
41 | opts.beforeLoad();
42 | }
43 | $(obj).children().attr('rel', 'loaded');
44 | $(obj).attr('scrollPagination', 'disabled');
45 | $.ajax({
46 | type: 'POST',
47 | url: opts.contentPage,
48 | data: opts.contentData,
49 | success: function(data){
50 | $(obj).append(data);
51 | var objectsRendered = $(obj).children('[rel!=loaded]');
52 |
53 | if (opts.afterLoad != null){
54 | opts.afterLoad(objectsRendered);
55 | }
56 | $(obj).attr('scrollPagination', 'enabled');
57 | },
58 | dataType: 'html'
59 | });
60 | }
61 |
62 | };
63 |
64 | $.fn.scrollPagination.init = function(obj, opts){
65 | var target = opts.scrollTarget;
66 | $(obj).attr('scrollPagination', 'enabled');
67 |
68 | $(target).scroll(function(event){
69 | if ($(obj).attr('scrollPagination') == 'enabled'){
70 | $.fn.scrollPagination.loadContent(obj, opts);
71 | }
72 | else {
73 | event.stopPropagation();
74 | }
75 | });
76 |
77 | $.fn.scrollPagination.loadContent(obj, opts);
78 |
79 | };
80 |
81 | $.fn.scrollPagination.defaults = {
82 | 'contentPage' : null,
83 | 'contentData' : {},
84 | 'beforeLoad': null,
85 | 'afterLoad': null ,
86 | 'scrollTarget': null,
87 | 'heightOffset': 0
88 | };
89 | })( jQuery );
--------------------------------------------------------------------------------
/tools/remove_user.py:
--------------------------------------------------------------------------------
1 | import sys
2 | sys.path.append('../')
3 |
4 | from past.store import db_conn
5 | from past.model.user import User
6 | from past.model.status import Status
7 | from past.model.kv import RawStatus
8 | from past import consts
9 | from past import config
10 |
11 | from past.utils.logger import logging
12 | log = logging.getLogger(__file__)
13 |
14 | suicide_log = logging.getLogger(__file__)
15 | suicide_log.addHandler(logging.FileHandler(config.SUICIDE_LOG))
16 |
17 | def remove_user(uid, clear_status=True):
18 | user = User.get(uid)
19 | if not user:
20 | print '---no user:%s' % uid
21 |
22 | suicide_log.info("---- delete from user, uid=%s" %uid)
23 | db_conn.execute("delete from user where id=%s", uid)
24 | db_conn.commit()
25 | User._clear_cache(uid)
26 |
27 | if clear_status:
28 | cursor = db_conn.execute("select id from status where user_id=%s", uid)
29 | if cursor:
30 | rows = cursor.fetchall()
31 | for row in rows:
32 | sid = row[0]
33 | suicide_log.info("---- delete status text, sid=%s" % sid)
34 | RawStatus.remove(sid)
35 |
36 | suicide_log.info("---- delete from status, uid=" %uid)
37 | db_conn.execute("delete from status where user_id=%s", uid)
38 | db_conn.commit()
39 | Status._clear_cache(uid, None)
40 |
41 | suicide_log.info("---- delete from passwd, uid=%s" %uid)
42 | db_conn.execute("delete from passwd where user_id=%s", uid)
43 | suicide_log.info("---- delete from sync_task, uid=%s" % uid)
44 | db_conn.execute("delete from sync_task where user_id=%s", uid)
45 | suicide_log.info("---- delete from user_alias, uid=%s" % uid)
46 | db_conn.execute("delete from user_alias where user_id=%s", uid)
47 | db_conn.commit()
48 |
49 |
50 | def remove_status(uid):
51 | cursor = db_conn.execute("select id from status where user_id=%s", uid)
52 | if cursor:
53 | rows = cursor.fetchall()
54 | for row in rows:
55 | sid = row[0]
56 | print "---- delete mongo text, sid=", sid
57 | RawStatus.remove(sid)
58 |
59 | print "---- delete from status, uid=", uid
60 | db_conn.execute("delete from status where user_id=%s", uid)
61 | db_conn.commit()
62 | Status._clear_cache(uid, None)
63 |
64 | if __name__ == "__main__":
65 | a = sys.argv
66 | uids = a[1:]
67 | for uid in uids:
68 | print "----- remove user:", uid
69 | remove_user(uid)
70 | print "----- remove status of user:", uid
71 | remove_status(uid)
72 |
73 |
74 |
--------------------------------------------------------------------------------
/past/templates/past.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% import "status.html" as status_tmpl_helper %}
4 | {% import "blocks.html" as blocks %}
5 |
6 |
7 | {% block css %}
8 | {{super()}}
9 |
10 |
11 | {% endblock %}
12 |
13 | {% block js%}
14 | {{super()}}
15 |
16 | {% endblock %}
17 |
18 | {% block title_block %} {{g.user.name}}{%endblock%}
19 |
20 | {%block content_block%}
21 |
22 | {{self.middlebar_block()}}
23 |
24 | {{blocks.rightbar_intros_block(intros)}}
25 | {{blocks.rightbar_note_block()}}
26 | {{blocks.rightbar_feedback_block()}}
27 |
28 |
29 |
30 | {%endblock%}
31 |
32 | {% block middlebar_block %}
33 |
34 | {%if not history_status%}
35 |
36 |
去年、前年、大前年... 都没有留下什么东西。
37 |
今天记录一下 明年再来看。
38 |
39 | {%else%}
40 | {%for t, status_list in history_status.items()%}
41 |
{{t.decode("utf8")}},找到{{status_list|length}}条往事
42 |
43 |
44 | {%set left = True%}
45 | {%for repeated_status in status_list%}
46 | {%if left%}
47 | -
48 | {%else%}
49 |
-
50 | {%endif%}
51 |
52 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}}
53 | {%set left = not left%}
54 |
55 | {%endfor%}
56 |
57 |
58 | {%endfor%}
59 | {%endif%}
60 |
61 |
62 |
63 |
68 |
69 |
74 |
75 | {% endblock %}
76 |
77 |
--------------------------------------------------------------------------------
/past/view/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 | from flask import g, session, request, \
3 | redirect, url_for, abort, render_template, flash
4 |
5 | from past import app
6 | from past import config
7 | from past.store import db_conn
8 | from past.model.user import User, UserAlias
9 | from past.corelib import auth_user_from_session
10 |
11 | import settings, pdf_view, note, user_past, views
12 |
13 | @app.before_request
14 | def before_request():
15 | g.config = config
16 | g.user = auth_user_from_session(session)
17 | #g.user = User.get(2)
18 | g.user_alias = UserAlias.gets_by_user_id(g.user.id) if g.user else None
19 |
20 | if request.method == 'POST':
21 | try:
22 | g.start = int(request.form.get('start', 0))
23 | except ValueError:
24 | g.start = 0
25 | try:
26 | g.count = int(request.form.get('count', 24))
27 | except ValueError:
28 | g.count = 0
29 | g.cate = request.form.get("cate", "")
30 | else:
31 | try:
32 | g.start = int(request.args.get('start', 0))
33 | except ValueError:
34 | g.start = 0
35 | try:
36 | g.count = int(request.args.get('count', 24))
37 | except ValueError:
38 | g.count = 0
39 | g.cate = request.args.get("cate", "")
40 | g.cate = int(g.cate) if g.cate.isdigit() else ""
41 |
42 | if g.user:
43 | g.binds = [ua.type for ua in g.user.get_alias()]
44 | unbinded = list(set(config.OPENID_TYPE_DICT.values()) -
45 | set(g.binds) - set([config.OPENID_TYPE_DICT[config.OPENID_THEPAST]]))
46 | tmp = {}
47 | for k, v in config.OPENID_TYPE_DICT.items():
48 | tmp[v] = k
49 | g.unbinded = [[x, tmp[x], config.OPENID_TYPE_NAME_DICT[x]] for x in unbinded]
50 |
51 | expired_providers = []
52 | for t in [ua.type for ua in g.user.get_alias()]:
53 | p = g.user.get_thirdparty_profile(t)
54 | if p and p.get("expired"):
55 | _ = [t, config.OPENID_TYPE_DICT_REVERSE.get(t), config.OPENID_TYPE_NAME_DICT.get(t, "")]
56 | expired_providers.append(_)
57 | g.expired = expired_providers
58 | if expired_providers:
59 | msg = " ".join([x[-1] for x in expired_providers])
60 | flash(u"你的 %s 授权已经过期了,会影响数据同步,你可以重新授权 :)" % msg, "tip")
61 | else:
62 | g.unbinded = None
63 |
64 | @app.teardown_request
65 | def teardown_request(exception):
66 | #http://stackoverflow.com/questions/9318347/why-are-some-mysql-connections-selecting-old-data-the-mysql-database-after-a-del
67 | db_conn.commit()
68 |
--------------------------------------------------------------------------------
/tools/move_data_from_mongo_to_mysql.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | import sys
4 | sys.path.append('../')
5 | import datetime
6 | import os
7 | from MySQLdb import IntegrityError
8 | from past.store import mongo_conn, db_conn
9 | from past.model.kv import UserProfile, RawStatus, Kv
10 | from past.utils.escape import json_decode, json_encode
11 |
12 | def move_user_profile():
13 | RAW_USER_REDIS_KEY = "/user/raw/%s"
14 |
15 | cursor = db_conn.execute("select id from user order by id")
16 | rows = cursor.fetchall()
17 | cursor and cursor.close()
18 | for row in rows:
19 | print '--------user raw id:', row[0]
20 | sys.stdout.flush()
21 | r1 = mongo_conn.get(RAW_USER_REDIS_KEY % row[0])
22 | if r1:
23 | print "r1"
24 | #UserProfile.set(row[0], r1)
25 | Kv.set('/profile/%s' %row[0], r1)
26 | r2 = mongo_conn.get("/profile/%s" % row[0])
27 | if r2:
28 | #Kv.set('/profile/%s' %row[0], r2)
29 | UserProfile.set(row[0], r2)
30 |
31 | def myset(status_id, text, raw):
32 | cursor = None
33 | text = json_encode(text) if not isinstance(text, basestring) else text
34 | raw = json_encode(raw) if not isinstance(raw, basestring) else raw
35 |
36 | db_conn.execute('''replace into raw_status (status_id, text, raw)
37 | values(%s,%s,%s)''', (status_id, text, raw))
38 |
39 |
40 | def move_status():
41 | STATUS_REDIS_KEY = "/status/text/%s"
42 | RAW_STATUS_REDIS_KEY = "/status/raw/%s"
43 |
44 | start = 3720000
45 | limit = 100000
46 | #r =db_conn.execute("select count(1) from status")
47 | #total = r.fetchone()[0]
48 | total = 4423725
49 | print '----total status:', total
50 | sys.stdout.flush()
51 |
52 | ef = open("error.log", "a")
53 | #cf = open("cmd.txt", "w")
54 | while (start <= int(total)):
55 | f = open("./midfile.txt", "w")
56 | print '-------start ', start
57 | sys.stdout.flush()
58 | cursor = db_conn.execute("select id from status order by id limit %s,%s", (start, limit))
59 | rows = cursor.fetchall()
60 | for row in rows:
61 | text = mongo_conn.get(STATUS_REDIS_KEY % row[0])
62 | raw = mongo_conn.get(RAW_STATUS_REDIS_KEY% row[0])
63 | if text and raw:
64 | text = json_encode(text) if not isinstance(text, basestring) else text
65 | raw = json_encode(raw) if not isinstance(raw, basestring) else raw
66 |
67 | db_conn.execute('''replace into raw_status (status_id, text, raw)
68 | values(%s,%s,%s)''', (row[0], text, raw))
69 | db_conn.commit()
70 | start += limit
71 |
72 |
73 | move_user_profile()
74 | move_status()
75 |
--------------------------------------------------------------------------------
/past/model/user_tokens.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 | # for dev -> api
3 |
4 | from MySQLdb import IntegrityError
5 | from past.corelib.cache import cache, pcache
6 | from past.store import mc, db_conn
7 |
8 | class UserTokens(object):
9 | def __init__(self, id, user_id, token, device):
10 | self.id = id
11 | self.user_id = str(user_id)
12 | self.token = str(token)
13 | self.device = device
14 |
15 | def __repr__(self):
16 | return "
" \
17 | % (self.id, self.user_id, self.token, self.device)
18 | __str__ = __repr__
19 |
20 | @classmethod
21 | @cache("user_token:{id}")
22 | def get(cls, id):
23 | return cls._find_by("id", id)
24 |
25 | @classmethod
26 | @cache("user_token:{token}")
27 | def get_by_token(cls, token):
28 | return cls._find_by("token", token)
29 |
30 | @classmethod
31 | @cache("user_token_ids:{user_id}")
32 | def get_ids_by_user_id(cls, user_id):
33 | r = cls._find_by("user_id", user_id, limit=0)
34 | if r:
35 | return [r.id for x in r]
36 | else:
37 | return []
38 |
39 | @classmethod
40 | def add(cls, user_id, token, device=""):
41 | cursor = None
42 | try:
43 | cursor = db_conn.execute('''insert into user_tokens (user_id, token, device)
44 | values (%s, %s, %s)''', (user_id, token, device))
45 | id_ = cursor.lastrowid
46 | db_conn.commit()
47 | return cls.get(id_)
48 | except IntegrityError:
49 | db_conn.rollback()
50 | finally:
51 | cursor and cursor.close()
52 |
53 | def remove(self):
54 | db_conn.execute('''delete from user_tokens where id=%s''', self.id)
55 | db_conn.commit()
56 | self._clear_cache()
57 |
58 | def _clear_cache(self):
59 | mc.delete("user_token:%s" % self.id)
60 | mc.delete("user_token:%s" % self.token)
61 | mc.delete("user_token_ids:%s" % self.user_id)
62 |
63 | @classmethod
64 | def _find_by(cls, col, value, start=0, limit=1):
65 | assert limit >= 0
66 | if limit == 0:
67 | cursor = db_conn.execute("""select id, user_id, token, device
68 | from user_tokens where `""" + col + """`=%s""", value)
69 | else:
70 | cursor = db_conn.execute("""select id, user_id, token, device
71 | from user_tokens where `""" + col + """`=%s limit %s, %s""", (value, start, limit))
72 | if limit == 1:
73 | row = cursor.fetchone()
74 | cursor and cursor.close()
75 | return row and cls(*row)
76 | else:
77 | rows = cursor.fetchall()
78 | cursor and cursor.close()
79 | return [cls(*row) for row in rows]
80 |
--------------------------------------------------------------------------------
/deploy/nginx-sites-available/sample:
--------------------------------------------------------------------------------
1 | # You may add here your
2 | # server {
3 | # ...
4 | # }
5 | # statements for each of your virtual hosts to this file
6 |
7 | ##
8 | # You should look at the following URL's in order to grasp a solid understanding
9 | # of Nginx configuration files in order to fully unleash the power of Nginx.
10 | # http://wiki.nginx.org/Pitfalls
11 | # http://wiki.nginx.org/QuickStart
12 | # http://wiki.nginx.org/Configuration
13 | #
14 | # Generally, you will want to move this file somewhere, and start with a clean
15 | # file but keep this around for reference. Or just disable in sites-enabled.
16 | #
17 | # Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
18 | ##
19 |
20 | server {
21 | #listen 80; ## listen for ipv4; this line is default and implied
22 | #listen [::]:80 default ipv6only=on; ## listen for ipv6
23 |
24 | root /usr/share/nginx/www;
25 | index index.html index.htm;
26 |
27 | # Make site accessible from http://localhost/
28 | server_name localhost;
29 |
30 | location / {
31 | # First attempt to serve request as file, then
32 | # as directory, then fall back to index.html
33 | try_files $uri $uri/ /index.html;
34 | }
35 |
36 | location /doc/ {
37 | alias /usr/share/doc;
38 | autoindex on;
39 | allow 127.0.0.1;
40 | deny all;
41 | }
42 |
43 | #error_page 404 /404.html;
44 |
45 | # redirect server error pages to the static page /50x.html
46 | #
47 | #error_page 500 502 503 504 /50x.html;
48 | #location = /50x.html {
49 | # root /usr/share/nginx/www;
50 | #}
51 |
52 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
53 | #
54 | #location ~ \.php$ {
55 | # fastcgi_split_path_info ^(.+\.php)(/.+)$;
56 | # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
57 | #
58 | # fastcgi_pass 127.0.0.1:9000;
59 | # fastcgi_index index.php;
60 | # include fastcgi_params;
61 | #}
62 |
63 | # deny access to .htaccess files, if Apache's document root
64 | # concurs with nginx's one
65 | #
66 | #location ~ /\.ht {
67 | # deny all;
68 | #}
69 | }
70 |
71 |
72 | # another virtual host using mix of IP-, name-, and port-based configuration
73 | #
74 | #server {
75 | # listen 8000;
76 | # listen somename:8080;
77 | # server_name somename alias another.alias;
78 | # root html;
79 | # index index.html index.htm;
80 | #
81 | # location / {
82 | # try_files $uri $uri/ /index.html;
83 | # }
84 | #}
85 |
86 |
87 | # HTTPS server
88 | #
89 | #server {
90 | # listen 443;
91 | # server_name localhost;
92 | #
93 | # root html;
94 | # index index.html index.htm;
95 | #
96 | # ssl on;
97 | # ssl_certificate cert.pem;
98 | # ssl_certificate_key cert.key;
99 | #
100 | # ssl_session_timeout 5m;
101 | #
102 | # ssl_protocols SSLv3 TLSv1;
103 | # ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
104 | # ssl_prefer_server_ciphers on;
105 | #
106 | # location / {
107 | # try_files $uri $uri/ /index.html;
108 | # }
109 | #}
110 |
--------------------------------------------------------------------------------
/past/templates/v2/note_create.html:
--------------------------------------------------------------------------------
1 | {% extends "v2/base.html" %}
2 | {% import "status.html" as status_tmpl_helper %}
3 | {% import "blocks.html" as blocks_tmpl_helper %}
4 |
5 |
6 | {%block title%}写日记{{user.name}} | 旧时光{%endblock%}
7 |
8 | {%block middlebar%}
9 |
10 |
45 |
46 |
47 |
48 | thepast友情提醒,每天只能记一篇日记,写好了就不能再修改。
49 |
50 |
51 |
52 | {%endblock%}
53 |
54 | {%block more_js%}
55 | {{super()}}
56 |
77 | {%endblock%}
78 |
--------------------------------------------------------------------------------
/past/templates/v2/user_past.html:
--------------------------------------------------------------------------------
1 | {% extends "v2/timeline_base.html" %}
2 | {% import "status.html" as status_tmpl_helper %}
3 |
4 | {%block css%}
5 | {{super()}}
6 |
7 | {%endblock%}
8 |
9 | {% block js%}
10 | {{super()}}
11 |
12 | {% endblock %}
13 |
14 | {%block sidebox%}
15 |
23 | {{super()}}
24 | {%endblock%}
25 |
26 | {%block rightbar%}
27 |
28 |
29 | {%if not history_status%}
30 |
31 |
去年、前年、大前年的今天... 都没有留下什么东西。
32 |
今天记录一下 明年再来看。
33 |
或者
34 |
看看历史上的昨天和前天吧:)
35 |
36 | {%else%}
37 | {%for t in history_status.keys()|sort(reverse=True)%}
38 | {%set status_list = history_status.get(t)%}
39 |
40 |
{{t.decode("utf8")}}
41 |
共找到 {{status_list|length}} 条往事。
42 |
43 |
44 |
45 |
46 | {%set left = True%}
47 | {%for repeated_status in status_list%}
48 | {%if left%}
49 | -
50 | {%else%}
51 |
-
52 | {%endif%}
53 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}}
54 |
55 | {%set left=not left%}
56 | {%endfor%}
57 |
58 | {%endfor%}
59 |
60 | {%endif%}
61 |
62 |
63 | {%endblock%}
64 |
65 | {%block more_js%}
66 | {{super()}}
67 |
90 | {%endblock%}
91 |
--------------------------------------------------------------------------------
/cronjob/generate_pdf.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | import sys
4 | sys.path.append("../")
5 |
6 | import time
7 | import datetime
8 | import calendar
9 | import commands
10 |
11 | activate_this = '../env/bin/activate_this.py'
12 | execfile(activate_this, dict(__file__=activate_this))
13 |
14 | from past.utils.pdf import generate_pdf, get_pdf_filename, is_pdf_file_exists, get_pdf_full_filename
15 | from past.model.user import User, UserAlias, PdfSettings
16 | from past.model.status import Status
17 | from past import config
18 |
19 |
20 | def generate(user_id, date, order='asc'):
21 | try:
22 | uas = UserAlias.gets_by_user_id(user_id)
23 | if not uas:
24 | return
25 |
26 | start_date = datetime.datetime(date.year, date.month, 1)
27 | end_date = datetime.datetime(date.year, date.month,
28 | calendar.monthrange(date.year, date.month)[1], 23, 59, 59)
29 |
30 | pdf_filename = get_pdf_filename(user_id, date.strftime("%Y%m"), "")
31 | pdf_filename_compressed = get_pdf_filename(user_id, date.strftime("%Y%m"))
32 | print '----generate pdf:', start_date, ' to ', end_date, ' file is', pdf_filename
33 |
34 | if is_pdf_file_exists(pdf_filename_compressed):
35 | print '---- %s exists, so ignore...' % pdf_filename_compressed
36 | return
37 |
38 | status_ids = Status.get_ids_by_date(user_id, start_date, end_date)[:900]
39 | if order == 'asc':
40 | status_ids = status_ids[::-1]
41 | if not status_ids:
42 | print '----- status ids is none', status_ids
43 | return
44 | generate_pdf(pdf_filename, user_id, status_ids)
45 |
46 | if not is_pdf_file_exists(pdf_filename):
47 | print '----%s generate pdf for user:%s fail' % (datetime.datetime.now(), user_id)
48 | else:
49 | commands.getoutput("cd %s && tar -zcf %s %s && rm %s" %(config.PDF_FILE_DOWNLOAD_DIR,
50 | pdf_filename_compressed, pdf_filename, pdf_filename))
51 | print '----%s generate pdf for user:%s succ' % (datetime.datetime.now(), user_id)
52 | except Exception, e:
53 | import traceback
54 | print '%s %s' % (datetime.datetime.now(), traceback.format_exc())
55 |
56 | def generate_pdf_by_user(user_id):
57 | user = User.get(user_id)
58 | if not user:
59 | return
60 |
61 | #XXX:暂时只生成2012年的(uid从98开始的用户)
62 | #XXX:暂时只生成2012年3月份的(uid从166开始的用户)
63 | start_date = Status.get_oldest_create_time(None, user_id)
64 | if not start_date:
65 | return
66 | now = datetime.datetime.now()
67 | now = datetime.datetime(now.year, now.month, now.day) - datetime.timedelta(days = calendar.monthrange(now.year, now.month)[1])
68 |
69 | d = start_date
70 | while d <= now:
71 | generate(user_id, d)
72 |
73 | days = calendar.monthrange(d.year, d.month)[1]
74 | d += datetime.timedelta(days=days)
75 | d = datetime.datetime(d.year, d.month, 1)
76 |
77 |
78 | if __name__ == "__main__":
79 | for uid in PdfSettings.get_all_user_ids():
80 | #print '------begin generate pdf of user:', uid
81 | #generate_pdf_by_user(uid)
82 |
83 | now = datetime.datetime.now()
84 | last_mongth = datetime.datetime(now.year, now.month, now.day) \
85 | - datetime.timedelta(days = calendar.monthrange(now.year, now.month)[1])
86 | print '----- generate last month pdf of user:', last_mongth, uid
87 | generate(uid, last_mongth)
88 |
--------------------------------------------------------------------------------
/past/static/js/cssrefresh.js:
--------------------------------------------------------------------------------
1 | /*
2 | * CSSrefresh v1.0.1
3 | *
4 | * Copyright (c) 2012 Fred Heusschen
5 | * www.frebsite.nl
6 | *
7 | * Dual licensed under the MIT and GPL licenses.
8 | * http://en.wikipedia.org/wiki/MIT_License
9 | * http://en.wikipedia.org/wiki/GNU_General_Public_License
10 | */
11 |
12 | (function() {
13 |
14 | var phpjs = {
15 |
16 | array_filter: function( arr, func )
17 | {
18 | var retObj = {};
19 | for ( var k in arr )
20 | {
21 | if ( func( arr[ k ] ) )
22 | {
23 | retObj[ k ] = arr[ k ];
24 | }
25 | }
26 | return retObj;
27 | },
28 | filemtime: function( file )
29 | {
30 | var headers = this.get_headers( file, 1 );
31 | return ( headers && headers[ 'Last-Modified' ] && Date.parse( headers[ 'Last-Modified' ] ) / 1000 ) || false;
32 | },
33 | get_headers: function( url, format )
34 | {
35 | var req = window.ActiveXObject ? new ActiveXObject( 'Microsoft.XMLHTTP' ) : new XMLHttpRequest();
36 | if ( !req )
37 | {
38 | throw new Error('XMLHttpRequest not supported.');
39 | }
40 |
41 | var tmp, headers, pair, i, j = 0;
42 |
43 | try
44 | {
45 | req.open( 'HEAD', url, false );
46 | req.send( null );
47 | if ( req.readyState < 3 )
48 | {
49 | return false;
50 | }
51 | tmp = req.getAllResponseHeaders();
52 | tmp = tmp.split( '\n' );
53 | tmp = this.array_filter( tmp, function ( value )
54 | {
55 | return value.toString().substring( 1 ) !== '';
56 | });
57 | headers = format ? {} : [];
58 |
59 | for ( i in tmp )
60 | {
61 | if ( format )
62 | {
63 | pair = tmp[ i ].toString().split( ':' );
64 | headers[ pair.splice( 0, 1 ) ] = pair.join( ':' ).substring( 1 );
65 | }
66 | else
67 | {
68 | headers[ j++ ] = tmp[ i ];
69 | }
70 | }
71 |
72 | return headers;
73 | }
74 | catch ( err )
75 | {
76 | return false;
77 | }
78 | }
79 | };
80 |
81 | var cssRefresh = function() {
82 |
83 | this.reloadFile = function( links )
84 | {
85 | for ( var a = 0, l = links.length; a < l; a++ )
86 | {
87 | var link = links[ a ],
88 | newTime = phpjs.filemtime( this.getRandom( link.href ) );
89 |
90 | // has been checked before
91 | if ( link.last )
92 | {
93 | // has been changed
94 | if ( link.last != newTime )
95 | {
96 | // reload
97 | link.elem.setAttribute( 'href', this.getRandom( this.getHref( link.elem ) ) );
98 | }
99 | }
100 |
101 | // set last time checked
102 | link.last = newTime;
103 | }
104 | setTimeout( function()
105 | {
106 | this.reloadFile( links );
107 | }, 1000 );
108 | };
109 |
110 | this.getHref = function( f )
111 | {
112 | return f.getAttribute( 'href' ).split( '?' )[ 0 ];
113 | };
114 | this.getRandom = function( f )
115 | {
116 | return f + '?x=' + Math.random();
117 | };
118 |
119 |
120 | var files = document.getElementsByTagName( 'link' ),
121 | links = [];
122 |
123 | for ( var a = 0, l = files.length; a < l; a++ )
124 | {
125 | var elem = files[ a ],
126 | rel = elem.rel;
127 | if ( typeof rel != 'string' || rel.length == 0 || rel == 'stylesheet' )
128 | {
129 | links.push({
130 | 'elem' : elem,
131 | 'href' : this.getHref( elem ),
132 | 'last' : false
133 | });
134 | }
135 | }
136 | this.reloadFile( links );
137 | };
138 |
139 |
140 | cssRefresh();
141 |
142 | })();
--------------------------------------------------------------------------------
/past/templates/pdf.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% import "blocks.html" as blocks %}
4 |
5 | {% block rightbar_block %}
6 | {{super()}}
7 | {{blocks.rightbar_intros_block(intros)}}
8 |
9 | {{blocks.rightbar_feedback_block()}}
10 | {% endblock %}
11 |
12 | {% block middlebar_block %}
13 |
14 | {{ blocks.notification_block(g.user) }}
15 |
16 |
17 |
18 |
首页 > PDF下载 [PDF会按月归档,下载链接如下]
19 |
47 | {% for year in files_dict.keys()|sort(reverse=True)%}
48 |
{{year}}年
49 |
50 |
51 | {%for i in [0,1,2,3,4,5]%}
52 | {% if files_dict[year][i]%}
53 | {%set date = files_dict[year][i][0]%}
54 | {%set filename = files_dict[year][i][1]%}
55 | {%set filesize = files_dict[year][i][2]%}
56 | | {{date.month}}月 [{{filesize}}] |
57 | {% else%}
58 | |
59 | {% endif%}
60 | {%endfor%}
61 |
62 |
63 | {%if files_dict[year]|length > 6%}
64 |
65 | {%for i in [6,7,8,9,10,11]%}
66 | {% if files_dict[year][i]%}
67 | {%set date = files_dict[year][i][0]%}
68 | {%set filename = files_dict[year][i][1]%}
69 | {%set filesize = files_dict[year][i][2]%}
70 | | {{date.month}}月 [{{filesize}}] |
71 | {% else%}
72 | |
73 | {% endif%}
74 | {%endfor%}
75 |
76 | {%endif%}
77 |
78 | {%endfor%}
79 |
80 | 说明:PDF每月会生成一个单独的文件,如果你想将多个PDF文件合并为一个,请下载各个PDF文件后,使用PDF编辑软件进行编辑合并
81 |
82 |
83 | {% endblock %}
84 |
--------------------------------------------------------------------------------
/past/templates/v2/pdf.html:
--------------------------------------------------------------------------------
1 | {% extends "v2/base.html" %}
2 | {% import "blocks.html" as blocks_tmpl_helper %}
3 |
4 | {%block title%}PDF-{{user.name}} | 旧时光{%endblock%}
5 |
6 | {%block main%}
7 |
8 |
9 |
10 |
11 |
12 |
首页 > PDF下载 [PDF会按月归档,下载链接如下]
13 |
41 | {% for year in files_dict.keys()|sort(reverse=True)%}
42 |
{{year}}年
43 |
44 |
45 | {%for i in [0,1,2,3,4,5]%}
46 | {% if files_dict[year][i]%}
47 | {%set date = files_dict[year][i][0]%}
48 | {%set filename = files_dict[year][i][1]%}
49 | {%set filesize = files_dict[year][i][2]%}
50 | | {{date.month}}月 [{{filesize}}] |
51 | {% else%}
52 | |
53 | {% endif%}
54 | {%endfor%}
55 |
56 |
57 | {%if files_dict[year]|length > 6%}
58 |
59 | {%for i in [6,7,8,9,10,11]%}
60 | {% if files_dict[year][i]%}
61 | {%set date = files_dict[year][i][0]%}
62 | {%set filename = files_dict[year][i][1]%}
63 | {%set filesize = files_dict[year][i][2]%}
64 | | {{date.month}}月 [{{filesize}}] |
65 | {% else%}
66 | |
67 | {% endif%}
68 | {%endfor%}
69 |
70 | {%endif%}
71 |
72 | {%endfor%}
73 |
74 | 说明:PDF每月会生成一个单独的文件,如果你想将多个PDF文件合并为一个,请下载各个PDF文件后,使用PDF编辑软件进行编辑合并
75 |
76 |
77 |
78 |
79 | {{super()}}
80 | {{blocks_tmpl_helper.rightbar_intros_block(intros)}}
81 | {{blocks_tmpl_helper.rightbar_feedback_block()}}
82 |
83 |
84 | {%endblock%}
85 |
86 |
--------------------------------------------------------------------------------
/past/store.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | import os
4 | import commands
5 | import datetime
6 |
7 | import MySQLdb
8 | import redis
9 | import memcache
10 |
11 | from past.utils.escape import json_decode, json_encode
12 | from past import config
13 |
14 | def init_db():
15 | cmd = """mysql -h%s -P%s -u%s -p%s < %s""" \
16 | % (config.DB_HOST, config.DB_PORT,
17 | config.DB_USER, config.DB_PASSWD,
18 | os.path.join(os.path.dirname(__file__), "schema.sql"))
19 | status, output = commands.getstatusoutput(cmd)
20 |
21 | if status != 0:
22 | print "init_db fail, output is: %s" % output
23 |
24 | return status
25 |
26 | def connect_db():
27 | try:
28 | conn = MySQLdb.connect(
29 | host=config.DB_HOST,
30 | port=config.DB_PORT,
31 | user=config.DB_USER,
32 | passwd=config.DB_PASSWD,
33 | db=config.DB_NAME,
34 | use_unicode=True,
35 | charset="utf8")
36 | return conn
37 | except Exception, e:
38 | print "connect db fail:%s" % e
39 | return None
40 |
41 | def connect_redis():
42 | return redis.Redis(config.REDIS_HOST, config.REDIS_PORT)
43 |
44 | def connect_redis_cache():
45 | return redis.Redis(config.REDIS_CACHE_HOST, config.REDIS_CACHE_PORT)
46 |
47 | def connect_mongo(dbname="thepast"):
48 | import pymongo
49 | conn = pymongo.connection.Connection('localhost')
50 | db = conn.thepast
51 | db = getattr(conn, dbname)
52 | return db and getattr(db, dbname)
53 |
54 | class MongoDB(object):
55 | def __init__(self, dbname="thepast"):
56 | self.dbname = dbname
57 | self._conn = connect_mongo(self.dbname)
58 |
59 | def connect(self):
60 | self._conn = connect_mongo(self.dbname)
61 | return self._conn
62 |
63 | def get(self, k):
64 | d = {"k":k}
65 | r = self._conn.find_one(d)
66 | if r:
67 | return r.get("v")
68 | return None
69 |
70 | def mget(self, keys):
71 | d = {"k": {"$in" : keys}}
72 | rs = self._conn.find(d)
73 | return [r["v"] for r in rs]
74 |
75 | def set(self, k, v):
76 | self._conn.update({"k":k},{"k":k, "v":v}, upsert=True)
77 |
78 | def remove(self, k):
79 | self._conn.remove({"k":k})
80 |
81 | def get_connection(self):
82 | return self._conn or self.connect()
83 |
84 | class DB(object):
85 |
86 | def __init__(self):
87 | self._conn = connect_db()
88 |
89 | def connect(self):
90 | self._conn = connect_db()
91 | return self._conn
92 |
93 | def execute(self, *a, **kw):
94 | cursor = kw.pop('cursor', None)
95 | try:
96 | cursor = cursor or self._conn.cursor()
97 | cursor.execute(*a, **kw)
98 | except (AttributeError, MySQLdb.OperationalError):
99 | print 'debug, %s re-connect to mysql' % datetime.datetime.now()
100 | self._conn and self._conn.close()
101 | self.connect()
102 | cursor = self._conn.cursor()
103 | cursor.execute(*a, **kw)
104 | return cursor
105 |
106 | def commit(self):
107 | return self._conn and self._conn.commit()
108 |
109 | def rollback(self):
110 | return self._conn and self._conn.rollback()
111 |
112 | def connect_memcached():
113 | mc = memcache.Client(['%s:%s' % (config.MEMCACHED_HOST, config.MEMCACHED_PORT)], debug=0)
114 | return mc
115 |
116 | db_conn = DB()
117 | mc = redis_cache_conn = connect_memcached()
118 | #redis_conn = connect_redis()
119 | #mongo_conn = MongoDB()
120 |
--------------------------------------------------------------------------------
/past/api/oauth2.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import urllib
4 | from past import config
5 | from past.model.user import UserAlias
6 | from past.utils.escape import json_decode
7 | from past.utils import httplib2_request
8 |
9 | from .error import OAuthLoginError
10 |
11 | class OAuth2(object):
12 |
13 | authorize_uri = ''
14 | access_token_uri = ''
15 | api_host = ''
16 |
17 | def __init__(self, provider=None, apikey=None, apikey_secret=None, redirect_uri=None,
18 | scope=None, state=None, display=None,
19 | alias=None, access_token=None, refresh_token=None):
20 |
21 | self.provider = provider
22 | self.apikey = apikey
23 | self.apikey_secret = apikey_secret
24 | self.redirect_uri = redirect_uri
25 |
26 | self.scope = scope
27 | self.state = state
28 | self.display = display
29 |
30 | self.alias = alias
31 | if alias:
32 | self.user_alias = UserAlias.get(
33 | config.OPENID_TYPE_DICT[provider], alias)
34 | else:
35 | self.user_alias = None
36 | self.access_token = access_token
37 | self.refresh_token = refresh_token
38 |
39 | def __repr__(self):
40 | return '' % (self.provider, self.alias, self.access_token,
42 | self.refresh_token, self.api_host)
43 | __str__ = __repr__
44 |
45 | def login(self):
46 | qs = {
47 | 'client_id' : self.apikey,
48 | 'response_type' : 'code',
49 | 'redirect_uri' : self.redirect_uri,
50 | }
51 | if self.display:
52 | qs['display'] = self.display
53 | if self.scope:
54 | qs['scope'] = self.scope
55 | if self.state:
56 | qs['state'] = self.state
57 |
58 | qs = urllib.urlencode(qs)
59 | uri = '%s?%s' %(self.authorize_uri, qs)
60 |
61 | return uri
62 |
63 | def get_access_token(self, authorization_code):
64 | qs = {
65 | "client_id": self.apikey,
66 | "client_secret": self.apikey_secret,
67 | "redirect_uri": self.redirect_uri,
68 | "grant_type": "authorization_code",
69 | "code": authorization_code,
70 | }
71 | qs = urllib.urlencode(qs)
72 | resp, content = httplib2_request(self.access_token_uri, "POST", body=qs)
73 | excp = OAuthLoginError(msg='get_access_token, status=%s,reason=%s,content=%s' \
74 | %(resp.status, resp.reason, content))
75 | if resp.status != 200:
76 | raise excp
77 |
78 | jdata = json_decode(content) if content else None
79 | return jdata
80 |
81 |
82 | def refresh_tokens(self):
83 | qs = {
84 | "grant_type": "refresh_token",
85 | "refresh_token": self.refresh_token,
86 | "client_id": self.apikey,
87 | "client_secret": self.apikey_secret,
88 | "redirect_uri": self.redirect_uri,
89 | }
90 | resp, content = httplib2_request(self.access_token_uri, "POST",
91 | body=urllib.urlencode(qs))
92 | excp = OAuthLoginError(self.user_alias.user_id, self.provider,
93 | 'refresh_tokens, status=%s,reason=%s,content=%s' \
94 | %(resp.status, resp.reason, content))
95 | if resp.status != 200:
96 | raise excp
97 |
98 | jdata = json_decode(content) if content else None
99 | return jdata
100 |
101 | def set_token(self, access_token, refresh_token):
102 | self.access_token = access_token
103 | self.refresh_token = refresh_token
104 |
105 | def get_user_info(self, uid):
106 | raise NotImplementedError
107 |
108 |
--------------------------------------------------------------------------------
/past/static/calendar/css/bootstrap.calendar.css:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * bootstrap-calendar plugin
4 | * Original author: @ahmontero
5 | * Licensed under the MIT license
6 | *
7 | */
8 |
9 | .calendar body {
10 | background-color: #FFFFFF;
11 | position: relative;
12 | }
13 |
14 | .year {
15 | color: rgba(0, 0, 0, 0.3);
16 | font-size: 15px;
17 | margin: 0px 0px 0px 0px;
18 | }
19 |
20 | .month {
21 | color: rgba(0, 0, 0, 0.3);
22 | font-size: 15px;
23 | margin: -35px 0px 0px 120px;
24 | }
25 |
26 | .calendar {
27 | table-layout:fixed;
28 | }
29 |
30 | .calendar TH {
31 | width: 30px;
32 | height: 30px;
33 |
34 | font-size: 10px;
35 | font-weight: bold;
36 | color: rgba(0, 0, 0, 0.3);
37 | text-align: center;
38 |
39 | -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s;
40 | -moz-transition: background 0.2s linear 0s;
41 |
42 | border-color: rgba(0, 0, 0, 0.1);
43 | border: 1px solid rgba(0, 0, 0, 0.1);
44 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
45 | }
46 |
47 | .calendar TD, TD.day{
48 | height: 30px;
49 | width: 30px;
50 |
51 | font-size: 10px;
52 | font-weight: bold;
53 | color: rgba(0, 0, 0, 0.3);
54 | text-align: center;
55 |
56 | -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s;
57 | -moz-transition: background 0.2s linear 0s;
58 |
59 | border: 1px solid rgba(0, 0, 0, 0.1);
60 | border-color: rgba(0, 0, 0, 0.1);
61 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
62 | }
63 |
64 | .calendar TD.day:hover{
65 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.025) inset, 0 0 10px rgba(0, 0, 0, 0.1);
66 | background: rgba(0, 0, 0, 0.1);
67 | outline: 0 none;
68 | cursor: pointer;
69 | color: #FFFFFF;
70 | }
71 |
72 | .calendar TD.weekend {
73 | color: rgba(0, 0, 0, 0.1);
74 | }
75 | .calendar TD.weekend:hover{
76 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 10px rgba(82, 168, 236, 0.6);
77 | background: rgba(0, 0, 0, 0.55);
78 | outline: 0 none;
79 | cursor: pointer;
80 | color: #FFFFFF;
81 | }
82 |
83 | .calendar TD.today {
84 | border: 2px solid red;
85 | color: rgba(0, 0, 0, 0.60);
86 | }
87 |
88 | .calendar TD.today:hover{
89 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 10px rgba(82, 168, 236, 0.6);
90 | background: rgba(0, 0, 0, 0.55);
91 | outline: 0 none;
92 | cursor: pointer;
93 | color: #FFFFFF;
94 | }
95 |
96 | .calendar TD.holiday {
97 | color: red;
98 | }
99 |
100 | .calendar TD.holiday:hover {
101 | cursor: pointer;
102 | }
103 |
104 | .calendar TD SPAN.weekday{
105 | background-color: rgba(0, 0, 0, 0.1);
106 | border-radius: 14%;
107 | color: #FFFFFF;
108 | font-size: 12px;
109 | font-weight: bold;
110 | padding: 1px 2px 1px;
111 | white-space: nowrap;
112 | }
113 |
114 | .calendar TD SPAN.weekday:hover{
115 | background-color: rgba(0, 0, 0, 0.25);
116 | }
117 |
118 | .calendar TFOOT, .calendar TFOOT TR TH.sel {
119 | height: 12px;
120 | width: 12px;
121 |
122 | font-size: 10px;
123 | font-weight: bold;
124 | color: rgba(0, 0, 0, 0.3);
125 | text-align: center;
126 |
127 | -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s;
128 | -moz-transition: background 0.2s linear 0s;
129 |
130 | border: 1px solid rgba(0, 0, 0, 0.1);
131 | border-color: rgba(0, 0, 0, 0.1);
132 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
133 |
134 | cursor:pointer;
135 | }
136 |
137 | .calendar TFOOT TR TH.sel:hover {
138 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.025) inset, 0 0 10px rgba(0, 0, 0, 0.1);
139 | background: rgba(0, 0, 0, 0.1);
140 | outline: 0 none;
141 | cursor: pointer;
142 | color: #FFFFFF;
143 | }
144 |
145 | .calendar .arrow{
146 | padding:2px 0px 0px 0px;
147 | }
148 |
149 |
--------------------------------------------------------------------------------
/tools/import_status_to_wordpress.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | import sys
4 | sys.path.append('../')
5 |
6 | from datetime import timedelta
7 | import MySQLdb
8 |
9 | import past
10 | from past import config
11 | from past.model.status import Status
12 |
13 | def connect_db():
14 | try:
15 | conn = MySQLdb.connect(
16 | host=config.DB_HOST,
17 | port=config.DB_PORT,
18 | user=config.DB_USER,
19 | passwd=config.DB_PASSWD,
20 | db="wp_linjuly",
21 | use_unicode=True,
22 | charset="utf8")
23 | return conn
24 | except Exception, e:
25 | print "connect db fail:%s" % e
26 | return None
27 | db_conn = connect_db()
28 |
29 | user_id = 34
30 | limit = 250
31 |
32 | status_ids = Status.get_ids(user_id, limit=limit, order="create_time desc")
33 |
34 | for s in Status.gets(status_ids):
35 | try:
36 | _t = ''.join( [x for x in s.text] )
37 |
38 | retweeted_data = s.get_retweeted_data()
39 | if retweeted_data:
40 | if isinstance(retweeted_data, basestring):
41 | _t += retweeted_data
42 | else:
43 | _t += retweeted_data.get_content()
44 | print '---sid:', s.id
45 | post_author = 1
46 | post_date = s.create_time
47 | post_date_gmt = s.create_time - timedelta(hours=8)
48 | post_content = _t
49 | post_title = u"%s" %post_content[:10]
50 | post_modified = post_date
51 | post_modified_gmt = post_date_gmt
52 | post_type = "post"
53 |
54 | post_excerpt = ""
55 | to_ping = ""
56 | pinged = ""
57 | post_content_filtered = ""
58 |
59 | cursor = None
60 | try:
61 | cursor = db_conn.cursor()
62 | cursor.execute('''insert into wp_posts (post_author, post_date, post_date_gmt, post_content,
63 | post_excerpt, to_ping, pinged, post_content_filtered,
64 | post_title, post_modified, post_modified_gmt, post_type)
65 | values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)''',
66 | (post_author, post_date, post_date_gmt, post_content,
67 | post_excerpt, to_ping, pinged, post_content_filtered,
68 | post_title, post_modified, post_date_gmt, post_type))
69 | post_id = cursor.lastrowid
70 | cursor.execute('''update wp_posts set guid = %s''',
71 | "http://www.linjuly.com/?p=%s" %post_id)
72 | cursor.execute('''insert into wp_term_relationships values(%s,3,0)''', post_id)
73 | db_conn.commit()
74 | except Exception, e:
75 | import traceback; print traceback.format_exc()
76 | db_conn.rollback()
77 | finally:
78 | cursor and cursor.close()
79 | except Exception, e:
80 | import traceback; print traceback.format_exc()
81 |
82 |
83 | #*************************** 1. row ***************************
84 | # ID: 8
85 | # post_author: 1
86 | # post_date: 2012-01-01 22:29:57
87 | # post_date_gmt: 2012-01-01 14:29:57
88 | # post_content: 2011,其实是蛮惨的一年。。。
89 | # post_title: 我的2011
90 | # post_excerpt:
91 | # post_status: publish
92 | # comment_status: open
93 | # ping_status: open
94 | # post_password:
95 | # post_name: %e6%88%91%e7%9a%842011
96 | # to_ping:
97 | # pinged:
98 | # post_modified: 2012-03-29 23:31:37
99 | # post_modified_gmt: 2012-03-29 15:31:37
100 | #post_content_filtered:
101 | # post_parent: 0
102 | # guid: http://www.linjuly.com/?p=8
103 | # menu_order: 0
104 | # post_type: post
105 | # post_mime_type:
106 | # comment_count: 0
107 | #1 row in set (0.00 sec)
108 | #
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 你好 旧时光 | 之个人杂志计划
2 | =============
3 |
4 | 当今的互联网,在高速发展,大家都在努力向前看!
5 |
6 | thepast,希望能帮助我们,时不时的停住脚步,去看看过往的一些事情…
7 |
8 |
9 | **目前已经完成的一些东西**:
10 |
11 | 1. 实时聚合个人在“豆瓣”、“人人”、“新浪微博”、“腾讯微博”、“Twitter”、“wordpress”、“instagram” 等平台的 Timeline(包括历史消息)。
12 | 1. 每天清晨,会邮件提醒你过往的今天都发生了些什么,或许是惊喜,亦或是怀念。
13 | 1. 聚合后的Timeline,生成PDF[预览]版本,供离线阅读或永久保存,打造你自己的个人杂志。
14 | 1. 同步更新微博到多个平台(有些偏离thepast的主旨,是提供给一小部分用户使用的)。
15 | 1. 提供日记功能,“今天写日记,明年再来看”。
16 |
17 | **计划中的一些事情**:
18 |
19 | 1. 导入更多的第三方(evernote、google+、facebook)
20 | 1. 提供多种形式的导出形式(不仅仅是PDF形式,可能会支持导出到google drive,dropbox等)
21 | 1. 个人挑选的历史信息,转为纸质杂志,作为留念,收藏。
22 | 1. 设置个性域名,提供个人名片服务(在个人数据挖掘的基础上提供全面的个人名片)。
23 | 1. Android、iOS移动客户端,提供更好的历史消息回顾体验。
24 |
25 |
26 | 还有一些更多可以做的东西:
27 | -------
28 | * 对聚合后的消息,提供搜索功能(个人信息的社会化搜索)
29 | * 根据聚合后的timeline,生成更权威的“个人关键字tag云”
30 | * 更好的排版格式,提升在移动设备上(手机,pad)的离线阅读体验,或者发送到kindle上也未尝不可。
31 | * 试想想,这样出一本记录自己,讲述自己的纸质杂志应该还是很令人期待的。
32 |
33 | **目前的运行状况**
34 |
35 | - 注册用户2300+
36 | - 用户数据500万条
37 | - 绑定的sns帐号,4000+
38 |
39 |
40 | 技术细节:
41 | -------
42 |
43 | * [linux(debian6)](http://debian.org) -- `stable and powerfull`
44 | * nginx/uwsgi -- `web server and serve static file`
45 | * mysql
46 | * python
47 | * [flask](http://flask.pocoo.org) -- `python web framework`
48 | * [redis](http://redis.io) -- `nosqldb, store text,img etc, and used for cache instead of memcached`
49 | * memcached -- `之前使用redis代替memcached,不过redis在小内存情况下表现较差,所以选择使用memcached`
50 | * mongodb -- `data storage`
51 | * [xhtml2pdf](https://github.com/chrisglass/xhtml2pdf) -- `convert html to pdf`
52 | * [scws](http://www.ftphp.com/scws) -- `simple chinese word segment`
53 | * git/github -- `code version control`
54 | * v2ex -- `thanks for v2ex and css of v2ex^^`
55 |
56 | 项目地址:
57 | -------
58 |
59 | https://github.com/laiwei/thepast
60 |
61 | 官方主页:
62 | -------
63 |
64 | http://thepast.me
65 |
66 |
67 | 作为开源项目,欢迎前端工程师 和 iOS工程师,一起来完善,也欢迎吐槽。
68 |
69 | 贡献者列表:
70 | -------
71 | * [laiwei](https://github.com/laiwei) --`项目发起者`
72 | * [lmm214](https://github.com/lmm214) --`设计,修改了首页timeline的展示方式`
73 |
74 |
75 | ChangeList:
76 | -------
77 | * `2012-10-03`: 支持绑定instagram了:)
78 | * `2012-10-02`: 支持绑定人人了:)
79 | * `2012-09-11`: 数据存储从mongodb转向了mysql,节省硬盘空间,[参考](http://laiwei.net/2012/09/15/mongodb和mysql的一个小小的取舍比较)
80 | * `2012-09-09`: “时间线”重构,使用flask的blueprint,松耦合;时间线支持reverse order
81 | * `2012-07-24`: 支持从thepast发送消息到多个第三方了
82 | * `2012-06-09`: 日记支持markdown格式,这样写日记方便好多了:)
83 | * `2012-04-22`: 增加了隐私设置,可以选择公开、仅登录用户可见、仅自己可见,保护用户的隐私; 增加了邮件退订功能。
84 | * `2012-04-18`: PDF归档,修改为按月归档,每个月生成一个独立的文件,代替了之前整理为一个PDF文件。
85 | * `2012-04-14`: 增加了[“时间线”](http://thepast.me/visual)栏目,以一种别样的视角看过去。
86 | * `2012-04-10`: 支持绑定自己的独立wordpress博客:)
87 | * `2012-04-05`: 增加了"我的过去"栏目,提供有意思的回忆功能
88 | * `2012-04-04`: 提供补充email功能,以便在PDF文件生成之后,通知用户或者直接发送附件
89 | * `2012-04-01`: redis在内存比较小的情况下,效率比较低,而且在分配的内存耗尽,没有及时淘汰掉key时,会造成写入失败,于是改用了memcached
90 | * `2012-04-01`: mongodb坏掉了,原因是在32位系统下,mongodb存在数据文件不能超过2G的限制,见[官方说明](http://blog.mongodb.org/post/137788967/32-bit-limitations); 于是将系统升级为64位debian,重新安装了64位版本mongodb,恢复了数据
91 | * `2012-03-31`: 加上了sidebar,用来展示用户的自我介绍,个人关键字等
92 | * `2012-03-30`: 恢复了早期新浪微博用户的status时间差了12小时的数据
93 | * `2012-03-25`: 增加了个人关键字提取功能,根据timeline的信息提取个人关键字,使用了[scws](http://www.ftphp.com/scws/),thanks
94 | * `2012-03-10`: 新的匿名用户首页和timeline页面,from木木[lmm214]
95 | * `2012-03-04`: 使用mongodb代替redis做数据持久化存储,并将redis中的37万条数据转存到mongodb中
96 | * `2012-03-04`: 使用豆瓣新广播的api,代替旧的miniblog API
97 | * `2012-03-01`: mysql connect增加了mysql gone away之后的重试机制
98 | * `2012-02-28`: 使用了新的logo,感谢木木[lmm214](https://github.com/lmm214)的设计
99 | * `2012-02-24`: 支持同步腾讯微博(使用腾讯微博的朋友看过来^^)
100 | * `2012-02-22`: 屏蔽搜索引擎收录(因为隐私还是很重要的)
101 | * `2012-02-18`: 加cache,使用redis充当memcache,提高访问速度,降低机器负载
102 | * `2012-02-17`: 优化PDF文件的下载效率,使用nginx来承担文件下载任务
103 | * `2012-02-16`: 优化代码解决生成PDF的效率(因为内存不够用了^^)
104 | * `2012-02-15`: 增加了个人杂志计划成员展示页
105 | * `2012-02-14`: 在v2ex社区介绍个人杂志计划,共有40人加入!
106 | * `2012-02-13`: 增加保存个人内容为排版后的PDF功能
107 | * `2012-02-12`: 开源项目,个人杂志计划上线
108 |
109 | thanks
110 |
111 | by laiwei
112 |
--------------------------------------------------------------------------------
/past/templates/bind_wordpress.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% import "blocks.html" as blocks %}
4 |
5 | {% block rightbar_block %}
6 | {{super()}}
7 | {{blocks.rightbar_intros_block(intros)}}
8 | {{blocks.rightbar_feedback_block()}}
9 | {%endblock%}
10 |
11 | {% block middlebar_block %}
12 |
13 | {{ blocks.notification_block() }}
14 |
15 |
16 | {%if wordpress_alias_list%}
17 |
18 |
已绑定的rss列表:
19 |
20 |
21 | {%for x in wordpress_alias_list%}
22 | - {{x.alias}}
23 | {%endfor%}
24 |
25 |
26 |
27 |
28 | {%endif%}
29 |
30 | {% if step == '1'%}
31 |
47 | {%else%}
48 |
49 |
绑定rss地址 > 等待验证...
50 |
51 |
你的blog feed地址为:{{feed_uri}}
52 |
你的blog认领验证码为:{{random_id}}
53 |
为了验证blog的主人^^,请发一篇blog,"文章内容"为 {{random_id}},完成该步骤后,请点下一步完成绑定
54 |
55 |
56 |
57 | {%endif%}
58 |
59 |
60 |
61 |
62 |
0. 如果你在绑定过程中遇到任何问题,请直接给管理员捎个话 help@thepast.me,我想没有解决不了的问题^^
63 |
1. 理论来说,支持导入所有的rss feed,而不仅仅是wordpress,注意尽量不要用feedburner的rss地址
64 |
2. 对于wordpress rss feed来讲,只能导入最近的15篇文章,如果需要导入更多,需要去wordpress的控制面板中设置rss输出的文章数目。
65 |
66 |
67 |
68 |
98 |
99 | {% endblock %}
100 |
--------------------------------------------------------------------------------
/past/templates/note_create.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% import "blocks.html" as blocks %}
4 |
5 | {% block css %}
6 | {{super()}}
7 |
8 | {% endblock %}
9 |
10 | {% block rightbar_block %}
11 | {{super()}}
12 | {{blocks.rightbar_note_block()}}
13 | {{blocks.rightbar_markdown_block()}}
14 | {{blocks.rightbar_feedback_block()}}
15 | {% endblock %}
16 |
17 | {% block middlebar_block %}
18 |
21 |
22 |
23 | {{ blocks.notification_block() }}
24 |
25 |
26 |
27 |
77 |
78 |
93 | {% endblock %}
94 |
--------------------------------------------------------------------------------
/past/utils/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 | import os
3 | import re
4 | import time
5 | import datetime
6 | import imghdr
7 | import httplib2
8 | import mimetypes
9 | import random
10 | import string
11 | import markdown2
12 | from past import config
13 |
14 | def randbytes(bytes_):
15 | return ''.join(random.sample(string.ascii_letters + string.digits, bytes_))
16 |
17 |
18 | def random_string (length):
19 | return ''.join(random.choice(string.letters) for ii in range (length + 1))
20 |
21 | def encode_multipart_data (data, files):
22 | boundary = random_string (30)
23 |
24 | def get_content_type (filename):
25 | return mimetypes.guess_type (filename)[0] or 'application/octet-stream'
26 |
27 | def encode_field(field_name):
28 | return ('--' + boundary,
29 | 'Content-Disposition: form-data; name="%s"' % field_name,
30 | '', str (data [field_name]))
31 |
32 | def encode_file(field_name):
33 | filename = files[field_name]
34 | return ('--' + boundary,
35 | 'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, filename),
36 | 'Content-Type: %s' % get_content_type(filename),
37 | '', open (filename, 'rb').read ())
38 |
39 | lines = []
40 | for name in data:
41 | lines.extend (encode_field (name))
42 | for name in files:
43 | lines.extend (encode_file (name))
44 | lines.extend (('--%s--' % boundary, ''))
45 | body = '\r\n'.join (lines)
46 |
47 | headers = {'content-type': 'multipart/form-data; boundary=' + boundary,
48 | 'content-length': str (len (body))}
49 |
50 | return body, headers
51 |
52 | def httplib2_request(uri, method="GET", body='', headers=None,
53 | redirections=httplib2.DEFAULT_MAX_REDIRECTS,
54 | connection_type=None, disable_ssl_certificate_validation=True):
55 |
56 | DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
57 |
58 | if not isinstance(headers, dict):
59 | headers = {}
60 |
61 | if method == "POST":
62 | headers['Content-Type'] = headers.get('Content-Type',
63 | DEFAULT_POST_CONTENT_TYPE)
64 |
65 | return httplib2.Http(disable_ssl_certificate_validation=disable_ssl_certificate_validation).\
66 | request(uri, method=method, body=body,
67 | headers=headers, redirections=redirections,
68 | connection_type=connection_type)
69 |
70 | def wrap_long_line(text, max_len=60):
71 | if len(text) <= max_len:
72 | return text
73 | out = ""
74 | parts = text.split("\n")
75 | parts_out = []
76 | for x in parts:
77 | parts_out.append( _wrap_long_line(x, max_len) )
78 | return "\n".join(parts_out)
79 |
80 | def _wrap_long_line(text, max_len):
81 | out_text = ""
82 | times = len(text)*1.0 / max_len
83 | if times > int(times):
84 | times = int(times) + 1
85 | else:
86 | times = int(times)
87 |
88 | i = 0
89 | index = 0
90 | while i < times:
91 | s = text[index:index+max_len]
92 | out_text += s
93 | if not ('<' in s or '>' in s):
94 | out_text += "\n"
95 | index += max_len
96 | i += 1
97 |
98 | return out_text
99 |
100 | def datetime2timestamp(datetime_):
101 | if not isinstance(datetime_, datetime.datetime):
102 | return 0
103 |
104 | return datetime_ and int(time.mktime(datetime_.timetuple()))
105 |
106 | EMAILRE = re.compile(r'^[_\.0-9a-zA-Z+-]+@([0-9a-zA-Z]+[0-9a-zA-Z-]*\.)+[a-zA-Z]{2,4}$')
107 | def is_valid_email(email):
108 | if len(email) >= 6:
109 | return EMAILRE.match(email) != None
110 | return False
111 |
112 | def is_valid_image(content):
113 | return content and imghdr.what(content) in \
114 | [ 'rgb' ,'gif' ,'pbm' ,'pgm' ,
115 | 'ppm' ,'tiff' ,'rast' ,'xbm' ,'jpeg' ,'bmp' ,'png']
116 |
117 | def sizeof_fmt(num):
118 | for x in ['bytes','KB','MB','GB','TB']:
119 | if num < 1024.0:
120 | return "%3.1f%s" % (num, x)
121 | num /= 1024.0
122 |
123 | def markdownize(content):
124 | return markdown2.markdown(content, extras=["wiki-tables", "code-friendly"])
125 |
126 |
--------------------------------------------------------------------------------
/deploy/mysql.conf:
--------------------------------------------------------------------------------
1 | #
2 | # The MySQL database server configuration file.
3 | #
4 | # You can copy this to one of:
5 | # - "/etc/mysql/my.cnf" to set global options,
6 | # - "~/.my.cnf" to set user-specific options.
7 | #
8 | # One can use all long options that the program supports.
9 | # Run program with --help to get a list of available options and with
10 | # --print-defaults to see which it would actually understand and use.
11 | #
12 | # For explanations see
13 | # http://dev.mysql.com/doc/mysql/en/server-system-variables.html
14 |
15 | # This will be passed to all mysql clients
16 | # It has been reported that passwords should be enclosed with ticks/quotes
17 | # escpecially if they contain "#" chars...
18 | # Remember to edit /etc/mysql/debian.cnf when changing the socket location.
19 | [client]
20 | port = 3306
21 | socket = /var/run/mysqld/mysqld.sock
22 |
23 | ## laiwei add
24 | default-character-set = utf8
25 |
26 | # Here is entries for some specific programs
27 | # The following values assume you have at least 32M ram
28 |
29 | # This was formally known as [safe_mysqld]. Both versions are currently parsed.
30 | [mysqld_safe]
31 | socket = /var/run/mysqld/mysqld.sock
32 | nice = 0
33 |
34 | [mysqld]
35 | #
36 | # * Basic Settings
37 | #
38 | user = mysql
39 | pid-file = /var/run/mysqld/mysqld.pid
40 | socket = /var/run/mysqld/mysqld.sock
41 | port = 3306
42 | basedir = /usr
43 | datadir = /var/lib/mysql
44 | tmpdir = /tmp
45 | language = /usr/share/mysql/english
46 | skip-external-locking
47 |
48 | ## laiwei add
49 | default-character-set = utf8
50 | #
51 | # Instead of skip-networking the default is now to listen only on
52 | # localhost which is more compatible and is not less secure.
53 | bind-address = 127.0.0.1
54 | #
55 | # * Fine Tuning
56 | #
57 | key_buffer = 16M
58 | max_allowed_packet = 16M
59 | thread_stack = 192K
60 | thread_cache_size = 8
61 | # This replaces the startup script and checks MyISAM tables if needed
62 | # the first time they are touched
63 | myisam-recover = BACKUP
64 | #max_connections = 100
65 | #table_cache = 64
66 | #thread_concurrency = 10
67 | #
68 | # * Query Cache Configuration
69 | #
70 | query_cache_limit = 1M
71 | query_cache_size = 16M
72 | #
73 | # * Logging and Replication
74 | #
75 | # Both location gets rotated by the cronjob.
76 | # Be aware that this log type is a performance killer.
77 | # As of 5.1 you can enable the log at runtime!
78 | #general_log_file = /var/log/mysql/mysql.log
79 | #general_log = 1
80 | #
81 | # Error logging goes to syslog due to /etc/mysql/conf.d/mysqld_safe_syslog.cnf.
82 | #
83 | # Here you can see queries with especially long duration
84 | #log_slow_queries = /var/log/mysql/mysql-slow.log
85 | #long_query_time = 2
86 | #log-queries-not-using-indexes
87 | #
88 | # The following can be used as easy to replay backup logs or for replication.
89 | # note: if you are setting up a replication slave, see README.Debian about
90 | # other settings you may need to change.
91 | #server-id = 1
92 | #log_bin = /var/log/mysql/mysql-bin.log
93 | expire_logs_days = 10
94 | max_binlog_size = 100M
95 | #binlog_do_db = include_database_name
96 | #binlog_ignore_db = include_database_name
97 | #
98 | # * InnoDB
99 | #
100 | # InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
101 | # Read the manual for more InnoDB related options. There are many!
102 | #
103 | # * Security Features
104 | #
105 | # Read the manual, too, if you want chroot!
106 | # chroot = /var/lib/mysql/
107 | #
108 | # For generating SSL certificates I recommend the OpenSSL GUI "tinyca".
109 | #
110 | # ssl-ca=/etc/mysql/cacert.pem
111 | # ssl-cert=/etc/mysql/server-cert.pem
112 | # ssl-key=/etc/mysql/server-key.pem
113 |
114 | ##laiwei add
115 | transaction-isolation = READ-COMMITTED
116 |
117 | [mysqldump]
118 | quick
119 | quote-names
120 | max_allowed_packet = 16M
121 |
122 | [mysql]
123 | #no-auto-rehash # faster start of mysql but no tab completition
124 |
125 | [isamchk]
126 | key_buffer = 16M
127 |
128 | #
129 | # * IMPORTANT: Additional settings that can override those from this file!
130 | # The files must end with '.cnf', otherwise they'll be ignored.
131 | #
132 | !includedir /etc/mysql/conf.d/
133 |
--------------------------------------------------------------------------------
/past/templates/v2/bind_wordpress.html:
--------------------------------------------------------------------------------
1 | {% extends "v2/base.html" %}
2 | {% import "blocks.html" as blocks_tmpl_helper %}
3 |
4 | {%block title%}绑定博客 | 旧时光{%endblock%}
5 |
6 | {%block main%}
7 |
8 |
9 |
10 | {%if wordpress_alias_list%}
11 |
12 |
已绑定的rss列表:
13 |
14 |
15 | {%for x in wordpress_alias_list%}
16 | - {{x.alias}}
17 | {%endfor%}
18 |
19 |
20 |
21 |
22 | {%endif%}
23 |
24 | {% if step == '1'%}
25 |
41 | {%else%}
42 |
43 |
绑定rss地址 > 等待验证...
44 |
45 |
你的blog feed地址为:{{feed_uri}}
46 |
你的blog认领验证码为:{{random_id}}
47 |
为了验证blog的主人^^,请发一篇blog,"文章内容"为 {{random_id}},完成该步骤后,请点下一步完成绑定
48 |
49 |
50 |
51 | {%endif%}
52 |
53 |
54 |
55 |
56 |
0. 如果你在绑定过程中遇到任何问题,请直接给管理员捎个话 help@thepast.me,我想没有解决不了的问题^^
57 |
1. 理论来说,支持导入所有的rss feed,而不仅仅是wordpress,注意尽量不要用feedburner的rss地址
58 |
2. 对于wordpress rss feed来讲,只能导入最近的15篇文章,如果需要导入更多,需要去wordpress的控制面板中设置rss输出的文章数目。
59 |
60 |
61 |
62 |
92 |
93 |
94 |
95 | {{super()}}
96 | {{blocks_tmpl_helper.rightbar_intros_block(intros)}}
97 | {{blocks_tmpl_helper.rightbar_feedback_block()}}
98 |
99 |
100 | {%endblock%}
101 |
--------------------------------------------------------------------------------
/past/templates/timeline.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% import "status.html" as status_tmpl_helper %}
4 | {% import "blocks.html" as blocks %}
5 |
6 | {% block css %}
7 | {{super()}}
8 |
9 |
10 | {% endblock %}
11 |
12 | {% block js%}
13 | {{super()}}
14 |
15 | {% endblock %}
16 |
17 | {% block title_block %} {{user.name}}{%endblock%}
18 |
19 | {%block content_block%}
20 |
21 | {{self.middlebar_block()}}
22 |
23 | {{blocks.rightbar_intros_block(intros)}}
24 | {{blocks.rightbar_note_block()}}
25 | {{blocks.rightbar_feedback_block()}}
26 |
27 |
28 |
29 | {%endblock%}
30 |
31 | {% block middlebar_block %}
32 |
33 |
34 |
35 |
36 | {%if g.cate == g.config.CATE_THEPAST_NOTE and 0%}
37 | -
38 |
40 |
41 |
42 | - Status
43 |
44 |
45 |
46 |
59 |
60 |
61 |
62 | {%set left = False%}
63 | {%else%}
64 | {%if not status_list and g.start == 0%}
65 |
66 |
你还没在thepast上写过日记呢。
67 |
今天记录一下 明年再来看。
68 |
>写日记
69 |
70 | {%endif%}
71 | {%set left = True%}
72 | {%endif%}
73 |
74 | {%for repeated_status in status_list%}
75 | {%if left%}
76 | -
77 | {%else%}
78 |
-
79 | {%endif%}
80 |
81 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}}
82 | {%set left = not left%}
83 |
84 | {%endfor%}
85 |
86 |
87 |
88 |
89 |
99 |
100 |
101 |
102 |
107 |
108 |
113 |
114 | {% endblock %}
115 |
116 |
--------------------------------------------------------------------------------
/past/api/instagram.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import urllib
4 | import urlparse
5 | from past import config
6 | from past.utils.logger import logging
7 | from past.utils.escape import json_decode
8 | from past.utils import httplib2_request
9 |
10 | from past.model.user import User, UserAlias, OAuth2Token
11 | from past.model.data import InstagramUser
12 | from past.model.data import InstagramStatusData
13 |
14 | from .oauth2 import OAuth2
15 | from .error import OAuthLoginError, OAuthTokenExpiredError
16 |
17 | log = logging.getLogger(__file__)
18 |
19 | class Instagram(OAuth2):
20 |
21 | authorize_uri = 'https://api.instagram.com/oauth/authorize/'
22 | access_token_uri = 'https://api.instagram.com/oauth/access_token'
23 | api_host = 'https://api.instagram.com'
24 |
25 | def __init__(self, alias=None, access_token=None, refresh_token=None, api_version="v1"):
26 | self.api_version = api_version
27 | d = config.APIKEY_DICT[config.OPENID_INSTAGRAM]
28 | super(Instagram, self).__init__(provider = config.OPENID_INSTAGRAM,
29 | apikey = d["key"],
30 | apikey_secret = d["secret"],
31 | redirect_uri = d["redirect_uri"],
32 | scope = "basic likes comments relationships",
33 | alias=alias,
34 | access_token=access_token,
35 | refresh_token=refresh_token)
36 |
37 | @classmethod
38 | def get_client(cls, user_id):
39 | alias = UserAlias.get_by_user_and_type(user_id,
40 | config.OPENID_TYPE_DICT[config.OPENID_INSTAGRAM])
41 | if not alias:
42 | return None
43 |
44 | token = OAuth2Token.get(alias.id)
45 | if not token:
46 | return None
47 |
48 | return cls(alias.alias, token.access_token, token.refresh_token)
49 |
50 | def _request(self, api, method="GET", extra_dict=None):
51 | uri = urlparse.urljoin(self.api_host, api)
52 | if extra_dict is None:
53 | extra_dict = {}
54 |
55 | params = {
56 | "access_token": self.access_token,
57 | }
58 | params.update(extra_dict)
59 | qs = urllib.urlencode(params)
60 | uri = "%s?%s" % (uri, qs)
61 |
62 | log.info('getting %s...' % uri)
63 | resp, content = httplib2_request(uri, method)
64 | if resp.status == 200:
65 | return json_decode(content) if content else None
66 | else:
67 | log.warn("get %s fail, status code=%s, msg=%s" \
68 | % (uri, resp.status, content))
69 |
70 | def get_user_info(self, uid=None):
71 | uid = uid or self.user_alias.alias or "self"
72 | jdata = self._request("/v1/users/%s" % uid, "GET")
73 | if jdata and isinstance(jdata, dict):
74 | return InstagramUser(jdata.get("data"))
75 |
76 | def get_timeline(self, uid=None, min_id=None, max_id=None, count=100):
77 | d = {}
78 | d["count"] = count
79 | if min_id:
80 | d["min_id"] = min_id
81 | if max_id:
82 | d["max_id"] = max_id
83 | uid = uid or self.alias or "self"
84 |
85 | contents = self._request("/v1/users/%s/media/recent" %uid, "GET", d)
86 | ##debug
87 | if contents and isinstance(contents, dict):
88 | code = str(contents.get("meta", {}).get("code", ""))
89 | if code == "200":
90 | data = contents.get("data", [])
91 | print '---get instagram feed succ, result length is:', len(data)
92 | return [InstagramStatusData(c) for c in data]
93 |
94 | def get_home_timeline(self, uid=None, min_id=None, max_id=None, count=100):
95 | d = {}
96 | d["count"] = count
97 | if min_id:
98 | d["min_id"] = min_id
99 | if max_id:
100 | d["max_id"] = max_id
101 | uid = uid or self.alias or "self"
102 |
103 | contents = self._request("/v1/users/%s/feed" %uid, "GET", d)
104 | ##debug
105 | if contents and isinstance(contents, dict):
106 | code = str(contents.get("meta", {}).get("code", ""))
107 | if code == "200":
108 | data = contents.get("data", [])
109 | print '---get instagram home_timeline succ, result length is:', len(data)
110 | return [InstagramStatusData(c) for c in data]
111 |
--------------------------------------------------------------------------------
/past/static/fbtimeline/stylesheets/timeline.css:
--------------------------------------------------------------------------------
1 | /* Timeline */
2 | .timeline {
3 | background: url(../images/line.png) top repeat-y;
4 | list-style: none;
5 | padding: 0;
6 | margin: 0;
7 | position: relative;
8 | /* Highlight */
9 |
10 | /* Spine */
11 |
12 | }
13 | .timeline > li {
14 | float: left;
15 | clear: left;
16 | width: 436px;
17 | margin-bottom: 15px;
18 | position: relative;
19 | }
20 | .timeline > .right {
21 | float: right;
22 | clear: right;
23 | }
24 | .timeline .pointer {
25 | background: url(../images/icons-4.png) -41px -28px no-repeat;
26 | width: 19px;
27 | height: 15px;
28 | position: absolute;
29 | right: -18px;
30 | top: 20px;
31 | }
32 | .timeline .right > .pointer {
33 | left: -18px;
34 | right: auto;
35 | background-position: -61px -28px;
36 | }
37 | .timeline > .left + .right > .pointer {
38 | top: 40px;
39 | }
40 | .timeline > .right + li > .pointer {
41 | top: 40px;
42 | }
43 | .timeline .highlight {
44 | clear: both;
45 | width: auto;
46 | float: none;
47 | }
48 | .timeline .highlight .pointer {
49 | background-image: url(../images/icons-5.png);
50 | background-position: -26px -28px;
51 | height: 21px;
52 | width: 15px;
53 | left: 50%;
54 | top: -20px !important;
55 | margin-left: -7px;
56 | }
57 | .timeline .spine {
58 | position: absolute;
59 | left: 436px;
60 | width: 29px;
61 | height: 100%;
62 | }
63 | .timeline .spine > a,
64 | .timeline .spine a:visited {
65 | display: block;
66 | height: 100%;
67 | }
68 | /* Unit */
69 | .unit {
70 | background: #fff;
71 | padding: 5px;
72 | border: 1px #C4CDE0 solid;
73 | border-radius: 3px;
74 | }
75 | .storyUnit {
76 | padding: 10px;
77 | }
78 | .imageUnit {
79 | border-bottom: 1px #ccc solid;
80 | padding-bottom: 5px;
81 | margin-bottom: 15px;
82 | font-size: 11px;
83 | }
84 | .imageUnit .imageUnit-content {
85 | display: inline-block;
86 | vertical-align: top;
87 | padding-left: 5px;
88 | }
89 | .imageUnit .imageUnit-content > p {
90 | margin: 0;
91 | }
92 | .formUnit {
93 | border: 1px solid #B4BBCD;
94 | padding: 5px;
95 | margin-top: 5px;
96 | position: relative;
97 | }
98 | .formUnit .active {
99 | background: url(../images/active-unit.png) top left no-repeat;
100 | position: absolute;
101 | left: 10px;
102 | top: -6px;
103 | width: 9px;
104 | height: 6px;
105 | }
106 | .photoUnit {
107 | margin: 5px 0px;
108 | }
109 | .controls {
110 | background: #F2F2F2;
111 | margin: 5px -5px -5px -5px;
112 | list-style: none;
113 | padding: 4px;
114 | border-top: 1px #E6E6E6 solid;
115 | }
116 | .controls > li {
117 | display: inline-block;
118 | }
119 | .controls .post {
120 | float: right;
121 | }
122 | /* Actions */
123 | .actions {
124 | list-style: none;
125 | padding: 0;
126 | margin: 0;
127 | overflow: hidden;
128 | font-weight: bold;
129 | font-size: 11px;
130 | }
131 | .actions > li {
132 | display: inline;
133 | border-left: 1px #E5E5E5 solid;
134 | float: left;
135 | }
136 | .actions > li:first-child {
137 | border-left: none;
138 | }
139 | .actions > li > a {
140 | padding: 5px 5px;
141 | margin: 0 3px;
142 | display: inline-block;
143 | }
144 | .actions > li > a:hover {
145 | background: #EBEEF4;
146 | text-decoration: none;
147 | }
148 | .actions .active > a {
149 | color: #000;
150 | }
151 | /* Story Actions */
152 | .storyActions {
153 | background: #EDEFF4;
154 | list-style: none;
155 | padding: 5px 10px;
156 | margin: 10px 0 0 0;
157 | font-size: 11px;
158 | }
159 | .storyActions > li {
160 | display: inline;
161 | }
162 | .storyActions > li:before {
163 | content: " · ";
164 | }
165 | .storyActions > li:first-child:before {
166 | content: '';
167 | }
168 | /* Icons */
169 | .icon {
170 | width: 16px;
171 | height: 16px;
172 | background: url(../images/icons.png) top left no-repeat;
173 | display: inline-block;
174 | margin-right: 4px;
175 | vertical-align: middle;
176 | }
177 | .icon-status {
178 | background-position: 0 -308px;
179 | }
180 | .icon-photo {
181 | background-image: url(../images/icons-3.png);
182 | background-position: -68px -118px;
183 | }
184 | .icon-place {
185 | background-image: url(../images/icons-2.png);
186 | background-position: -664px -80px;
187 | }
188 | .icon-event {
189 | background-image: url(../images/icons-2.png);
190 | background-position: -647px -80px;
191 | }
192 | /* Spinner */
193 | #Spinner {
194 | text-align: center;
195 | padding: 10px 0;
196 | }
197 |
198 |
--------------------------------------------------------------------------------
/dep.txt:
--------------------------------------------------------------------------------
1 |
2 | sudo apt-get install python-virtualenv python-pip
3 | sudo apt-get install git ipython
4 | sudo apt-get install mysql-server
5 | #root password:password
6 | mysql_secure_installation
7 |
8 | sudo apt-get install python-mysqldb
9 | #cd env/lib/python2.6/ && ln -s /usr/lib/pymodules/python2.6/MySQLdb/ MySQLdb && ln -s /usr/lib/pymodules/python2.6/_mysql_exceptions.py _mysql_exceptions.py && ln -s /usr/lib/pymodules/python2.6/_mysql.so _mysql.so
10 |
11 | ##cd env/lib/python2.7
12 | ##ln -s /usr/lib/python2.7/dist-packages/MySQLdb MySQLdb
13 | ##ln -s /usr/lib/python2.7/dist-packages/_mysql_exceptions.py _mysql_exceptions.py
14 | ##ln -s /usr/lib/python2.7/dist-packages/_mysql.so _mysql.so
15 |
16 |
17 | #redis not use any more, instead by memcached and mongodb
18 | #sudo apt-get install redis-server
19 |
20 | git clone git://github.com/laiwei/thepast.git
21 | #virtualenv --no-site-packages env
22 | virtualenv env
23 |
24 | pip install flask redis tweepy httplib2
25 |
26 |
27 | ## mysql dump schema
28 | mysqldump -u root -pmypassword test_database --no-data=true --add-drop-table=false > schema_dump.sql
29 |
30 | ## mysql dump data
31 | mysqldump -u root -pmypassword test_database --add-drop-table=false > data_dump.sql
32 |
33 | ## import table
34 | create database `thepast`
35 | mysql -u root -ppassword thepast < past/schema.sql
36 |
37 | ##install nginx
38 | ###http://wiki.nginx.org/Install
39 | sudo -s
40 | nginx=stable # use nginx=development for latest development version
41 | echo "deb http://ppa.launchpad.net/nginx/$nginx/ubuntu lucid main" > /etc/apt/sources.list.d/nginx-$nginx-lucid.list
42 | apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C
43 |
44 | apt-get update
45 | apt-get install nginx
46 |
47 | ##deploy with nginx and uwsgi
48 | ###http://projects.unbit.it/uwsgi/wiki/Quickstart
49 | apt-get install build-essential python-dev libxml2-dev
50 | pip install uwsgi
51 |
52 | ## run app
53 | #uwsgi --socket 127.0.0.1:3031 --file /home/work/proj/thepast/pastme.py --callable app --processes 2
54 | #or
55 | #uwsgi -s /tmp/uwsgi.sock --file /home/work/proj/thepast/pastme.py --callable app --processes 2
56 | #recommend
57 | nohup uwsgi -s /tmp/uwsgi.sock --file /home/work/proj/thepast/pastme.py --callable app --processes 2 &
58 |
59 | ## xhtml2pdf
60 | git clone git://github.com/chrisglass/xhtml2pdf.git
61 | cd xhtml2pdf/
62 | #modify requirements.xml, == to >=
63 | pip install -r requirements.xml
64 | python setup.py install
65 |
66 | ### for test
67 | sudo apt-get install python-nose
68 | nosetests --with-coverage
69 |
70 |
71 | ## PIL setup
72 |
73 | sudo apt-get install libfreetype6-dev libjpeg8-dev
74 | sudo ln -s /usr/lib/i386-linux-gnu/libz.so /usr/lib/
75 | sudo ln -s /usr/lib/i386-linux-gnu/libfreetype.so.6 /usr/lib/
76 | sudo ln -s /usr/lib/i386-linux-gnu/libjpeg.so /usr/lib/
77 |
78 | #sudo ln -s /usr/lib/x86_64-linux-gnu/libfreetype.so /usr/lib/
79 | #sudo ln -s /usr/lib/x86_64-linux-gnu/libz.so /usr/lib/
80 | #sudo ln -s /usr/lib/x86_64-linux-gnu/libjpeg.so /usr/lib/
81 |
82 | #pip install -U PIL
83 | pip install --no-index -f http://dist.plone.org/thirdparty/ -U PIL
84 |
85 |
86 | ## config cache
87 | ## use redis instead memcached (no use any more)
88 | ### /etc/redis_7379.conf include ^/deploy/redis_cache.conf
89 | ### cp /etc/init.d/redis_6379 /etc/init.d/redis_7379
90 | ### sudo update-rc.d redis_7379 defaults
91 |
92 | ##mongo db
93 | ## sudo apt-get install mongodb-server
94 | wget http://fastdl.mongodb.org/linux/mongodb-linux-i686-2.0.3.tgz
95 | sudo mkdir -p /data/db
96 | tar xzf mongodb-linux-i686-2.0.3.tgz
97 | ./mongodb-xxxx/bin/mongod &
98 |
99 | pip install pymongo
100 |
101 | ## about mongodb
102 | http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries
103 | http://www.mongodb.org/display/DOCS/Optimization
104 | ##index
105 | http://www.mongodb.org/display/DOCS/Indexes
106 | http://blog.nosqlfan.com/html/271.html
107 |
108 | db.thepast.getIndexes
109 | db.thepast.ensureIndex({k:1})
110 |
111 |
112 | ## memcached
113 | sudo apt-get install memcached
114 | ## edit /etc/memcached.conf, -m 100 means max-memory = 100M
115 | ## sudo /etc/init.d/memcached restart
116 | ## telnet 127.0.0.1 11211 "stats"
117 | pip install python-memcached
118 |
119 |
120 | ## rss feed parse
121 | pip install feedparser
122 | #http://stackoverflow.com/questions/2244836/rss-feed-parser-library-in-python
123 | #http://packages.python.org/feedparser/
124 |
125 | ## add note
126 | pip install markdown2
127 | https://github.com/trentm/python-markdown2
128 |
129 |
130 |
--------------------------------------------------------------------------------
/past/view/pdf_view.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 | import os
3 | from datetime import datetime, timedelta
4 | import calendar
5 | import time
6 | from collections import defaultdict
7 |
8 | from flask import g, request, redirect, url_for, abort, render_template,\
9 | make_response, flash
10 |
11 | from past import app
12 | from past import config
13 | from past.model.user import User, PdfSettings
14 | from past.model.status import Status
15 |
16 | from past.utils import sizeof_fmt
17 | from past.utils.pdf import is_pdf_file_exists, get_pdf_filename, get_pdf_full_filename
18 | from past.utils.escape import json_encode
19 | from past import consts
20 | from .utils import require_login, check_access_user, statuses_timelize, get_sync_list
21 |
22 | @app.route("/pdf")
23 | @require_login()
24 | def mypdf():
25 | if not g.user:
26 | return redirect(url_for("pdf", uid=config.MY_USER_ID))
27 | else:
28 | return redirect(url_for("pdf", uid=g.user.id))
29 |
30 | @app.route("/pdf/apply", methods=["POST"])
31 | @require_login()
32 | def pdf_apply():
33 | delete = request.form.get("delete")
34 | if delete:
35 | PdfSettings.remove_user_id(g.user.id)
36 | flash(u"删除PDF的请求提交成功,系统会在接下来的一天里删除掉PDF文件!", "tip")
37 | return redirect("/pdf")
38 | else:
39 | PdfSettings.add_user_id(g.user.id)
40 | flash(u"申请已通过,请明天早上来下载数据吧!", "tip")
41 | return redirect("/pdf")
42 |
43 | @app.route("/demo-pdf")
44 | def demo_pdf():
45 | pdf_filename = "demo.pdf"
46 | full_file_name = os.path.join(config.PDF_FILE_DOWNLOAD_DIR, pdf_filename)
47 | resp = make_response()
48 | resp.headers['Cache-Control'] = 'no-cache'
49 | resp.headers['Content-Type'] = 'application/pdf'
50 | resp.headers['Content-Disposition'] = 'attachment; filename=%s' % pdf_filename
51 | resp.headers['Content-Length'] = os.path.getsize(full_file_name)
52 | redir = '/down/pdf/' + pdf_filename
53 | resp.headers['X-Accel-Redirect'] = redir
54 | return resp
55 |
56 | #PDF只允许登录用户查看
57 | @app.route("//pdf")
58 | @require_login()
59 | def pdf(uid):
60 | user = User.get(uid)
61 | if not user:
62 | abort(404, "No such user")
63 |
64 | if uid != g.user.id and user.get_profile_item('user_privacy') == consts.USER_PRIVACY_PRIVATE:
65 | flash(u"由于该用户设置了仅自己可见的权限,所以,我们就看不到了", "tip")
66 | return redirect("/")
67 |
68 | intros = [g.user.get_thirdparty_profile(x).get("intro") for x in config.OPENID_TYPE_DICT.values()]
69 | intros = filter(None, intros)
70 |
71 | pdf_files = []
72 | start_date = Status.get_oldest_create_time(None, user.id)
73 | now = datetime.now()
74 | d = start_date
75 | while d and d <= now:
76 | pdf_filename = get_pdf_filename(user.id, d.strftime("%Y%m"))
77 | if is_pdf_file_exists(pdf_filename):
78 | full_file_name = get_pdf_full_filename(pdf_filename)
79 | pdf_files.append([d, pdf_filename, sizeof_fmt(os.path.getsize(full_file_name))])
80 |
81 | days = calendar.monthrange(d.year, d.month)[1]
82 | d += timedelta(days=days)
83 | d = datetime(d.year, d.month, 1)
84 | files_dict = defaultdict(list)
85 | for date, filename, filesize in pdf_files:
86 | files_dict[date.year].append([date, filename, filesize])
87 |
88 | pdf_applyed = PdfSettings.is_user_id_exists(g.user.id)
89 | return render_template("v2/pdf.html", **locals())
90 |
91 | @app.route("/pdf/")
92 | @require_login()
93 | def pdf_down(filename):
94 | pdf_filename = filename
95 | if not is_pdf_file_exists(pdf_filename):
96 | abort(404, "Please wait one day to download the PDF version, because the vps memory is limited")
97 |
98 | user_id = pdf_filename.split('_')[1]
99 | u = User.get(user_id)
100 | if not u:
101 | abort(400, 'Bad request')
102 |
103 | if user_id != g.user.id and u.get_profile_item('user_privacy') == consts.USER_PRIVACY_PRIVATE:
104 | abort(403, 'Not allowed')
105 |
106 | full_file_name = get_pdf_full_filename(pdf_filename)
107 | resp = make_response()
108 | resp.headers['Cache-Control'] = 'no-cache'
109 | resp.headers['Content-Type'] = 'text/html'
110 | resp.headers['Content-Encoding'] = 'gzip'
111 | resp.headers['Content-Disposition'] = 'attachment; filename=%s' % pdf_filename
112 | resp.headers['Content-Length'] = os.path.getsize(full_file_name)
113 | redir = '/down/pdf/' + pdf_filename
114 | resp.headers['X-Accel-Redirect'] = redir
115 | return resp
116 |
117 |
--------------------------------------------------------------------------------
/past/static/js/date.format.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Date Format 1.2.3
3 | * (c) 2007-2009 Steven Levithan
4 | * MIT license
5 | *
6 | * Includes enhancements by Scott Trenda
7 | * and Kris Kowal
8 | *
9 | * Accepts a date, a mask, or a date and a mask.
10 | * Returns a formatted version of the given date.
11 | * The date defaults to the current date/time.
12 | * The mask defaults to dateFormat.masks.default.
13 | */
14 |
15 | var dateFormat = function () {
16 | var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
17 | timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
18 | timezoneClip = /[^-+\dA-Z]/g,
19 | pad = function (val, len) {
20 | val = String(val);
21 | len = len || 2;
22 | while (val.length < len) val = "0" + val;
23 | return val;
24 | };
25 |
26 | // Regexes and supporting functions are cached through closure
27 | return function (date, mask, utc) {
28 | var dF = dateFormat;
29 |
30 | // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
31 | if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
32 | mask = date;
33 | date = undefined;
34 | }
35 |
36 | // Passing date through Date applies Date.parse, if necessary
37 | date = date ? new Date(date) : new Date;
38 | if (isNaN(date)) throw SyntaxError("invalid date");
39 |
40 | mask = String(dF.masks[mask] || mask || dF.masks["default"]);
41 |
42 | // Allow setting the utc argument via the mask
43 | if (mask.slice(0, 4) == "UTC:") {
44 | mask = mask.slice(4);
45 | utc = true;
46 | }
47 |
48 | var _ = utc ? "getUTC" : "get",
49 | d = date[_ + "Date"](),
50 | D = date[_ + "Day"](),
51 | m = date[_ + "Month"](),
52 | y = date[_ + "FullYear"](),
53 | H = date[_ + "Hours"](),
54 | M = date[_ + "Minutes"](),
55 | s = date[_ + "Seconds"](),
56 | L = date[_ + "Milliseconds"](),
57 | o = utc ? 0 : date.getTimezoneOffset(),
58 | flags = {
59 | d: d,
60 | dd: pad(d),
61 | ddd: dF.i18n.dayNames[D],
62 | dddd: dF.i18n.dayNames[D + 7],
63 | m: m + 1,
64 | mm: pad(m + 1),
65 | mmm: dF.i18n.monthNames[m],
66 | mmmm: dF.i18n.monthNames[m + 12],
67 | yy: String(y).slice(2),
68 | yyyy: y,
69 | h: H % 12 || 12,
70 | hh: pad(H % 12 || 12),
71 | H: H,
72 | HH: pad(H),
73 | M: M,
74 | MM: pad(M),
75 | s: s,
76 | ss: pad(s),
77 | l: pad(L, 3),
78 | L: pad(L > 99 ? Math.round(L / 10) : L),
79 | t: H < 12 ? "a" : "p",
80 | tt: H < 12 ? "am" : "pm",
81 | T: H < 12 ? "A" : "P",
82 | TT: H < 12 ? "AM" : "PM",
83 | Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
84 | o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
85 | S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
86 | };
87 |
88 | return mask.replace(token, function ($0) {
89 | return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
90 | });
91 | };
92 | }();
93 |
94 | // Some common format strings
95 | dateFormat.masks = {
96 | "default": "ddd mmm dd yyyy HH:MM:ss",
97 | shortDate: "m/d/yy",
98 | mediumDate: "mmm d, yyyy",
99 | longDate: "mmmm d, yyyy",
100 | fullDate: "dddd, mmmm d, yyyy",
101 | shortTime: "h:MM TT",
102 | mediumTime: "h:MM:ss TT",
103 | longTime: "h:MM:ss TT Z",
104 | isoDate: "yyyy-mm-dd",
105 | isoTime: "HH:MM:ss",
106 | isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
107 | isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
108 | };
109 |
110 | // Internationalization strings
111 | dateFormat.i18n = {
112 | dayNames: [
113 | "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
114 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
115 | ],
116 | monthNames: [
117 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
118 | "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
119 | ]
120 | };
121 |
122 | // For convenience...
123 | Date.prototype.format = function (mask, utc) {
124 | return dateFormat(this, mask, utc);
125 | };
126 |
127 |
--------------------------------------------------------------------------------
/past/model/kv.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | from MySQLdb import IntegrityError
4 |
5 | from past.corelib.cache import cache
6 | from past.store import db_conn, mc
7 | from past.utils.escape import json_encode, json_decode
8 |
9 | class Kv(object):
10 | def __init__(self, key_, val, time):
11 | self.key_ = key_
12 | self.val = val
13 | self.time = time
14 |
15 | @classmethod
16 | def clear_cache(cls, key_):
17 | mc.delete("mc_kv:%s" %key_)
18 |
19 | @classmethod
20 | @cache("mc_kv:{key_}")
21 | def get(cls, key_):
22 | cursor = db_conn.execute('''select `key`, value, time from kv
23 | where `key`=%s''', key_)
24 | row = cursor.fetchone()
25 | if row:
26 | return cls(*row)
27 | cursor and cursor.close()
28 |
29 | @classmethod
30 | def set(cls, key_, val):
31 | cursor = None
32 | val = json_encode(val) if not isinstance(val, basestring) else val
33 |
34 | try:
35 | cursor = db_conn.execute('''replace into kv(`key`, value)
36 | values(%s,%s)''', (key_, val))
37 | db_conn.commit()
38 | cls.clear_cache(key_)
39 | except IntegrityError:
40 | db_conn.rollback()
41 |
42 | cursor and cursor.close()
43 |
44 | @classmethod
45 | def remove(cls, key_):
46 | cursor = None
47 | try:
48 | cursor = db_conn.execute('''delete from kv where `key` = %s''', key_)
49 | db_conn.commit()
50 | cls.clear_cache(key_)
51 | except IntegrityError:
52 | db_conn.rollback()
53 | cursor and cursor.close()
54 |
55 | class UserProfile(object):
56 | def __init__(self, user_id, val, time):
57 | self.user_id = user_id
58 | self.val = val
59 | self.time = time
60 |
61 | @classmethod
62 | def clear_cache(cls, user_id):
63 | mc.delete("mc_user_profile:%s" %user_id)
64 |
65 | @classmethod
66 | @cache("mc_user_profile:{user_id}")
67 | def get(cls, user_id):
68 | cursor = db_conn.execute('''select user_id, profile, time from user_profile
69 | where user_id=%s''', user_id)
70 | row = cursor.fetchone()
71 | if row:
72 | return cls(*row)
73 | cursor and cursor.close()
74 |
75 | @classmethod
76 | def set(cls, user_id, val):
77 | cursor = None
78 | val = json_encode(val) if not isinstance(val, basestring) else val
79 |
80 | try:
81 | cursor = db_conn.execute('''replace into user_profile (user_id, profile)
82 | values(%s,%s)''', (user_id, val))
83 | db_conn.commit()
84 | cls.clear_cache(user_id)
85 | except IntegrityError:
86 | db_conn.rollback()
87 |
88 | cursor and cursor.close()
89 |
90 | @classmethod
91 | def remove(cls, user_id):
92 | cursor = None
93 | try:
94 | cursor = db_conn.execute('''delete from user_profile where user_id= %s''', user_id)
95 | db_conn.commit()
96 | cls.clear_cache(user_id)
97 | except IntegrityError:
98 | db_conn.rollback()
99 | cursor and cursor.close()
100 |
101 | class RawStatus(object):
102 | def __init__(self, status_id, text, raw, time):
103 | self.status_id = status_id
104 | self.text = text
105 | self.raw = raw
106 | self.time = time
107 |
108 | @classmethod
109 | def clear_cache(cls, status_id):
110 | mc.delete("mc_raw_status:%s" %status_id)
111 |
112 | @classmethod
113 | @cache("mc_raw_status:{status_id}")
114 | def get(cls, status_id):
115 | cursor = db_conn.execute('''select status_id, text, raw, time from raw_status
116 | where status_id=%s''', status_id)
117 | row = cursor.fetchone()
118 | if row:
119 | return cls(*row)
120 | cursor and cursor.close()
121 |
122 | @classmethod
123 | def set(cls, status_id, text, raw):
124 | cursor = None
125 | text = json_encode(text) if not isinstance(text, basestring) else text
126 | raw = json_encode(raw) if not isinstance(raw, basestring) else raw
127 |
128 | try:
129 | cursor = db_conn.execute('''replace into raw_status (status_id, text, raw)
130 | values(%s,%s,%s)''', (status_id, text, raw))
131 | db_conn.commit()
132 | cls.clear_cache(status_id)
133 | except IntegrityError:
134 | db_conn.rollback()
135 |
136 | cursor and cursor.close()
137 |
138 | @classmethod
139 | def remove(cls, status_id):
140 | cursor = None
141 | try:
142 | cursor = db_conn.execute('''delete from raw_status where status_id = %s''', status_id )
143 | db_conn.commit()
144 | cls.clear_cache(status_id)
145 | except IntegrityError:
146 | db_conn.rollback()
147 | cursor and cursor.close()
148 |
--------------------------------------------------------------------------------
/past/templates/v2/timeline_base.html:
--------------------------------------------------------------------------------
1 | {% extends "v2/base.html" %}
2 | {% import "status.html" as status_tmpl_helper %}
3 | {% import "blocks.html" as blocks_tmpl_helper %}
4 |
5 | {%block css%}
6 | {{super()}}
7 |
8 | {%endblock%}
9 |
10 | {% block js%}
11 | {{super()}}
12 |
13 | {% endblock %}
14 |
15 | {%block sidebar%}
16 |
17 |
85 |
86 | {%endblock%}
87 |
88 | {%block rightbar%}
89 |
90 |
91 | {%if not status_list%}
92 |
93 |
欢迎加入旧时光,thepast正在从第三方同步你的消息,请稍等(最多5分钟哦)
94 |
趁着这块空闲,你可以随机看看别人的页面
95 |
或者
96 |
看看laiwei历史上的每天
97 |
98 | {%else%}
99 |
100 |
101 | {%set left = True%}
102 | {%for repeated_status in status_list%}
103 | {%if left%}
104 | -
105 | {%else%}
106 |
-
107 | {%endif%}
108 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}}
109 |
110 | {%set left=not left%}
111 | {%endfor%}
112 |
113 |
正在努力加载更多中...
114 | {%endif%}
115 |
116 |
117 | {%endblock%}
118 |
119 |
--------------------------------------------------------------------------------
/past/model/note.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | import markdown2
4 | from MySQLdb import IntegrityError
5 | import datetime
6 |
7 | from past.store import db_conn, mc
8 | from past.corelib.cache import cache, pcache, HALF_HOUR
9 | from past.utils.escape import json_encode, json_decode
10 | from past import consts
11 | from past import config
12 |
13 | class Note(object):
14 |
15 | def __init__(self, id, user_id, title, content, create_time, update_time, fmt, privacy):
16 | self.id = id
17 | self.user_id = str(user_id)
18 | self.title = title
19 | self.content = content
20 | self.create_time = create_time
21 | self.update_time = update_time
22 | self.fmt = fmt
23 | self.privacy = privacy
24 |
25 | @classmethod
26 | def _clear_cache(cls, user_id, note_id):
27 | if user_id:
28 | mc.delete("note_ids:%s" % user_id)
29 | mc.delete("note_ids_asc:%s" % user_id)
30 | if note_id:
31 | mc.delete("note:%s" % note_id)
32 |
33 | def flush_note(self):
34 | Note._clear_cache(None, self.id)
35 | return Note.get(self.id)
36 |
37 | @classmethod
38 | @cache("note:{id}")
39 | def get(cls, id):
40 | cursor = db_conn.execute('''select id, user_id, title, content, create_time, update_time, fmt, privacy
41 | from note where id = %s''', id)
42 | row = cursor.fetchone()
43 | if row:
44 | return cls(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7])
45 |
46 | def render_content(self):
47 | if self.fmt == consts.NOTE_FMT_MARKDOWN:
48 | return markdown2.markdown(self.content, extras=["wiki-tables", "code-friendly"])
49 | else:
50 | return self.content
51 |
52 | @classmethod
53 | def add(cls, user_id, title, content, fmt=consts.NOTE_FMT_PLAIN, privacy=consts.STATUS_PRIVACY_PUBLIC):
54 | cursor = None
55 | try:
56 | cursor = db_conn.execute('''insert into note (user_id, title, content, create_time, fmt, privacy)
57 | values (%s, %s, %s, %s, %s, %s)''',
58 | (user_id, title, content, datetime.datetime.now(), fmt, privacy))
59 | db_conn.commit()
60 |
61 | note_id = cursor.lastrowid
62 | note = cls.get(note_id)
63 | from past.model.status import Status
64 | Status.add(user_id, note_id,
65 | note.create_time, config.OPENID_TYPE_DICT[config.OPENID_THEPAST],
66 | config.CATE_THEPAST_NOTE, "")
67 | cls._clear_cache(user_id, None)
68 | return note
69 | except IntegrityError:
70 | db_conn.rollback()
71 | finally:
72 | cursor and cursor.close()
73 |
74 | def update(self, title, content, fmt, privacy):
75 | if title and title != self.title or fmt and fmt != self.fmt or content and content != self.content or privacy and privacy != self.privacy:
76 | _fmt = fmt or self.fmt
77 | _title = title or self.title
78 | _content = content or self.content
79 | _privacy = privacy or self.privacy
80 | db_conn.execute('''update note set title = %s, content = %s, fmt = %s, privacy = %s where id = %s''',
81 | (_title, _content, _fmt, _privacy, self.id))
82 | db_conn.commit()
83 | self.flush_note()
84 |
85 | if title != self.title:
86 | from past.model.status import Status
87 | Status._clear_cache(None, self.get_status_id(), None)
88 |
89 | @cache("note:status_id:{self.id}")
90 | def get_status_id(self):
91 | cursor = db_conn.execute("""select id from status where origin_id = %s and category = %s""",
92 | (self.id, config.CATE_THEPAST_NOTE))
93 | row = cursor.fetchone()
94 | cursor and cursor.close()
95 | return row and row[0]
96 |
97 | @classmethod
98 | def delete(cls, id):
99 | note = cls.get(id)
100 | if note:
101 | db_conn.execute("""delete from status where id=%s""", id)
102 | db_conn.commit()
103 | cls._clear_cache(note.user_id, note.id)
104 |
105 | @classmethod
106 | @pcache("note_ids:user:{user_id}")
107 | def get_ids_by_user(cls, user_id, start, limit):
108 | return cls._get_ids_by_user(user_id, start, limit)
109 |
110 | @classmethod
111 | @pcache("note_ids_asc:user:{user_id}")
112 | def get_ids_by_user_asc(cls, user_id, start, limit):
113 | return cls._get_ids_by_user(user_id, start, limit, order="create_time asc")
114 |
115 | @classmethod
116 | def _get_ids_by_user(cls, user_id, start=0, limit=20, order="create_time desc"):
117 | sql = """select id from note where user_id=%s order by """ + order \
118 | + """ limit %s,%s"""
119 | cursor = db_conn.execute(sql, (user_id, start, limit))
120 | rows = cursor.fetchall()
121 | return [x[0] for x in rows]
122 |
123 | @classmethod
124 | def gets(cls, ids):
125 | return [cls.get(x) for x in ids]
126 |
--------------------------------------------------------------------------------
/past/view/note.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | #past.view.note
4 |
5 | import markdown2
6 | from flask import g, flash, request, render_template, redirect, abort, url_for
7 | from past import app
8 |
9 | from past.utils.escape import json_encode
10 | from past.utils import randbytes
11 | from past.store import mc
12 | from past.model.user import User
13 | from past.model.note import Note
14 | from past import consts
15 | from past import config
16 |
17 | from .utils import require_login, check_access_note
18 |
19 | @app.route("/notes", methods=["GET"])
20 | @require_login()
21 | def my_notes():
22 | return redirect("/%s?cate=%s" % (g.user.uid, config.CATE_THEPAST_NOTE))
23 |
24 | @app.route("//notes", methods=["GET"])
25 | def user_notes(uid):
26 | user = User.get(uid)
27 | if not user:
28 | abort(403, "no_such_user")
29 |
30 | return redirect("/%s?cate=%s" % (uid, config.CATE_THEPAST_NOTE))
31 |
32 | @app.route("/note/", methods=["GET",])
33 | def note(nid):
34 | note = Note.get(nid)
35 | if not note:
36 | abort(404, "no such note")
37 |
38 | r = check_access_note(note)
39 | if r:
40 | flash(r[1].decode("utf8"), "tip")
41 | return redirect(url_for("home"))
42 |
43 | title = note.title
44 | content = note.content
45 | fmt = note.fmt
46 | if fmt == consts.NOTE_FMT_MARKDOWN:
47 | content = markdown2.markdown(note.content, extras=["wiki-tables", "code-friendly"])
48 | create_time = note.create_time
49 | user = User.get(note.user_id)
50 | return render_template("v2/note.html", consts=consts, **locals())
51 |
52 | @app.route("/note/edit/", methods=["GET", "POST"])
53 | @require_login()
54 | def note_edit(nid):
55 | note = Note.get(nid)
56 | if not note:
57 | abort(404, "no such note")
58 |
59 | if g.user.id != note.user_id:
60 | abort(403, "not edit privileges")
61 |
62 | error = ""
63 | if request.method == "GET":
64 | title = note.title
65 | content = note.content
66 | fmt = note.fmt
67 | privacy = note.privacy
68 | return render_template("v2/note_create.html", consts=consts, **locals())
69 |
70 | elif request.method == "POST":
71 | # edit
72 | title = request.form.get("title", "")
73 | content = request.form.get("content", "")
74 | fmt = request.form.get("fmt", consts.NOTE_FMT_PLAIN)
75 | privacy = request.form.get("privacy", consts.STATUS_PRIVACY_PUBLIC)
76 |
77 | if request.form.get("cancel"):
78 | return redirect("/note/%s" % note.id)
79 |
80 | if request.form.get("submit"):
81 | error = check_note(title, content)
82 | if not error:
83 | note.update(title, content, fmt, privacy)
84 | flash(u"日记修改成功", "tip")
85 | return redirect("/note/%s" % note.id)
86 | else:
87 | flash(error.decode("utf8"), "error")
88 | return render_template("v2/note_create.html", consts=consts, **locals())
89 |
90 | else:
91 | return redirect("/note/%s" % note.id)
92 |
93 | @app.route("/note/create", methods=["GET", "POST"])
94 | @require_login(msg="先登录才能写日记")
95 | def note_create():
96 | user = g.user
97 | error = ""
98 | if request.method == "POST":
99 |
100 | title = request.form.get("title", "")
101 | content = request.form.get("content", "")
102 | fmt = request.form.get("fmt", consts.NOTE_FMT_PLAIN)
103 | privacy = request.form.get("privacy", consts.STATUS_PRIVACY_PUBLIC)
104 |
105 | if request.form.get("cancel"):
106 | return redirect("/i")
107 |
108 | # submit
109 | error = check_note(title, content)
110 |
111 | if not error:
112 | note = Note.add(g.user.id, title, content, fmt, privacy)
113 | if note:
114 | flash(u"日记写好了,看看吧", "tip")
115 | return redirect("/note/%s" % note.id)
116 | else:
117 | error = "添加日记的时候失败了,真不走运,再试试吧^^"
118 | if error:
119 | flash(error.decode("utf8"), "error")
120 | return render_template("v2/note_create.html", consts=consts, **locals())
121 |
122 | elif request.method == "GET":
123 | return render_template("v2/note_create.html", consts=consts, **locals())
124 |
125 | else:
126 | abort("wrong_http_method")
127 |
128 | @app.route("/note/preview", methods=["POST"])
129 | def note_preview():
130 | r = {}
131 | content = request.form.get("content", "")
132 | fmt = request.form.get("fmt", consts.NOTE_FMT_PLAIN)
133 | if fmt == consts.NOTE_FMT_MARKDOWN:
134 | r['data'] = markdown2.markdown(content, extras=["wiki-tables", "code-friendly"])
135 | else:
136 | r['data'] = content
137 |
138 | return json_encode(r)
139 |
140 | def check_note(title, content):
141 | error = ""
142 | if not title:
143 | error = "得有个标题^^"
144 | elif not content:
145 | error = "写点内容撒^^"
146 | elif len(title) > 120:
147 | error = "标题有些太长了"
148 | elif len(content) > 102400:
149 | error = "正文也太长了吧"
150 |
151 | return error
152 |
--------------------------------------------------------------------------------
/past/api/twitter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import tweepy
4 | from tweepy.error import TweepError
5 |
6 | from past import config
7 | from past.utils.escape import json_encode, json_decode
8 | from past.utils import httplib2_request
9 |
10 | from past.model.user import User, UserAlias, OAuth2Token
11 | from past.model.user import OAuth2Token
12 | from past.model.data import TwitterUser
13 | from past.model.data import TwitterStatusData
14 |
15 | from .error import OAuthTokenExpiredError
16 |
17 | class TwitterOAuth1(object):
18 | provider = config.OPENID_TWITTER
19 |
20 | def __init__(self, alias=None,
21 | apikey=None, apikey_secret=None, redirect_uri=None,
22 | token=None, token_secret=None):
23 |
24 | d = config.APIKEY_DICT[config.OPENID_TWITTER]
25 |
26 | self.consumer_key = apikey or d['key']
27 | self.consumer_secret = apikey_secret or d['secret']
28 | self.callback = redirect_uri or d['redirect_uri']
29 |
30 | self.token = token
31 | self.token_secret = token_secret
32 |
33 | self.alias = alias
34 | if alias:
35 | self.user_alias = UserAlias.get(
36 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], alias)
37 | else:
38 | self.user_alias = None
39 |
40 | self.auth = tweepy.OAuthHandler(self.consumer_key, self.consumer_secret, self.callback)
41 | if self.token and self.token_secret and self.auth:
42 | self.auth.set_access_token(self.token, self.token_secret)
43 |
44 | def __repr__(self):
45 | return "" \
46 | % (self.consumer_key, self.consumer_secret, self.token, self.token_secret)
47 | __str__ = __repr__
48 |
49 | def login(self):
50 | return self.auth.get_authorization_url()
51 |
52 | def get_access_token(self, verifier=None):
53 | self.auth.get_access_token(verifier)
54 | t = {"access_token":self.auth.access_token.key,
55 | "access_token_secret": self.auth.access_token.secret,}
56 | return t
57 |
58 | def save_request_token_to_session(self, session_):
59 | t = {"key": self.auth.request_token.key,
60 | "secret": self.auth.request_token.secret,}
61 | session_['request_token'] = json_encode(t)
62 |
63 | def get_request_token_from_session(self, session_, delete=True):
64 | t = session_.get("request_token")
65 | token = json_decode(t) if t else {}
66 | if delete:
67 | self.delete_request_token_from_session(session_)
68 | return token
69 |
70 | def delete_request_token_from_session(self, session_):
71 | session_.pop("request_token", None)
72 |
73 | @classmethod
74 | def get_client(cls, user_id):
75 | alias = UserAlias.get_by_user_and_type(user_id,
76 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER])
77 | if not alias:
78 | return None
79 |
80 | token = OAuth2Token.get(alias.id)
81 | if not token:
82 | return None
83 |
84 | return cls(alias=alias.alias, token=token.access_token, token_secret=token.refresh_token)
85 |
86 | def api(self):
87 | return tweepy.API(self.auth, parser=tweepy.parsers.JSONParser())
88 |
89 | def get_user_info(self):
90 | user = self.api().me()
91 | return TwitterUser(user)
92 |
93 | def get_timeline(self, since_id=None, max_id=None, count=200):
94 | user_id = self.user_alias and self.user_alias.user_id or None
95 | try:
96 | contents = self.api().user_timeline(since_id=since_id, max_id=max_id, count=count)
97 | excp = OAuthTokenExpiredError(user_id,
98 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], "")
99 | excp.clear_the_profile()
100 | return [TwitterStatusData(c) for c in contents]
101 | except TweepError, e:
102 | excp = OAuthTokenExpiredError(user_id,
103 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER],
104 | "%s:%s" %(e.reason, e.response))
105 | excp.set_the_profile()
106 | raise excp
107 |
108 | def post_status(self, text):
109 | user_id = self.user_alias and self.user_alias.user_id or None
110 | try:
111 | self.api().update_status(status=text)
112 | excp = OAuthTokenExpiredError(user_id,
113 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], "")
114 | excp.clear_the_profile()
115 | except TweepError, e:
116 | excp = OAuthTokenExpiredError(user_id,
117 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER],
118 | "%s:%s" %(e.reason, e.response))
119 | excp.set_the_profile()
120 | raise excp
121 |
122 |
--------------------------------------------------------------------------------
/past/corelib/cache.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 |
3 | '''from douban code, cool '''
4 |
5 | import inspect
6 | from functools import wraps
7 | import time
8 |
9 | try:
10 | import cPickle as pickle
11 | except:
12 | import pickle
13 |
14 | from .empty import Empty
15 | from .format import format
16 |
17 | from past.store import mc
18 |
19 | # some time consts for mc expire
20 | HALF_HOUR = 1800
21 | ONE_HOUR = 3600
22 | HALF_DAY = ONE_HOUR * 12
23 | ONE_DAY = ONE_HOUR * 24
24 | ONE_WEEK = ONE_DAY * 7
25 | ONE_MONTH = ONE_DAY * 30
26 |
27 |
28 | def gen_key(key_pattern, arg_names, defaults, *a, **kw):
29 | return gen_key_factory(key_pattern, arg_names, defaults)(*a, **kw)
30 |
31 |
32 | def gen_key_factory(key_pattern, arg_names, defaults):
33 | args = dict(zip(arg_names[-len(defaults):], defaults)) if defaults else {}
34 | if callable(key_pattern):
35 | names = inspect.getargspec(key_pattern)[0]
36 | def gen_key(*a, **kw):
37 | aa = args.copy()
38 | aa.update(zip(arg_names, a))
39 | aa.update(kw)
40 | if callable(key_pattern):
41 | key = key_pattern(*[aa[n] for n in names])
42 | else:
43 | key = format(key_pattern, *[aa[n] for n in arg_names], **aa)
44 | return key and key.replace(' ','_'), aa
45 | return gen_key
46 |
47 | def cache_(key_pattern, mc, expire=0, max_retry=0):
48 | def deco(f):
49 | arg_names, varargs, varkw, defaults = inspect.getargspec(f)
50 | if varargs or varkw:
51 | raise Exception("do not support varargs")
52 | gen_key = gen_key_factory(key_pattern, arg_names, defaults)
53 | @wraps(f)
54 | def _(*a, **kw):
55 | key, args = gen_key(*a, **kw)
56 | if not key:
57 | return f(*a, **kw)
58 | if isinstance(key, unicode):
59 | key = key.encode("utf8")
60 | r = mc.get(key)
61 |
62 | # anti miss-storm
63 | retry = max_retry
64 | while r is None and retry > 0:
65 | time.sleep(0.1)
66 | r = mc.get(key)
67 | retry -= 1
68 | r = pickle.loads(r) if r else None
69 |
70 | if r is None:
71 | r = f(*a, **kw)
72 | if r is not None:
73 | mc.set(key, pickle.dumps(r), expire)
74 |
75 | if isinstance(r, Empty):
76 | r = None
77 | return r
78 | _.original_function = f
79 | return _
80 | return deco
81 |
82 | def pcache_(key_pattern, mc, count=300, expire=0, max_retry=0):
83 | def deco(f):
84 | arg_names, varargs, varkw, defaults = inspect.getargspec(f)
85 | if varargs or varkw:
86 | raise Exception("do not support varargs")
87 | if not ('limit' in arg_names):
88 | raise Exception("function must has 'limit' in args")
89 | gen_key = gen_key_factory(key_pattern, arg_names, defaults)
90 | @wraps(f)
91 | def _(*a, **kw):
92 | key, args = gen_key(*a, **kw)
93 | start = args.pop('start', 0)
94 | limit = args.pop('limit')
95 | start = int(start)
96 | limit = int(limit)
97 | if not key or limit is None or start+limit > count:
98 | return f(*a, **kw)
99 | if isinstance(key, unicode):
100 | key = key.encode("utf8")
101 | r = mc.get(key)
102 |
103 | # anti miss-storm
104 | retry = max_retry
105 | while r is None and retry > 0:
106 | time.sleep(0.1)
107 | r = mc.get(key)
108 | retry -= 1
109 | r = pickle.loads(r) if r else None
110 |
111 | if r is None:
112 | r = f(limit=count, **args)
113 | mc.set(key, pickle.dumps(r), expire)
114 | return r[start:start+limit]
115 |
116 | _.original_function = f
117 | return _
118 | return deco
119 |
120 | def delete_cache_(key_pattern, mc):
121 | def deco(f):
122 | arg_names, varargs, varkw, defaults = inspect.getargspec(f)
123 | if varargs or varkw:
124 | raise Exception("do not support varargs")
125 | gen_key = gen_key_factory(key_pattern, arg_names, defaults)
126 | @wraps(f)
127 | def _(*a, **kw):
128 | key, args = gen_key(*a, **kw)
129 | r = f(*a, **kw)
130 | mc.delete(key)
131 | return r
132 | return _
133 | _.original_function = f
134 | return deco
135 |
136 | def create_decorators(mc):
137 |
138 | def _cache(key_pattern, expire=0, mc=mc, max_retry=0):
139 | return cache_(key_pattern, mc, expire=expire, max_retry=max_retry)
140 |
141 | def _pcache(key_pattern, count=300, expire=0, max_retry=0):
142 | return pcache_(key_pattern, mc, count=count, expire=expire, max_retry=max_retry)
143 |
144 | def _delete_cache(key_pattern):
145 | return delete_cache_(key_pattern, mc=mc)
146 |
147 | return dict(cache=_cache, pcache=_pcache, delete_cache=_delete_cache)
148 |
149 |
150 | globals().update(create_decorators(mc))
151 |
152 |
--------------------------------------------------------------------------------
/past/static/js/jquery.masonry.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jQuery Masonry v2.1.05
3 | * A dynamic layout plugin for jQuery
4 | * The flip-side of CSS Floats
5 | * http://masonry.desandro.com
6 | *
7 | * Licensed under the MIT license.
8 | * Copyright 2012 David DeSandro
9 | */
10 | (function(a,b,c){"use strict";var d=b.event,e;d.special.smartresize={setup:function(){b(this).bind("resize",d.special.smartresize.handler)},teardown:function(){b(this).unbind("resize",d.special.smartresize.handler)},handler:function(a,c){var d=this,f=arguments;a.type="smartresize",e&&clearTimeout(e),e=setTimeout(function(){b.event.handle.apply(d,f)},c==="execAsap"?0:100)}},b.fn.smartresize=function(a){return a?this.bind("smartresize",a):this.trigger("smartresize",["execAsap"])},b.Mason=function(a,c){this.element=b(c),this._create(a),this._init()},b.Mason.settings={isResizable:!0,isAnimated:!1,animationOptions:{queue:!1,duration:500},gutterWidth:0,isRTL:!1,isFitWidth:!1,containerStyle:{position:"relative"}},b.Mason.prototype={_filterFindBricks:function(a){var b=this.options.itemSelector;return b?a.filter(b).add(a.find(b)):a},_getBricks:function(a){var b=this._filterFindBricks(a).css({position:"absolute"}).addClass("masonry-brick");return b},_create:function(c){this.options=b.extend(!0,{},b.Mason.settings,c),this.styleQueue=[];var d=this.element[0].style;this.originalStyle={height:d.height||""};var e=this.options.containerStyle;for(var f in e)this.originalStyle[f]=d[f]||"";this.element.css(e),this.horizontalDirection=this.options.isRTL?"right":"left",this.offset={x:parseInt(this.element.css("padding-"+this.horizontalDirection),10),y:parseInt(this.element.css("padding-top"),10)},this.isFluid=this.options.columnWidth&&typeof this.options.columnWidth=="function";var g=this;setTimeout(function(){g.element.addClass("masonry")},0),this.options.isResizable&&b(a).bind("smartresize.masonry",function(){g.resize()}),this.reloadItems()},_init:function(a){this._getColumns(),this._reLayout(a)},option:function(a,c){b.isPlainObject(a)&&(this.options=b.extend(!0,this.options,a))},layout:function(a,b){for(var c=0,d=a.length;c