Compare commits
14 commits
Author | SHA1 | Date | |
---|---|---|---|
Michael Hope | b604cce31c | ||
Michael Hope | a1e04d5834 | ||
Michael Hope | 79e47bc39a | ||
Michael Hope | ded2046179 | ||
Michael Hope | 5fd6e981e9 | ||
Michael Hope | 4cee16ba96 | ||
Michael Hope | 71e78fbb8e | ||
Michael Hope | 278193dc5a | ||
Michael Hope | 14ed4b9d1f | ||
b99a317835 | |||
a376aba8a6 | |||
e928eae275 | |||
1cd4b58142 | |||
05989ab593 |
14
Makefile
14
Makefile
|
@ -1,14 +0,0 @@
|
|||
all: version.py
|
||||
|
||||
version.py: dummy
|
||||
echo __version__ = \'$(shell git describe --always --dirty --long)\' > $@
|
||||
|
||||
dummy:
|
||||
|
||||
DB=$(shell cat niche.ini | grep ^db= | cut -d= -f2)
|
||||
|
||||
import: $(wildcard mofi-*.sql.gz)
|
||||
zcat $< \
|
||||
| sed -r "s# CHARSET\=latin1# CHARSET\=utf8#g" \
|
||||
| sed "s#ENGINE=MyISAM##" \
|
||||
| mysql -u root -p $(DB)
|
28
NOTES.txt
28
NOTES.txt
|
@ -18,3 +18,31 @@ mysql -u niche -p niche < schema.sql
|
|||
Sanitizing
|
||||
----------
|
||||
bleach looks fine https://github.com/jsocol/bleach
|
||||
|
||||
Performance
|
||||
-----------
|
||||
All with niche-dev running a local server.
|
||||
|
||||
base: 1592 ms
|
||||
web.config.debug = False: 841 ms
|
||||
Pre-compiled templates: 848 ms
|
||||
Basic MySQL caching: ~366 ms
|
||||
|
||||
cProfile doesn't do multiple threads.
|
||||
|
||||
HTML templates: 366 ms
|
||||
Precompiled templates: 360 ms (no advantage)
|
||||
|
||||
Baseline: 366 ms
|
||||
Basic caching: 228 ms
|
||||
Binary pickle: 221 ms
|
||||
|
||||
Before launch
|
||||
-------------
|
||||
Error and access logging: DONE
|
||||
Check /static/ is served by nginx: DONE
|
||||
Check gzip effects: DONE
|
||||
Way of logging bytes transferred:
|
||||
error.log has no SQL in it: DONE
|
||||
memcached in monit: DONE
|
||||
monit emails sending: DONE
|
||||
|
|
0
niche/__init__.py
Normal file
0
niche/__init__.py
Normal file
4
niche/__main__.py
Normal file
4
niche/__main__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from . import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.main()
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import datetime
|
||||
import hashlib
|
||||
import ConfigParser
|
||||
import configparser
|
||||
import passlib
|
||||
import markdown
|
||||
import re
|
||||
|
@ -12,16 +12,17 @@ import json
|
|||
import random
|
||||
import sys
|
||||
import pickle
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import os.path
|
||||
|
||||
import memcache
|
||||
import web
|
||||
import bleach
|
||||
from passlib.apps import custom_app_context as pwd_context
|
||||
|
||||
import strings
|
||||
import utils
|
||||
import version
|
||||
from . import strings
|
||||
from . import utils
|
||||
from . import version
|
||||
|
||||
web.config.debug = False
|
||||
|
||||
|
@ -30,32 +31,56 @@ web.config.debug = False
|
|||
# pylint: disable=no-init
|
||||
|
||||
urls = (
|
||||
r'/?', 'index',
|
||||
r'/links(/\d+)?(/\d+)?(/\d+)?', 'links',
|
||||
r'/link/new', 'new_link',
|
||||
r'/link/(\d+)', 'link',
|
||||
r'/link/(\d+)/hide', 'hide_link',
|
||||
r'/link/(\d+)/close', 'close_link',
|
||||
r'/link/(\d+)/new', 'new_comment',
|
||||
r'/comment/(\d+)/delete', 'delete_comment',
|
||||
r'/comment/(\d+)/like', 'like_comment',
|
||||
r'/user/([^/]+)', 'user',
|
||||
r'/user/([^/]+)/links', 'user_links',
|
||||
r'/user/([^/]+)/comments', 'user_comments',
|
||||
r'/user/([^/]+)/checkout', 'checkout',
|
||||
r'/user/([^/]+)/password', 'password',
|
||||
r'/user/([^/]+)/edit', 'user_edit',
|
||||
r'/login', 'login',
|
||||
r'/logout', 'logout',
|
||||
r'/rss', 'rss',
|
||||
r'/debug/counters', 'debug_counters',
|
||||
r'/debug/diediedie', 'debug_die',
|
||||
r'/?',
|
||||
'index',
|
||||
r'/links(/\d+)?(/\d+)?(/\d+)?',
|
||||
'links',
|
||||
r'/link/new',
|
||||
'new_link',
|
||||
r'/link/(\d+)',
|
||||
'link',
|
||||
r'/link/(\d+)/hide',
|
||||
'hide_link',
|
||||
r'/link/(\d+)/close',
|
||||
'close_link',
|
||||
r'/link/(\d+)/new',
|
||||
'new_comment',
|
||||
r'/comment/(\d+)/delete',
|
||||
'delete_comment',
|
||||
r'/comment/(\d+)/like',
|
||||
'like_comment',
|
||||
r'/user/([^/]+)',
|
||||
'user',
|
||||
r'/user/([^/]+)/links',
|
||||
'user_links',
|
||||
r'/user/([^/]+)/comments',
|
||||
'user_comments',
|
||||
r'/user/([^/]+)/checkout',
|
||||
'checkout',
|
||||
r'/user/([^/]+)/password',
|
||||
'password',
|
||||
r'/user/([^/]+)/edit',
|
||||
'user_edit',
|
||||
r'/login',
|
||||
'login',
|
||||
r'/logout',
|
||||
'logout',
|
||||
r'/rss',
|
||||
'rss',
|
||||
r'/debug/counters',
|
||||
'debug_counters',
|
||||
r'/debug/diediedie',
|
||||
'debug_die',
|
||||
|
||||
# MonkeyFilter compatible URLs.
|
||||
r'/link\.php/(\d+)', 'link',
|
||||
r'/user\.php/([^/]+)', 'user',
|
||||
r'/rss.php', 'rss',
|
||||
r'/rss.xml', 'rss',
|
||||
r'/link\.php/(\d+)',
|
||||
'link',
|
||||
r'/user\.php/([^/]+)',
|
||||
'user',
|
||||
r'/rss.php',
|
||||
'rss',
|
||||
r'/rss.xml',
|
||||
'rss',
|
||||
)
|
||||
|
||||
ALLOWED_TAGS = """
|
||||
|
@ -75,41 +100,51 @@ ALLOWED_ATTRIBUTES = {
|
|||
# Default configuration.
|
||||
DEFAULTS = [
|
||||
('general', {
|
||||
'dateformat': '%B %d, %Y',
|
||||
'base': '/',
|
||||
'extra_tags': '',
|
||||
'limit': 20,
|
||||
'server_type': 'dev',
|
||||
'user_fields': ('realname email homepage gravatar_email '
|
||||
'team location twitter facebook '
|
||||
'google_plus_ skype aim'),
|
||||
'history_days': 7,
|
||||
}),
|
||||
'dateformat':
|
||||
'%B %d, %Y',
|
||||
'base':
|
||||
'/',
|
||||
'has_https':
|
||||
False,
|
||||
'extra_tags':
|
||||
'',
|
||||
'limit':
|
||||
20,
|
||||
'server_type':
|
||||
'dev',
|
||||
'user_fields': ('realname email homepage gravatar_email '
|
||||
'team location twitter facebook '
|
||||
'google_plus_ skype aim'),
|
||||
'history_days':
|
||||
7,
|
||||
'templates':
|
||||
'templates',
|
||||
}),
|
||||
('groups', {
|
||||
'admins': '',
|
||||
}),
|
||||
'admins': '',
|
||||
}),
|
||||
('db', {
|
||||
'db': 'niche',
|
||||
'user': 'niche',
|
||||
'password': 'whatever',
|
||||
}),
|
||||
'db': 'niche',
|
||||
'user': 'niche',
|
||||
'password': 'whatever',
|
||||
}),
|
||||
('cache', {
|
||||
'host': 'localhost:11211',
|
||||
'max_age': 15,
|
||||
}),
|
||||
'host': 'localhost:11211',
|
||||
'max_age': 15,
|
||||
}),
|
||||
('site', {
|
||||
'name': 'Nichefilter',
|
||||
'subtitle': 'of no fixed subtitle',
|
||||
'contact': None,
|
||||
'license': None,
|
||||
'secret': '',
|
||||
}),
|
||||
]
|
||||
'name': 'Nichefilter',
|
||||
'subtitle': 'of no fixed subtitle',
|
||||
'contact': None,
|
||||
'license': None,
|
||||
'secret': '',
|
||||
}),
|
||||
]
|
||||
|
||||
counters = utils.Counters()
|
||||
|
||||
|
||||
class Config(ConfigParser.RawConfigParser):
|
||||
class Config(configparser.RawConfigParser):
|
||||
def set_defaults(self, defaults):
|
||||
for section, items in defaults:
|
||||
self.add_section(section)
|
||||
|
@ -128,6 +163,7 @@ def read_config():
|
|||
cfg.read('niche.ini')
|
||||
return cfg
|
||||
|
||||
|
||||
config = read_config()
|
||||
|
||||
FEATURES = 'likes gravatar rss checkout'.split()
|
||||
|
@ -143,6 +179,7 @@ def get_features(config):
|
|||
|
||||
return features
|
||||
|
||||
|
||||
features = get_features(config)
|
||||
|
||||
|
||||
|
@ -161,13 +198,15 @@ def get_string(id):
|
|||
id = re.sub(r'_$', '', id)
|
||||
return strings.__dict__[id]
|
||||
|
||||
|
||||
_ = get_string
|
||||
|
||||
db = web.database(dbn='mysql',
|
||||
user=config.get('db', 'user'),
|
||||
pw=config.get('db', 'password'),
|
||||
db=config.get('db', 'db'),
|
||||
)
|
||||
db = web.database(
|
||||
dbn='mysql',
|
||||
user=config.get('db', 'user'),
|
||||
pw=config.get('db', 'password'),
|
||||
db=config.get('db', 'db'),
|
||||
)
|
||||
|
||||
|
||||
class DBCache:
|
||||
|
@ -183,12 +222,12 @@ class DBCache:
|
|||
|
||||
def make_key(self, table, column, value, limit):
|
||||
return '/'.join((
|
||||
self._prefix,
|
||||
table,
|
||||
column,
|
||||
hashlib.md5(str(value)).hexdigest(),
|
||||
str(limit)
|
||||
))
|
||||
self._prefix,
|
||||
table,
|
||||
column,
|
||||
hashlib.md5(str(value).encode('utf-8')).hexdigest(),
|
||||
str(limit),
|
||||
))
|
||||
|
||||
def select(self, table, column, value, limit=None):
|
||||
dirty = self.make_dirty_key(table)
|
||||
|
@ -199,8 +238,11 @@ class DBCache:
|
|||
counters.bump('select_cache_hit')
|
||||
return got[elem]
|
||||
else:
|
||||
rows = list(self._db.select(table, where='%s = $value' % column,
|
||||
vars={'value': value}, limit=limit))
|
||||
rows = list(
|
||||
self._db.select(table,
|
||||
where='%s = $value' % column,
|
||||
vars={'value': value},
|
||||
limit=limit))
|
||||
counters.bump('select_cache_miss')
|
||||
self._cache.set(elem, rows, time=self._max_age)
|
||||
return rows
|
||||
|
@ -211,7 +253,8 @@ class DBCache:
|
|||
dirty = self.make_dirty_key(table)
|
||||
self._db.update(table,
|
||||
where='%s = $id' % column,
|
||||
vars={'id': id}, **kwargs)
|
||||
vars={'id': id},
|
||||
**kwargs)
|
||||
self._cache.set(dirty, 1)
|
||||
|
||||
def insert(self, type, **kwargs):
|
||||
|
@ -221,6 +264,7 @@ class DBCache:
|
|||
self._cache.set(dirty, 1)
|
||||
return result
|
||||
|
||||
|
||||
cache = DBCache(db,
|
||||
host=config.get('cache', 'host'),
|
||||
max_age=config.get('cache', 'max_age'),
|
||||
|
@ -235,6 +279,7 @@ def require_feature(name):
|
|||
def now():
|
||||
return time.time()
|
||||
|
||||
|
||||
fallbacks = {
|
||||
'user': web.utils.Storage(username='anonymous'),
|
||||
}
|
||||
|
@ -295,9 +340,8 @@ class AutoMapper:
|
|||
field = name[:-6]
|
||||
query = ('SELECT COUNT(*) AS total FROM 1_%ss '
|
||||
'WHERE %sID = $id') % (field, self._type)
|
||||
results = db.query(
|
||||
query,
|
||||
vars={'id': getattr(self, '%sID' % self._type)})
|
||||
results = db.query(query,
|
||||
vars={'id': getattr(self, '%sID' % self._type)})
|
||||
return results[0].total
|
||||
|
||||
if name.endswith('s'):
|
||||
|
@ -374,9 +418,9 @@ def render_input(v, use_markdown=False):
|
|||
return v
|
||||
|
||||
if use_markdown:
|
||||
return bleach.clean(
|
||||
markdown.markdown(v, output_format='html5'),
|
||||
tags=tags, attributes=attrs)
|
||||
return bleach.clean(markdown.markdown(v, output_format='html5'),
|
||||
tags=tags,
|
||||
attributes=attrs)
|
||||
else:
|
||||
v = bleach.clean(v, tags=tags, attributes=attrs)
|
||||
out = ''
|
||||
|
@ -421,13 +465,13 @@ class Model:
|
|||
|
||||
def get_user_by_name(self, name):
|
||||
"""Get a user by user name"""
|
||||
name = urllib.unquote(name)
|
||||
name = urllib.parse.unquote_plus(name)
|
||||
user = first('user', 'username', name)
|
||||
return first('user', 'userID', user.userID)
|
||||
|
||||
def get_gravatar(self, email):
|
||||
"""Get the gravatar hash for an email"""
|
||||
return hashlib.md5(email.strip().lower()).hexdigest()
|
||||
return hashlib.md5(email.strip().lower().encode('utf-8')).hexdigest()
|
||||
|
||||
def get_message(self):
|
||||
"""Get the message for the user, if any, and clear"""
|
||||
|
@ -453,6 +497,9 @@ class Model:
|
|||
|
||||
return first_or_none('user', 'userID', id)
|
||||
|
||||
def is_active(self):
|
||||
return session.get('userID', None) is not None
|
||||
|
||||
def paginate(self, offset, total, per_page):
|
||||
# TODO(michaelh): really a helper, not part of the model.
|
||||
page = 1 + offset // per_page
|
||||
|
@ -462,13 +509,12 @@ class Model:
|
|||
if pages / step <= 6:
|
||||
break
|
||||
if pages > 1:
|
||||
indexes = set(
|
||||
[1, 2, pages, pages-1, page]
|
||||
+ range(step, pages, step))
|
||||
indexes = set([1, 2, pages, pages - 1, page] +
|
||||
list(range(step, pages, step)))
|
||||
if page > 1:
|
||||
indexes.add(page-1)
|
||||
indexes.add(page - 1)
|
||||
if page < pages:
|
||||
indexes.add(page+1)
|
||||
indexes.add(page + 1)
|
||||
else:
|
||||
indexes = []
|
||||
return page, sorted(indexes)
|
||||
|
@ -489,13 +535,15 @@ class Model:
|
|||
return '%d %ss' % (value, name)
|
||||
|
||||
def get_new(self):
|
||||
since = now() - 60*60*24*config.get('general', 'history_days')
|
||||
comments = db.select(
|
||||
'1_comments',
|
||||
where='timestamp >= $since AND userID <> $user',
|
||||
order='timestamp ASC', limit=50,
|
||||
vars={'since': since,
|
||||
'user': session.get('userID', None)})
|
||||
since = now() - 60 * 60 * 24 * config.get('general', 'history_days')
|
||||
comments = db.select('1_comments',
|
||||
where='timestamp >= $since AND userID <> $user',
|
||||
order='timestamp ASC',
|
||||
limit=50,
|
||||
vars={
|
||||
'since': since,
|
||||
'user': session.get('userID', None)
|
||||
})
|
||||
# Pull out the unique links.
|
||||
ids = {}
|
||||
for comment in comments:
|
||||
|
@ -504,6 +552,7 @@ class Model:
|
|||
comments = sorted(ids.values(), key=lambda x: x.linkID)
|
||||
return [AutoMapper('comment', x) for x in comments]
|
||||
|
||||
|
||||
model = Model()
|
||||
|
||||
render_globals = {
|
||||
|
@ -516,15 +565,15 @@ render_globals = {
|
|||
}
|
||||
|
||||
render = web.template.render(
|
||||
'templates/',
|
||||
config.get('general', 'templates'),
|
||||
base='layout',
|
||||
globals=render_globals,
|
||||
)
|
||||
)
|
||||
|
||||
naked_render = web.template.render(
|
||||
'templates/',
|
||||
config.get('general', 'templates'),
|
||||
globals=render_globals,
|
||||
)
|
||||
)
|
||||
|
||||
app = web.application(urls, locals())
|
||||
|
||||
|
@ -532,12 +581,10 @@ app = web.application(urls, locals())
|
|||
def get_csrf():
|
||||
token = session.get('csrf_token', None)
|
||||
if token is None:
|
||||
token = hashlib.md5(''.join((
|
||||
str(random.randrange(0, 2**20)),
|
||||
config.get('site', 'secret'),
|
||||
config.get('db', 'db'),
|
||||
config.get('db', 'user')))
|
||||
).hexdigest()
|
||||
key = ''.join(
|
||||
(str(random.randrange(0, 2**20)), config.get('site', 'secret'),
|
||||
config.get('db', 'db'), config.get('db', 'user')))
|
||||
token = hashlib.md5(key.encode('utf-8')).hexdigest()
|
||||
session.csrf_token = token
|
||||
return token
|
||||
|
||||
|
@ -566,6 +613,7 @@ class CSRFInput(web.form.Hidden):
|
|||
def validate(self, value):
|
||||
return check_csrf(value)
|
||||
|
||||
|
||||
TEXT_SIZE = 80
|
||||
TEXT_MAX_LENGTH = 150
|
||||
|
||||
|
@ -584,21 +632,21 @@ def tidy_form(form):
|
|||
def make_session():
|
||||
"""Helper that makes the session object, even if in debug mode."""
|
||||
if web.config.get('_session') is None:
|
||||
session = web.session.Session(app, web.session.DiskStore('sessions'),
|
||||
initializer={'message': None}
|
||||
)
|
||||
session = web.session.Session(app,
|
||||
web.session.DiskStore('sessions'),
|
||||
initializer={'message': None})
|
||||
web.config._session = session
|
||||
else:
|
||||
session = web.config._session
|
||||
|
||||
return session
|
||||
|
||||
|
||||
session = make_session()
|
||||
|
||||
# Validate a password. Pretty lax.
|
||||
password_validator = web.form.Validator(
|
||||
_("Short password"),
|
||||
lambda x: len(x) >= 3)
|
||||
password_validator = web.form.Validator(_("Short password"),
|
||||
lambda x: len(x) >= 3)
|
||||
|
||||
|
||||
def url_validator(v):
|
||||
|
@ -610,7 +658,19 @@ def url_validator(v):
|
|||
|
||||
def redirect(url):
|
||||
"""Bounce to a different site absolute URL."""
|
||||
raise web.seeother(url)
|
||||
has_https = config.get('general', 'has_https')
|
||||
if has_https and model.is_active():
|
||||
base = config.get('general', 'base')
|
||||
if base.endswith('/') and url.startswith('/'):
|
||||
path = base[:-1] + url
|
||||
else:
|
||||
path = base + url
|
||||
|
||||
raise web.seeother('https://{host}{path}'.format(host=web.ctx.host,
|
||||
path=path),
|
||||
absolute=True)
|
||||
else:
|
||||
raise web.seeother(url)
|
||||
|
||||
|
||||
def authenticate(msg=_("Login required")):
|
||||
|
@ -659,22 +719,24 @@ def render_links(where=None, span=None, vars=None, date_range=None):
|
|||
limit = int(input.get('limit', config.get('general', 'limit')))
|
||||
limit = max(0, min(200, limit))
|
||||
|
||||
links = db.select('1_links', where=where, vars=vars,
|
||||
limit=limit, offset=offset,
|
||||
links = db.select('1_links',
|
||||
where=where,
|
||||
vars=vars,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
order="timestamp DESC")
|
||||
|
||||
if where:
|
||||
results = db.query(
|
||||
'SELECT COUNT(*) AS total FROM 1_links WHERE %s' % where,
|
||||
vars=vars)
|
||||
results = db.query('SELECT COUNT(*) AS total FROM 1_links WHERE %s' %
|
||||
where,
|
||||
vars=vars)
|
||||
else:
|
||||
results = db.query("SELECT COUNT(*) AS total FROM 1_links")
|
||||
|
||||
total = results[0].total
|
||||
|
||||
return render.links(map_all('link', links), span,
|
||||
web.ctx.path, offset, limit,
|
||||
total, date_range)
|
||||
return render.links(map_all('link', links), span, web.ctx.path, offset,
|
||||
limit, total, date_range)
|
||||
|
||||
|
||||
class index:
|
||||
|
@ -733,18 +795,21 @@ class links:
|
|||
last = datetime.datetime.fromtimestamp(limits.last)
|
||||
|
||||
date_range = web.utils.Storage(
|
||||
years=range(first.year, last.year+1),
|
||||
years=list(range(first.year, last.year + 1)),
|
||||
year=None if no_year else year,
|
||||
month=None if no_month else month,
|
||||
months=calendar.month_name,
|
||||
)
|
||||
)
|
||||
|
||||
return render_links(
|
||||
where='timestamp >= $tstart and timestamp < $tend',
|
||||
vars={'tstart': tstart, 'tend': tend},
|
||||
vars={
|
||||
'tstart': tstart,
|
||||
'tend': tend
|
||||
},
|
||||
span=span,
|
||||
date_range=date_range,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class link:
|
||||
|
@ -758,22 +823,19 @@ class link:
|
|||
class new_link:
|
||||
form = web.form.Form(
|
||||
web.form.Textbox('title', web.form.notnull),
|
||||
web.form.Textbox('url', web.form.Validator(
|
||||
_("Not a URL"), url_validator)),
|
||||
web.form.Textbox('url',
|
||||
web.form.Validator(_("Not a URL"), url_validator)),
|
||||
web.form.Textbox('url_description'),
|
||||
web.form.Textarea('description', rows=5, cols=80),
|
||||
web.form.Textarea('extended', rows=5, cols=80),
|
||||
web.form.Checkbox('use_markdown', value='use_markdown'),
|
||||
CSRFInput(),
|
||||
validators=[
|
||||
web.form.Validator(
|
||||
_("URLs need a description"),
|
||||
lambda x: x.url_description if x.url else True),
|
||||
web.form.Validator(
|
||||
_("Need a URL or description"),
|
||||
lambda x: x.url or x.description),
|
||||
]
|
||||
)
|
||||
web.form.Validator(_("URLs need a description"),
|
||||
lambda x: x.url_description if x.url else True),
|
||||
web.form.Validator(_("Need a URL or description"),
|
||||
lambda x: x.url or x.description),
|
||||
])
|
||||
form = tidy_form(form)
|
||||
|
||||
def authenticate(self):
|
||||
|
@ -799,12 +861,11 @@ class new_link:
|
|||
extended = render_input(form.d.extended, markdown)
|
||||
|
||||
if 'preview' in web.input():
|
||||
preview = web.utils.Storage(
|
||||
title=form.d.title,
|
||||
URL=form.d.url,
|
||||
URL_description=url_description,
|
||||
description=description,
|
||||
extended=extended)
|
||||
preview = web.utils.Storage(title=form.d.title,
|
||||
URL=form.d.url,
|
||||
URL_description=url_description,
|
||||
description=description,
|
||||
extended=extended)
|
||||
|
||||
return render.new_link(form, preview)
|
||||
|
||||
|
@ -815,8 +876,7 @@ class new_link:
|
|||
URL=form.d.url,
|
||||
URL_description=url_description,
|
||||
description=description,
|
||||
extended=extended
|
||||
)
|
||||
extended=extended)
|
||||
|
||||
model.inform(_("New post success"))
|
||||
redirect('/link/%d' % next)
|
||||
|
@ -853,7 +913,7 @@ class new_comment:
|
|||
web.form.Textarea('comment', web.form.notnull, rows=5, cols=80),
|
||||
web.form.Checkbox('use_markdown', value='use_markdown'),
|
||||
CSRFInput(),
|
||||
)
|
||||
)
|
||||
form = tidy_form(form)
|
||||
|
||||
def check(self, id):
|
||||
|
@ -882,8 +942,7 @@ class new_comment:
|
|||
linkID=link.linkID,
|
||||
userID=user.userID,
|
||||
timestamp=now(),
|
||||
content=comment
|
||||
)
|
||||
content=comment)
|
||||
model.inform(_("New comment success"))
|
||||
redirect('/link/%d' % link.linkID)
|
||||
|
||||
|
@ -926,17 +985,18 @@ class user_links:
|
|||
def GET(self, id):
|
||||
counters.bump(self)
|
||||
target = model.get_user_by_name(id)
|
||||
return render_links(where='userID=$id', vars={'id': target.userid})
|
||||
return render_links(where='userID=$id', vars={'id': target.userID})
|
||||
|
||||
|
||||
class user_comments:
|
||||
def GET(self, id):
|
||||
counters.bump(self)
|
||||
userid = first('user', 'username', id).userID
|
||||
comments = db.select(
|
||||
'1_comments', where='userID=$id',
|
||||
order='timestamp DESC', vars={'id': userid},
|
||||
limit=config.get('general', 'limit'))
|
||||
comments = db.select('1_comments',
|
||||
where='userID=$id',
|
||||
order='timestamp DESC',
|
||||
vars={'id': userid},
|
||||
limit=config.get('general', 'limit'))
|
||||
return render.user_comments(
|
||||
[AutoMapper('comment', x) for x in comments])
|
||||
|
||||
|
@ -958,7 +1018,7 @@ class login:
|
|||
web.form.Textbox('username', web.form.notnull),
|
||||
web.form.Password('password', web.form.notnull),
|
||||
CSRFInput(),
|
||||
)
|
||||
)
|
||||
login = tidy_form(login)
|
||||
|
||||
def GET(self):
|
||||
|
@ -996,18 +1056,16 @@ class logout:
|
|||
|
||||
|
||||
class password:
|
||||
form = web.form.Form(
|
||||
web.form.Password('password', web.form.notnull),
|
||||
web.form.Password('new_password',
|
||||
web.form.notnull, password_validator),
|
||||
web.form.Password('again', web.form.notnull),
|
||||
CSRFInput(),
|
||||
validators=[
|
||||
web.form.Validator(
|
||||
_("Passwords don't match"),
|
||||
lambda x: x.new_password == x.again)
|
||||
]
|
||||
)
|
||||
form = web.form.Form(web.form.Password('password', web.form.notnull),
|
||||
web.form.Password('new_password', web.form.notnull,
|
||||
password_validator),
|
||||
web.form.Password('again', web.form.notnull),
|
||||
CSRFInput(),
|
||||
validators=[
|
||||
web.form.Validator(
|
||||
_("Passwords don't match"),
|
||||
lambda x: x.new_password == x.again)
|
||||
])
|
||||
form = tidy_form(form)
|
||||
|
||||
def authenticate(self, name):
|
||||
|
@ -1035,7 +1093,8 @@ class password:
|
|||
form.note = _('Bad password')
|
||||
return render.password(form)
|
||||
|
||||
cache.update('user', target.userID,
|
||||
cache.update('user',
|
||||
target.userID,
|
||||
password=pwd_context.encrypt(form.d.new_password))
|
||||
model.inform(_("Password changed"))
|
||||
redirect('/user/%s' % name)
|
||||
|
@ -1051,8 +1110,8 @@ class user_edit:
|
|||
return value if value else user.get(name)
|
||||
|
||||
fields = [web.form.Textbox(x, value=get(x), size=60) for x in names]
|
||||
fields.append(web.form.Textarea('bio',
|
||||
rows=5, cols=80, value=get('bio')))
|
||||
fields.append(
|
||||
web.form.Textarea('bio', rows=5, cols=80, value=get('bio')))
|
||||
fields.append(CSRFInput())
|
||||
return tidy_form(web.form.Form(*fields))
|
||||
|
||||
|
@ -1126,5 +1185,6 @@ def main():
|
|||
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
2
niche/test_niche.py
Normal file
2
niche/test_niche.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
def test_loads():
|
||||
import niche
|
1
niche/version.py
Normal file
1
niche/version.py
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = 'r1-5-gb99a317-dirty'
|
30
setup.py
Normal file
30
setup.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
import glob
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='niche',
|
||||
version_format='{tag}.dev{commitcount}+{gitsha}',
|
||||
setup_requires=['setuptools-git-version'],
|
||||
url='https://juju.nz/src/juju/niche',
|
||||
author='Michael Hope',
|
||||
author_email='michaelh@juju.nz',
|
||||
description='A community web log for monkey niches.',
|
||||
zip_safe=False,
|
||||
packages=find_packages(),
|
||||
data_files=[
|
||||
('lib/niche/templates', glob.glob('templates/*')),
|
||||
('lib/niche/static/themes/common',
|
||||
glob.glob('static/themes/common/*')),
|
||||
('lib/niche/static/themes/able-wpcom',
|
||||
glob.glob('static/themes/able-wpcom/*')),
|
||||
],
|
||||
install_requires=[
|
||||
'passlib>=1.7.4',
|
||||
'bleach>=3.2.1',
|
||||
'markdown>=3.1.1',
|
||||
'python-memcached>=1.59',
|
||||
'web.py>=0.62',
|
||||
'pymysql>=0.9.3',
|
||||
],
|
||||
)
|
|
@ -31,7 +31,11 @@ $ active = model.get_active()
|
|||
<li><a href="user/$active.username">$active.username</a>
|
||||
<li><a href="logout">Logout</a>
|
||||
$else:
|
||||
<li><a href="login">Login</a>
|
||||
<li>
|
||||
$if config.get('general', 'has_https'):
|
||||
<a href="https://$config.get('general', 'domain')${base}login">Login</a>
|
||||
$else:
|
||||
<li><a href="login">Login</a>
|
||||
<li><a href="link/new">New post</a>
|
||||
<li><a href="links">Archive</a>
|
||||
</ul>
|
||||
|
|
|
@ -36,6 +36,7 @@ $for comment in comments:
|
|||
<li>
|
||||
<div class="comment" id="comment_$comment.commentID">
|
||||
$:comment.content
|
||||
</b></i>
|
||||
<footer>
|
||||
$ user = comment.user
|
||||
posted <a href="$here#comment_$comment.commentID">$comment.ago()</a> ago
|
||||
|
|
Loading…
Reference in a new issue