rewrite using NIH

pull/16/head
posativ 12 years ago
parent 440787ff67
commit a4514e1f91

@ -20,51 +20,29 @@
# #
# Isso a lightweight Disqus alternative # Isso a lightweight Disqus alternative
__version__ = '0.2' __version__ = '0.3'
import sys; reload(sys) import sys; reload(sys)
sys.setdefaultencoding('utf-8') # we only support UTF-8 and python 2.X :-) sys.setdefaultencoding('utf-8') # we only support UTF-8 and python 2.X :-)
import io import io
import json import json
import traceback
from os.path import join, dirname from os.path import dirname
from optparse import OptionParser, make_option, SUPPRESS_HELP from optparse import OptionParser, make_option, SUPPRESS_HELP
from itsdangerous import URLSafeTimedSerializer from itsdangerous import URLSafeTimedSerializer
from werkzeug.wsgi import SharedDataMiddleware from isso import admin, comment, db, migrate, wsgi
from werkzeug.routing import Map, Rule from isso.utils import determine, import_object, IssoEncoder
from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound, InternalServerError
from isso import admin, comment, db, migrate
from isso.utils import determine, import_object, RegexConverter, IssoEncoder
# override default json :func:`dumps`. # override default json :func:`dumps`.
_dumps = json.dumps _dumps = json.dumps
setattr(json, 'dumps', lambda obj, **kw: _dumps(obj, cls=IssoEncoder, **kw)) setattr(json, 'dumps', lambda obj, **kw: _dumps(obj, cls=IssoEncoder, **kw))
# yep. lazy.
url = lambda path, endpoint, methods: Rule(path, endpoint=endpoint, methods=methods)
url_map = Map([
# moderation panel
url('/', 'admin.login', ['GET', 'POST']),
url('/admin/', 'admin.index', ['GET', 'POST']),
# comment API, note that the client side quotes the URL, but this is
# actually unnecessary. PEP 333 aka WSGI always unquotes PATH_INFO.
url('/1.0/<re(".+"):path>/', 'comment.get', ['GET']),
url('/1.0/<re(".+"):path>/new', 'comment.create', ['POST']),
url('/1.0/<re(".+"):path>/<int:id>', 'comment.get', ['GET']),
url('/1.0/<re(".+"):path>/<int:id>', 'comment.modify', ['PUT', 'DELETE']),
url('/1.0/<re(".+"):path>/<int:id>/approve', 'comment.approve', ['PUT'])
], converters={'re': RegexConverter})
class Isso(object):
class Isso:
PRODUCTION = True PRODUCTION = True
SECRET = 'secret' SECRET = 'secret'
@ -75,6 +53,12 @@ class Isso:
HOST = 'http://localhost:8000/' HOST = 'http://localhost:8000/'
MAX_AGE = 15 * 60 MAX_AGE = 15 * 60
HTTP_STATUS_CODES = {
200: 'Ok', 201: 'Created', 202: 'Accepted', 301: 'Moved Permanently',
400: 'Bad Request', 404: 'Not Found', 403: 'Forbidden',
500: 'Internal Server Error',
}
def __init__(self, conf): def __init__(self, conf):
self.__dict__.update(dict((k, v) for k, v in conf.iteritems() if k.isupper())) self.__dict__.update(dict((k, v) for k, v in conf.iteritems() if k.isupper()))
@ -85,6 +69,22 @@ class Isso:
self.db = db.SQLite(self) self.db = db.SQLite(self)
self.markup = import_object(conf.get('MARKUP', 'isso.markup.Markdown'))(conf) self.markup = import_object(conf.get('MARKUP', 'isso.markup.Markdown'))(conf)
self.adapter = map(
lambda r: (wsgi.Rule(r[0]), r[1], r[2] if isinstance(r[2], list) else [r[2]]), [
# moderation panel
('/', admin.login, ['GET', 'POST']),
('/admin/', admin.index, ['GET', 'POST']),
# comment API, note that the client side quotes the URL, but this is
# actually unnecessary. PEP 333 aka WSGI always unquotes PATH_INFO.
('/1.0/<(.+?):path>/new', comment.create, 'POST'),
('/1.0/<(.+?):path>/<(int):id>', comment.get, 'GET'),
('/1.0/<(.+?):path>/<(int):id>', comment.modify, ['PUT', 'DELETE']),
('/1.0/<(.+?):path>/<(int):id>/approve', comment.approve, 'PUT'),
('/1.0/<(.+?):path>', comment.get, 'GET'),
])
def sign(self, obj): def sign(self, obj):
return self.signer.dumps(obj) return self.signer.dumps(obj)
@ -92,27 +92,71 @@ class Isso:
def unsign(self, obj): def unsign(self, obj):
return self.signer.loads(obj, max_age=self.MAX_AGE) return self.signer.loads(obj, max_age=self.MAX_AGE)
def dispatch(self, request, start_response): def status(self, code):
adapter = url_map.bind_to_environ(request.environ) return '%i %s' % (code, self.HTTP_STATUS_CODES[code])
def dispatch(self, path, method):
for rule, handler, methods in self.adapter:
if isinstance(methods, basestring):
methods = [methods, ]
if method not in methods:
continue
m = rule.match(path)
if m is not None:
return handler, m
else:
return (lambda app, environ, request, **kw: (404, 'Not Found', {}), {})
def wsgi(self, environ, start_response):
try: try:
endpoint, values = adapter.match() request = wsgi.Request(environ)
module, function = endpoint.split('.', 1) handler, kwargs = self.dispatch(environ['PATH_INFO'], request.method)
handler = getattr(globals()[module], function) code, body, headers = handler(self, environ, request, **kwargs)
return handler(self, request.environ, request, **values)
except NotFound, e: if code == 404:
return Response('Not Found', 404) try:
except HTTPException, e: code, body, headers = wsgi.sendfile(environ['PATH_INFO'], dirname(__file__))
return e except (IOError, OSError):
except InternalServerError, e: pass
return Response(e, 500)
if request == 'HEAD':
def wsgi_app(self, environ, start_response): body = ''
request = Request(environ)
response = self.dispatch(request, start_response) start_response(self.status(code), headers.items())
return response(environ, start_response) return body
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
traceback.print_exc(file=sys.stderr)
headers = [('Content-Type', 'text/html; charset=utf-8')]
start_response(self.status(500), headers)
return '<h1>' + self.status(500) + '</h1>'
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response) return self.wsgi(environ, start_response)
class ReverseProxied(object):
def __init__(self, app, prefix=None):
self.app = app
self.prefix = prefix if prefix is not None else ''
def __call__(self, environ, start_response):
script_name = environ.get('HTTP_X_SCRIPT_NAME', self.prefix)
if script_name:
environ['SCRIPT_NAME'] = script_name
path_info = environ['PATH_INFO']
if path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
return self.app(environ, start_response)
def main(): def main():
@ -122,7 +166,7 @@ def main():
make_option("--sqlite", dest="sqlite", metavar='FILE', default="/tmp/sqlite.db", make_option("--sqlite", dest="sqlite", metavar='FILE', default="/tmp/sqlite.db",
help="use SQLite3 database"), help="use SQLite3 database"),
make_option("--port", dest="port", default=8000, help="webserver port"), make_option("--port", dest="port", default=8000, help="webserver port"),
make_option("--test", dest="production", action="store_false", default=True, make_option("--debug", dest="production", action="store_false", default=True,
help=SUPPRESS_HELP), help=SUPPRESS_HELP),
] ]
@ -133,7 +177,7 @@ def main():
print 'isso', __version__ print 'isso', __version__
sys.exit(0) sys.exit(0)
app = Isso({'SQLITE': options.sqlite, 'PRODUCTION': options.production}) app = Isso({'SQLITE': options.sqlite, 'PRODUCTION': options.production, 'MODERATION': False})
if len(args) > 0 and args[0] == 'import': if len(args) > 0 and args[0] == 'import':
if len(args) < 2: if len(args) < 2:
@ -142,8 +186,8 @@ def main():
with io.open(args[1], encoding='utf-8') as fp: with io.open(args[1], encoding='utf-8') as fp:
migrate.disqus(app.db, fp.read()) migrate.disqus(app.db, fp.read())
else: else:
app = SharedDataMiddleware(app, { from wsgiref.simple_server import make_server
'/static': join(dirname(__file__), 'static'), httpd = make_server('127.0.0.1', 8080, app)
'/js': join(dirname(__file__), 'js')}) httpd.serve_forever()
run_simple('127.0.0.1', 8000, app, use_reloader=True)

