|
|
@@ -13,7 +13,11 @@ import gc
|
|
13
|
13
|
import collections
|
|
14
|
14
|
import json
|
|
15
|
15
|
import random
|
|
|
16
|
+import sys
|
|
|
17
|
+import pickle
|
|
|
18
|
+import urllib
|
|
16
|
19
|
|
|
|
20
|
+import memcache
|
|
17
|
21
|
import web
|
|
18
|
22
|
import bleach
|
|
19
|
23
|
from passlib.apps import custom_app_context as pwd_context
|
|
|
@@ -22,6 +26,8 @@ import strings
|
|
22
|
26
|
import utils
|
|
23
|
27
|
import version
|
|
24
|
28
|
|
|
|
29
|
+web.config.debug = False
|
|
|
30
|
+
|
|
25
|
31
|
# pylint: disable=redefined-builtin
|
|
26
|
32
|
# pylint: disable=redefined-outer-name
|
|
27
|
33
|
# pylint: disable=no-init
|
|
|
@@ -75,7 +81,7 @@ DEFAULTS = [
|
|
75
|
81
|
'dateformat': '%B %d, %Y',
|
|
76
|
82
|
'base': '/',
|
|
77
|
83
|
'extra_tags': '',
|
|
78
|
|
- 'limit': 50,
|
|
|
84
|
+ 'limit': 20,
|
|
79
|
85
|
'server_type': 'dev',
|
|
80
|
86
|
'user_fields': 'realname email homepage gravatar_email team location twitter facebook google_plus_ skype aim',
|
|
81
|
87
|
'history_days': 7,
|
|
|
@@ -99,6 +105,7 @@ DEFAULTS = [
|
|
99
|
105
|
|
|
100
|
106
|
counters = utils.Counters()
|
|
101
|
107
|
|
|
|
108
|
+
|
|
102
|
109
|
class Config(ConfigParser.RawConfigParser):
|
|
103
|
110
|
def set_defaults(self, defaults):
|
|
104
|
111
|
for section, items in defaults:
|
|
|
@@ -153,6 +160,43 @@ db = web.database(dbn='mysql',
|
|
153
|
160
|
db=config.get('db','db'),
|
|
154
|
161
|
)
|
|
155
|
162
|
|
|
|
163
|
+class DBCache:
|
|
|
164
|
+ def __init__(self, db):
|
|
|
165
|
+ self._db = db
|
|
|
166
|
+ self._cache = memcache.Client(['localhost:11211'],
|
|
|
167
|
+ pickleProtocol=pickle.HIGHEST_PROTOCOL)
|
|
|
168
|
+
|
|
|
169
|
+ def make_key(self, table, column, value, limit):
|
|
|
170
|
+ return '/'.join((
|
|
|
171
|
+ table,
|
|
|
172
|
+ column,
|
|
|
173
|
+ hashlib.md5(str(value)).hexdigest(),
|
|
|
174
|
+ str(limit)
|
|
|
175
|
+ ))
|
|
|
176
|
+
|
|
|
177
|
+ def select(self, table, column, value, limit=None):
|
|
|
178
|
+ key = self.make_key(table, column, value, limit)
|
|
|
179
|
+
|
|
|
180
|
+ got = self._cache.get(key)
|
|
|
181
|
+ if got is not None:
|
|
|
182
|
+ counters.bump('select_cache_hit')
|
|
|
183
|
+ return got
|
|
|
184
|
+ else:
|
|
|
185
|
+ got = list(self._db.select(table, where='%s = $value' % column,
|
|
|
186
|
+ vars={'value': value}, limit=limit))
|
|
|
187
|
+ counters.bump('select_cache_miss')
|
|
|
188
|
+ self._cache.set(key, got, time=600)
|
|
|
189
|
+ return got
|
|
|
190
|
+
|
|
|
191
|
+ def update(self, type, id, **kwargs):
|
|
|
192
|
+ table = '1_%ss' % type
|
|
|
193
|
+ column = '%sID' % type
|
|
|
194
|
+ key = self.make_key(table, column, id, 1)
|
|
|
195
|
+ self._db.update(table, where='%s = $id' % column, vars={'id': id}, **kwargs)
|
|
|
196
|
+ self._cache.delete(key)
|
|
|
197
|
+
|
|
|
198
|
+cache = DBCache(db)
|
|
|
199
|
+
|
|
156
|
200
|
def require_feature(name):
|
|
157
|
201
|
if not features[name]:
|
|
158
|
202
|
raise web.notfound()
|
|
|
@@ -183,7 +227,7 @@ class JSONMapper:
|
|
183
|
227
|
if value != getattr(self, key):
|
|
184
|
228
|
self._values[key] = value
|
|
185
|
229
|
encoded = json.dumps(self._values)
|
|
186
|
|
- db.update('1_users', where='userID = $id', contacts=encoded, vars={'id': self._around.userID})
|
|
|
230
|
+ cache.update('user', self._around.userID, contacts=encoded)
|
|
187
|
231
|
|
|
188
|
232
|
class AutoMapper:
|
|
189
|
233
|
def __init__(self, type, around):
|
|
|
@@ -220,7 +264,7 @@ class AutoMapper:
|
|
220
|
264
|
assert self._type
|
|
221
|
265
|
key = '%sID' % self._type
|
|
222
|
266
|
|
|
223
|
|
- rows = db.select(table, where='%s = $id' % key, vars={'id': getattr(self, key)})
|
|
|
267
|
+ rows = cache.select(table, key, getattr(self, key))
|
|
224
|
268
|
return [AutoMapper(singular, x) for x in rows]
|
|
225
|
269
|
|
|
226
|
270
|
raise AttributeError(name)
|
|
|
@@ -255,7 +299,7 @@ def first_or_none(type, column, id, strict=False):
|
|
255
|
299
|
no match.
|
|
256
|
300
|
"""
|
|
257
|
301
|
table = '1_%ss' % type
|
|
258
|
|
- vs = db.select(table, where='%s = $id' % column, vars={'id': id}, limit=1)
|
|
|
302
|
+ vs = cache.select(table, column, id, limit=1)
|
|
259
|
303
|
|
|
260
|
304
|
if len(vs):
|
|
261
|
305
|
return AutoMapper(type, vs[0])
|
|
|
@@ -322,7 +366,9 @@ class Model:
|
|
322
|
366
|
|
|
323
|
367
|
def get_user_by_name(self, name):
|
|
324
|
368
|
"""Get a user by user name"""
|
|
325
|
|
- return first_or_none('user', 'username', name)
|
|
|
369
|
+ name = urllib.unquote(name)
|
|
|
370
|
+ user = first('user', 'username', name)
|
|
|
371
|
+ return first('user', 'userID', user.userID)
|
|
326
|
372
|
|
|
327
|
373
|
def get_gravatar(self, email):
|
|
328
|
374
|
"""Get the gravatar hash for an email"""
|
|
|
@@ -685,7 +731,7 @@ class hide_link:
|
|
685
|
731
|
need_admin(_('Admin needed to hide a link'))
|
|
686
|
732
|
|
|
687
|
733
|
next = not link.hidden
|
|
688
|
|
- db.update('1_links', where='linkID = $id', hidden=next, vars={'id': id})
|
|
|
734
|
+ cache.update('link', id, hidden=next)
|
|
689
|
735
|
|
|
690
|
736
|
model.inform(_("Link is hidden") if next else _("Link now shows"))
|
|
691
|
737
|
redirect('/link/%s' % id)
|
|
|
@@ -697,7 +743,7 @@ class close_link:
|
|
697
|
743
|
|
|
698
|
744
|
need_admin(_('Admin needed to close a link'))
|
|
699
|
745
|
next = not link.closed
|
|
700
|
|
- db.update('1_links', where='linkID = $id', closed=next, vars={'id': id})
|
|
|
746
|
+ cache.update('link', id, closed=next)
|
|
701
|
747
|
|
|
702
|
748
|
model.inform(_("Link is closed") if next else _("Link is open"))
|
|
703
|
749
|
redirect('/link/%s' % id)
|
|
|
@@ -769,21 +815,21 @@ class like_comment:
|
|
769
|
815
|
class user:
|
|
770
|
816
|
def GET(self, id):
|
|
771
|
817
|
counters.bump(self)
|
|
772
|
|
- user = first('user', 'username', id)
|
|
773
|
|
- return render.user(user)
|
|
|
818
|
+ target = model.get_user_by_name(id)
|
|
|
819
|
+ return render.user(target)
|
|
774
|
820
|
|
|
775
|
821
|
class user_links:
|
|
776
|
822
|
def GET(self, id):
|
|
777
|
823
|
counters.bump(self)
|
|
778
|
|
- user = first('user', 'username', id)
|
|
779
|
|
- return render_links(where='userID=$id', vars={'id': user.userID})
|
|
|
824
|
+ target = model.get_user_by_name(id)
|
|
|
825
|
+ return render_links(where='userID=$id', vars={'id': target.userid})
|
|
780
|
826
|
|
|
781
|
827
|
class user_comments:
|
|
782
|
828
|
def GET(self, id):
|
|
783
|
829
|
counters.bump(self)
|
|
784
|
|
- user = first('user', 'username', id)
|
|
|
830
|
+ userid = first('user', 'username', id).userID
|
|
785
|
831
|
comments = db.select('1_comments', where='userID=$id', order='timestamp DESC',
|
|
786
|
|
- vars={'id': user.userID},
|
|
|
832
|
+ vars={'id': userid},
|
|
787
|
833
|
limit=config.get('general', 'limit'))
|
|
788
|
834
|
return render.user_comments([AutoMapper('comment', x) for x in comments])
|
|
789
|
835
|
|
|
|
@@ -874,8 +920,7 @@ class password:
|
|
874
|
920
|
form.note = _('Bad password')
|
|
875
|
921
|
return render.password(form)
|
|
876
|
922
|
|
|
877
|
|
- db.update('1_users', password=pwd_context.encrypt(form.d.new_password), where='userID=$id', vars={'id': target.userID})
|
|
878
|
|
-
|
|
|
923
|
+ cache.update('user', target.userID, password=pwd_context.encrypt(form.d.new_password))
|
|
879
|
924
|
model.inform(_("Password changed"))
|
|
880
|
925
|
redirect('/user/%s' % name)
|
|
881
|
926
|
|
|
|
@@ -917,12 +962,12 @@ class user_edit:
|
|
917
|
962
|
|
|
918
|
963
|
for name in names:
|
|
919
|
964
|
if target.has(name):
|
|
920
|
|
- db.update('1_users', where='userID = $id', vars={'id': target.userID}, **{name: form[name].value})
|
|
|
965
|
+ cache.update('user', target.userID, **{name: form[name].value})
|
|
921
|
966
|
else:
|
|
922
|
967
|
values.set(name, form[name].value)
|
|
923
|
968
|
|
|
924
|
969
|
bio = render_input(form.d.bio)
|
|
925
|
|
- db.update('1_users', bio=bio, where='userID = $id', vars={'id': target.userID})
|
|
|
970
|
+ cache.update('user', target.userID, bio=bio)
|
|
926
|
971
|
redirect('/user/%s' % username)
|
|
927
|
972
|
|
|
928
|
973
|
|
|
|
@@ -939,7 +984,7 @@ class rss:
|
|
939
|
984
|
class debug_counters:
|
|
940
|
985
|
def GET(self):
|
|
941
|
986
|
counters.bump(self)
|
|
942
|
|
- need_admin('Only admins can access server status pages.')
|
|
|
987
|
+ need_admin(_('Only admins can access debug pages.'))
|
|
943
|
988
|
web.header('Content-Type', 'application/json')
|
|
944
|
989
|
return json.dumps(counters.get_snapshot())
|
|
945
|
990
|
|