@ -3,12 +3,12 @@
# Copyright 2012, Martin Zimmermann <info@posativ.org>. All rights reserved. # Copyright 2012, Martin Zimmermann <info@posativ.org>. All rights reserved.
# License: BSD Style, 2 clauses. see isso/__init__.py # License: BSD Style, 2 clauses. see isso/__init__.py
from werkzeug.utils import redirect
from werkzeug.wrappers import Response
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from itsdangerous import SignatureExpired, BadSignature from itsdangerous import SignatureExpired, BadSignature
from isso.wsgi import setcookie
mako = TemplateLookup(directories=['isso/templates'], input_encoding='utf-8') mako = TemplateLookup(directories=['isso/templates'], input_encoding='utf-8')
render = lambda template, **context: mako.get_template(template).render_unicode(**context) render = lambda template, **context: mako.get_template(template).render_unicode(**context)
@ -16,12 +16,14 @@ render = lambda template, **context: mako.get_template(template).render_unicode(
def login(app, environ, request): def login(app, environ, request):
if request.method == 'POST': if request.method == 'POST':
if request.form.get('secret') == app.SECRET: if request.form.getfirst('secret') == app.SECRET:
rdr = redirect('/admin/', 301) return 301, '', {
rdr.set_cookie('session-admin', app.signer.dumps('*'), max_age=app.MAX_AGE) 'Location': '/admin/',
return rdr 'Set-Cookie': setcookie('session-admin', app.signer.dumps('*'),
max_age=app.MAX_AGE, path='/')
}
return Response(render('login.mako'), content_type='text/html') return 200, render('login.mako').encode('utf-8'), {'Content-Type': 'text/html'}
def index(app, environ, request): def index(app, environ, request):
@ -29,7 +31,7 @@ def index(app, environ, request):
try: try:
app.unsign(request.cookies.get('session-admin', '')) app.unsign(request.cookies.get('session-admin', ''))
except (SignatureExpired, BadSignature): except (SignatureExpired, BadSignature):
return redirect('/') return 301, '', {'Location': '/'}
ctx = {'app': app, 'request': request} ctx = {'app': app, 'request': request}
return Response(render('admin.mako', **ctx), content_type='text/html') return 200, render('admin.mako', **ctx).encode('utf-8'), {'Content-Type': 'text/html'}

@ -6,50 +6,48 @@
import cgi import cgi
import urllib import urllib
from werkzeug.wrappers import Response
from werkzeug.exceptions import abort
from itsdangerous import SignatureExpired, BadSignature from itsdangerous import SignatureExpired, BadSignature
from isso import json, models, utils from isso import json, models, utils, wsgi
def create(app, environ, request, path): def create(app, environ, request, path):
if app.PRODUCTION and not utils.urlexists(app.HOST, '/' + path): if app.PRODUCTION and not utils.urlexists(app.HOST, '/' + path):
return abort(404) return 400, 'URL does not exist', {}
try: try:
comment = models.Comment.fromjson(request.data) comment = models.Comment.fromjson(request.data)
except ValueError: except ValueError as e:
return abort(400) return 400, unicode(e), {}
for attr in 'author', 'email', 'website': for attr in 'author', 'email', 'website':
if getattr(comment, attr) is not None: if getattr(comment, attr) is not None:
try: try:
setattr(comment, attr, cgi.escape(getattr(comment, attr))) setattr(comment, attr, cgi.escape(getattr(comment, attr)))
except AttributeError: except AttributeError:
abort(400) return 400, '', {}
try: try:
rv = app.db.add(path, comment) rv = app.db.add(path, comment)
except ValueError: except ValueError:
return abort(400) return 400, '', {}
md5 = rv.md5 md5 = rv.md5
rv.text = app.markup.convert(rv.text) rv.text = app.markup.convert(rv.text)
response = Response(json.dumps(rv), 202 if rv.pending else 201, content_type='application/json') return 202 if rv.pending else 201, json.dumps(rv), {
response.set_cookie('session-%s-%s' % (urllib.quote(path, ''), rv.id), 'Content-Type': 'application/json',
app.signer.dumps([path, rv.id, md5]), max_age=app.MAX_AGE) 'Set-Cookie': wsgi.setcookie('%s-%s' % (path, rv.id),
return response app.sign([path, rv.id, md5]), max_age=app.MAX_AGE, path='/')
}
def get(app, environ, request, path, id=None): def get(app, environ, request, path, id=None):
rv = list(app.db.retrieve(path)) if id is None else app.db.get(path, id) rv = list(app.db.retrieve(path)) if id is None else app.db.get(path, id)
if not rv: if not rv:
abort(404) return 400, '', {}
if request.args.get('plain', '0') == '0': if request.args.get('plain', '0') == '0':
if isinstance(rv, list): if isinstance(rv, list):
@ -58,46 +56,47 @@ def get(app, environ, request, path, id=None):
else: else:
rv.text = app.markup.convert(rv.text) rv.text = app.markup.convert(rv.text)
return Response(json.dumps(rv), 200, content_type='application/json') return 200, json.dumps(rv), {'Content-Type': 'application/json'}
def modify(app, environ, request, path, id): def modify(app, environ, request, path, id):
try: try:
rv = app.unsign(request.cookies.get('session-%s-%s' % (urllib.quote(path, ''), id), '')) rv = app.unsign(request.cookies.get('%s-%s' % (urllib.quote(path, ''), id), ''))
except (SignatureExpired, BadSignature): except (SignatureExpired, BadSignature) as e:
try: try:
rv = app.unsign(request.cookies.get('session-admin', '')) rv = app.unsign(request.cookies.get('admin', ''))
except (SignatureExpired, BadSignature): except (SignatureExpired, BadSignature):
return abort(403) return 403, '', {}
# verify checksum, mallory might skip cookie deletion when he deletes a comment # verify checksum, mallory might skip cookie deletion when he deletes a comment
if not (rv == '*' or rv[0:2] == [path, id] or app.db.get(path, id).md5 != rv[2]): if not (rv == '*' or rv[0:2] == [path, id] or app.db.get(path, id).md5 != rv[2]):
abort(403) return 403, '', {}
if request.method == 'PUT': if request.method == 'PUT':
try: try:
rv = app.db.update(path, id, models.Comment.fromjson(request.data)) rv = app.db.update(path, id, models.Comment.fromjson(request.data))
rv.text = app.markup.convert(rv.text) rv.text = app.markup.convert(rv.text)
return Response(json.dumps(rv), 200, content_type='application/json') return 200, json.dumps(rv), {'Content-Type': 'application/json'}
except ValueError as e: except ValueError as e:
return Response(unicode(e), 400) return 400, unicode(e), {}
if request.method == 'DELETE': if request.method == 'DELETE':
rv = app.db.delete(path, id) rv = app.db.delete(path, id)
response = Response(json.dumps(rv), 200, content_type='application/json') return 200, json.dumps(rv), {
response.delete_cookie('session-%s-%s' % (urllib.quote(path, ''), id)) 'Content-Type': 'application/json',
return response 'Set-Cookie': wsgi.setcookie(path + '-' + str(id), 'deleted', max_age=0, path='/')
}
def approve(app, environ, request, path, id): def approve(app, environ, request, path, id):
try: try:
if app.unsign(request.cookies.get('session-admin', '')) != '*': if app.unsign(request.cookies.get('admin', '')) != '*':
abort(403) return 403, '', {}
except (SignatureExpired, BadSignature): except (SignatureExpired, BadSignature):
abort(403) return 403, '', {}
app.db.activate(path, id) app.db.activate(path, id)
return Response(json.dumps(app.db.get(path, id)), 200, content_type='application/json') return 200, json.dumps(app.db.get(path, id)), {'Content-Type': 'application/json'}

@ -16,6 +16,9 @@
* zfill(argument, i): zero fill `argument` with `i` zeros * zfill(argument, i): zero fill `argument` with `i` zeros
*/ */
// var prefix = "/comments";
var prefix = "";
function read(cookie){ function read(cookie){
return(document.cookie.match('(^|; )' + cookie + '=([^;]*)') || 0)[2] return(document.cookie.match('(^|; )' + cookie + '=([^;]*)') || 0)[2]
@ -60,7 +63,7 @@ function create(data, func) {
return; return;
} }
$.ajax('POST', '/1.0/' + encodeURIComponent(window.location.pathname) + '/new', $.ajax('POST', prefix + '/1.0/' + encodeURIComponent(window.location.pathname) + '/new',
JSON.stringify(data), {'Content-Type': 'application/json'}).then(func); JSON.stringify(data), {'Content-Type': 'application/json'}).then(func);
}; };
@ -70,7 +73,7 @@ function modify(id, data, func) {
return; return;
} }
$.ajax('PUT', '/1.0/' + encodeURIComponent(window.location.pathname) + '/' + id, $.ajax('PUT', prefix + '/1.0/' + encodeURIComponent(window.location.pathname) + '/' + id,
JSON.stringify(data), {'Content-Type': 'application/json'}).then(func) JSON.stringify(data), {'Content-Type': 'application/json'}).then(func)
}; };
@ -169,14 +172,15 @@ function insert(post) {
.append('<span class="note">Kommentar muss noch freigeschaltet werden</span>'); .append('<span class="note">Kommentar muss noch freigeschaltet werden</span>');
} }
if (read('session-' + path + '-' + post['id'])) { if (read(path + '-' + post['id'])) {
$('#isso_' + post['id'] + '> footer > a:first-child') $('#isso_' + post['id'] + '> footer > a:first-child')
.after('<a class="delete" href="#">Löschen</a>') .after('<a class="delete" href="#">Löschen</a>')
.after('<a class="edit" href="#">Bearbeiten</a>'); .after('<a class="edit" href="#">Bearbeiten</a>');
// DELETE // DELETE
$('#isso_' + post['id'] + ' > footer .delete').on('click', function(event) { $('#isso_' + post['id'] + ' > footer .delete').on('click', function(event) {
$.ajax('DELETE', '/1.0/' + path + '/' + post['id']).then(function(status, rv) { $.ajax('DELETE', prefix + '/1.0/' + path + '/' + post['id'])
.then(function(status, rv) {
// XXX comment might not actually deleted // XXX comment might not actually deleted
$('#isso_' + post['id']).remove(); $('#isso_' + post['id']).remove();
}); });
@ -188,7 +192,7 @@ function insert(post) {
if ($('#issoform_' + post['id']).length == 0) { if ($('#issoform_' + post['id']).length == 0) {
$.ajax('GET', '/1.0/' + path + '/' + post['id'], {'plain': '1'}) $.ajax('GET', prefix + '/1.0/' + path + '/' + post['id'], {'plain': '1'})
.then(function(status, rv) { .then(function(status, rv) {
rv = JSON.parse(rv); rv = JSON.parse(rv);
form(post['id'], form(post['id'],
@ -285,7 +289,8 @@ function initialize(thread) {
function fetch(thread) { function fetch(thread) {
$.ajax('GET', '/1.0/' + encodeURIComponent(window.location.pathname) + '/', console.log(window.location.pathname);
$.ajax('GET', prefix + '/1.0/' + encodeURIComponent(window.location.pathname),
{}, {'Content-Type': 'application/json'}).then(function(status, rv) { {}, {'Content-Type': 'application/json'}).then(function(status, rv) {
if (status != 200) { if (status != 200) {

@ -17,7 +17,7 @@
def get(name, convert): def get(name, convert):
limit = request.args.get(name) limit = request.args.get(name)
return convert(limit) if limit is not None else None return convert(limit[0]) if limit is not None else None
%> %>
<%block name="title"> <%block name="title">

@ -6,10 +6,9 @@
import json import json
import socket import socket
import httplib import httplib
import urlparse
import contextlib import contextlib
import werkzeug.routing from urlparse import urlparse
from isso.models import Comment from isso.models import Comment
@ -23,12 +22,6 @@ class IssoEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
class RegexConverter(werkzeug.routing.BaseConverter):
def __init__(self, url_map, *items):
super(RegexConverter, self).__init__(url_map)
self.regex = items[0]
def urlexists(host, path): def urlexists(host, path):
with contextlib.closing(httplib.HTTPConnection(host)) as con: with contextlib.closing(httplib.HTTPConnection(host)) as con:
try: try:
@ -43,7 +36,7 @@ def determine(host):
if not host.startswith(('http://', 'https://')): if not host.startswith(('http://', 'https://')):
host = 'http://' + host host = 'http://' + host
rv = urlparse.urlparse(host) rv = urlparse(host)
return (rv.netloc + ':443') if rv.scheme == 'https' else rv.netloc return (rv.netloc + ':443') if rv.scheme == 'https' else rv.netloc

@ -0,0 +1,121 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import io
import os
import re
import cgi
import tempfile
import urlparse
import mimetypes
from Cookie import SimpleCookie
from urllib import quote
class Request(object):
def __init__(self, environ):
# from bottle.py Copyright 2012, Marcel Hellkamp, License: MIT.
maxread = max(0, int(environ['CONTENT_LENGTH'] or '0'))
stream = environ['wsgi.input']
body = tempfile.TemporaryFile(mode='w+b')
while maxread > 0:
part = stream.read(maxread)
if not part:
break
body.write(part)
maxread -= len(part)
self.body = body
self.body.seek(0)
self.environ = environ
self.query_string = self.environ['QUERY_STRING']
@property
def data(self):
self.body.seek(0)
return self.body.read()
@property
def method(self):
return self.environ['REQUEST_METHOD']
@property
def args(self):
return urlparse.parse_qs(self.environ['QUERY_STRING'])
@property
def form(self):
if self.environ['CONTENT_TYPE'] == 'application/x-www-form-urlencoded':
return cgi.FieldStorage(fp=self.body, environ=self.environ)
return dict()
@property
def cookies(self):
cookie = SimpleCookie(self.environ.get('HTTP_COOKIE', ''))
return {v.key: v.value for v in cookie.values()}
class Rule(str):
repl = {'int': r'[0-9]+', 'float': r'\-?[0-9]+\.[0-9]+'}
def __init__(self, string):
first, last, rv = 0, -1, []
f = lambda m: '(?P<%s>%s)' % (m.group(2), self.repl.get(m.group(1), m.group(1)))
for i, c in enumerate(string):
if c == '<':
first = i
rv.append(re.escape(string[last+1:first]))
if c == '>':
last = i
if last > first:
rv.append(re.sub(r'<\(([^:]+)\):(\w+)>', f, string[first:last+1]))
first = last
rv.append(re.escape(string[last+1:]))
self.rule = re.compile('^' + ''.join(rv) + '$')
def match(self, obj):
match = re.match(self.rule, obj)
if not match: return
kwargs = match.groupdict()
for key, value in kwargs.items():
for type in int, float:
try:
kwargs[key] = type(value)
break
except ValueError:
pass
return kwargs
def sendfile(filename, root):
headers = {}
root = os.path.abspath(root) + os.sep
filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
if not filename.startswith(root):
return 403, '', headers
mimetype, encoding = mimetypes.guess_type(filename)
if mimetype: headers['Content-Type'] = mimetype
if encoding: headers['Content-Encoding'] = encoding
stats = os.stat(filename)
headers['Content-Length'] = str(stats.st_size)
return 200, io.open(filename, 'rb'), headers
def setcookie(name, value, **kwargs):
return '; '.join([quote(name, '') + '=' + quote(value, '')] +
[k.replace('_', '-') + '=' + str(v) for k, v in kwargs.iteritems()])

@ -28,7 +28,7 @@ setup(
"Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7" "Programming Language :: Python :: 2.7"
], ],
install_requires=['werkzeug', 'mako', 'itsdangerous'], install_requires=['mako', 'itsdangerous'],
entry_points={ entry_points={
'console_scripts': 'console_scripts':
['isso = isso:main'], ['isso = isso:main'],

@ -113,8 +113,8 @@ class TestComments(unittest.TestCase):
data=json.dumps(comment(text='...'))).status_code == 201 data=json.dumps(comment(text='...'))).status_code == 201
for path in paths: for path in paths:
assert self.get('/1.0/' + path) assert self.get('/1.0/' + path + '/').status_code == 200
assert self.get('/1.0/' + path + '/1') assert self.get('/1.0/' + path + '/1').status_code == 200
def testDeleteAndCreateByDifferentUsersButSamePostId(self): def testDeleteAndCreateByDifferentUsersButSamePostId(self):

Loading…
Cancel
Save