fixes #29: port to Python 3 - welcome to the future

This commit is contained in:
Davide Alberani 2017-02-12 14:16:25 +01:00
parent 26296974cb
commit 83b29fd2d4
6 changed files with 56 additions and 32 deletions

View file

@ -22,10 +22,9 @@ See [https://ibt2.ismito.it:3002/](https://ibt2.ismito.it:3002/)
To install it: To install it:
``` bash ``` bash
wget https://bootstrap.pypa.io/get-pip.py wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py sudo python3 get-pip.py
sudo pip install tornado sudo pip3 install tornado
sudo pip install pymongo sudo pip3 install pymongo
sudo pip install python-dateutil
git clone https://github.com/raspibo/ibt2 git clone https://github.com/raspibo/ibt2
cd ibt2 cd ibt2
``` ```
@ -62,7 +61,7 @@ npm run devserver &
npm run dev npm run dev
# only when the devserver is running, you can also run the testsuite # only when the devserver is running, you can also run the testsuite
python ./tests/ibt2_tests.py python3 ./tests/ibt2_tests.py
``` ```
Your browser will automatically open [http://localhost:8080/](http://localhost:8080/) (that's the server for development) Your browser will automatically open [http://localhost:8080/](http://localhost:8080/) (that's the server for development)

20
ibt2.py
View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
"""I'll Be There, 2 (ibt2) - an oversimplified attendees registration system. """I'll Be There, 2 (ibt2) - an oversimplified attendees registration system.
Copyright 2016-2017 Davide Alberani <da@erlug.linux.it> Copyright 2016-2017 Davide Alberani <da@erlug.linux.it>
@ -63,8 +63,8 @@ class BaseHandler(tornado.web.RequestHandler):
collection = None collection = None
# A property to access the first value of each argument. # A property to access the first value of each argument.
arguments = property(lambda self: dict([(k, v[0]) arguments = property(lambda self: dict([(k, v[0].decode('utf-8'))
for k, v in self.request.arguments.iteritems()])) for k, v in self.request.arguments.items()]))
# Arguments suitable for a query on MongoDB. # Arguments suitable for a query on MongoDB.
clean_arguments = property(lambda self: self._clean_dict(self.arguments)) clean_arguments = property(lambda self: self._clean_dict(self.arguments))
@ -86,8 +86,8 @@ class BaseHandler(tornado.web.RequestHandler):
:param data: dictionary to clean :param data: dictionary to clean
:type data: dict""" :type data: dict"""
if isinstance(data, dict): if isinstance(data, dict):
for key in data.keys(): for key in list(data.keys()):
if (isinstance(key, (str, unicode)) and key.startswith('$')) or key in ('_id', 'created_by', if (isinstance(key, str) and key.startswith('$')) or key in ('_id', 'created_by',
'created_at', 'updated_by', 'created_at', 'updated_by',
'updated_at'): 'updated_at'):
del data[key] del data[key]
@ -109,13 +109,16 @@ class BaseHandler(tornado.web.RequestHandler):
def initialize(self, **kwargs): def initialize(self, **kwargs):
"""Add every passed (key, value) as attributes of the instance.""" """Add every passed (key, value) as attributes of the instance."""
for key, value in kwargs.iteritems(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
@property @property
def current_user(self): def current_user(self):
"""Retrieve current user ID from the secure cookie.""" """Retrieve current user ID from the secure cookie."""
return self.get_secure_cookie("user") current_user = self.get_secure_cookie("user")
if isinstance(current_user, bytes):
current_user = current_user.decode('utf-8')
return current_user
@property @property
def current_user_info(self): def current_user_info(self):
@ -381,8 +384,7 @@ class GroupsHandler(BaseHandler):
if not (day and group): if not (day and group):
return self.build_error(status=404, message='unable to access the resource') return self.build_error(status=404, message='unable to access the resource')
if not self.current_user_info.get('isAdmin'): if not self.current_user_info.get('isAdmin'):
self.build_error(status=401, message='insufficient permissions: must be admin') return self.build_error(status=401, message='insufficient permissions: must be admin')
return False
query = {'day': day, 'group': group} query = {'day': day, 'group': group}
howMany = self.db.delete('attendees', query) howMany = self.db.delete('attendees', query)
self.db.delete('groups', query) self.db.delete('groups', query)

View file

@ -56,7 +56,7 @@ def convert(seq):
""" """
if isinstance(seq, dict): if isinstance(seq, dict):
d = {} d = {}
for key, item in seq.iteritems(): for key, item in seq.items():
if key in _force_conversion: if key in _force_conversion:
try: try:
d[key] = _force_conversion[key](item) d[key] = _force_conversion[key](item)
@ -254,7 +254,7 @@ class Monco(object):
operator = self._operations.get(operation) operator = self._operations.get(operation)
if updateList: if updateList:
newData = {} newData = {}
for key, value in data.iteritems(): for key, value in data.items():
newData['%s.$.%s' % (updateList, key)] = value newData['%s.$.%s' % (updateList, key)] = value
data = newData data = newData
res = db[collection].find_and_modify(query=_id_or_query, res = db[collection].find_and_modify(query=_id_or_query,

View file

@ -15,8 +15,8 @@
}, },
"scripts": { "scripts": {
"dev": "node build/dev-server.js", "dev": "node build/dev-server.js",
"devserver": "python ibt2.py --debug --db_name=ibt2_test", "devserver": "python3 ibt2.py --debug --db_name=ibt2_test",
"server": "python ibt2.py --debug", "server": "python3 ibt2.py --debug",
"build": "node build/build.js" "build": "node build/build.js"
}, },
"dependencies": { "dependencies": {

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
"""I'll Be There, 2 (ibt2) - tests """I'll Be There, 2 (ibt2) - tests
Copyright 2016-2017 Davide Alberani <da@erlug.linux.it> Copyright 2016-2017 Davide Alberani <da@erlug.linux.it>
@ -23,7 +23,7 @@ BASE_URL = 'http://localhost:3000/v1.1/'
DB_NAME = 'ibt2_test' DB_NAME = 'ibt2_test'
def dictInDict(d, dContainer): def dictInDict(d, dContainer):
for k, v in d.viewitems(): for k, v in d.items():
if k not in dContainer: if k not in dContainer:
return False return False
if v != dContainer[k]: if v != dContainer[k]:
@ -39,15 +39,15 @@ class Ibt2Tests(unittest.TestCase):
self.db['attendees'].drop() self.db['attendees'].drop()
self.db['days'].drop() self.db['days'].drop()
self.db['groups'].drop() self.db['groups'].drop()
self.db['users'].remove({'username': 'newuser'}) self.db['users'].delete_one({'username': 'newuser'})
self.db['users'].remove({'username': 'newuser2'}) self.db['users'].delete_one({'username': 'newuser2'})
def tearDown(self): def tearDown(self):
self.db['attendees'].drop() self.db['attendees'].drop()
self.db['days'].drop() self.db['days'].drop()
self.db['groups'].drop() self.db['groups'].drop()
self.db['users'].remove({'username': 'newuser'}) self.db['users'].delete_one({'username': 'newuser'})
self.db['users'].remove({'username': 'newuser2'}) self.db['users'].delete_one({'username': 'newuser2'})
def add_attendee(self, attendee): def add_attendee(self, attendee):
r = requests.post('%sattendees' % BASE_URL, json=attendee) r = requests.post('%sattendees' % BASE_URL, json=attendee)
@ -85,9 +85,11 @@ class Ibt2Tests(unittest.TestCase):
attendee = {'name': 'A Name', 'day': '2017-01-15', 'group': 'A group'} attendee = {'name': 'A Name', 'day': '2017-01-15', 'group': 'A group'}
r = self.add_attendee(attendee) r = self.add_attendee(attendee)
id_ = r.json().get('_id') id_ = r.json().get('_id')
r.connection.close()
r = requests.delete(BASE_URL + 'attendees/' + id_) r = requests.delete(BASE_URL + 'attendees/' + id_)
r.raise_for_status() r.raise_for_status()
self.assertTrue(r.json().get('success')) self.assertTrue(r.json().get('success'))
r.connection.close()
def test_get_days(self): def test_get_days(self):
self.add_attendee({'day': '2017-01-15', 'name': 'A name', 'group': 'group A'}) self.add_attendee({'day': '2017-01-15', 'name': 'A name', 'group': 'group A'})
@ -118,9 +120,11 @@ class Ibt2Tests(unittest.TestCase):
def test_create_user(self): def test_create_user(self):
r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'}) r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'})
r.raise_for_status() r.raise_for_status()
r.connection.close()
s = self.login('newuser', 'ibt2') s = self.login('newuser', 'ibt2')
r = s.get(BASE_URL + 'users/current') r = s.get(BASE_URL + 'users/current')
r.raise_for_status() r.raise_for_status()
r.connection.close()
def test_update_user(self): def test_update_user(self):
r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'}) r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'})
@ -130,17 +134,20 @@ class Ibt2Tests(unittest.TestCase):
r.raise_for_status() r.raise_for_status()
id2_ = r.json()['_id'] id2_ = r.json()['_id']
r = requests.put(BASE_URL + 'users/' + id_, json={'email': 't@example.com'}) r = requests.put(BASE_URL + 'users/' + id_, json={'email': 't@example.com'})
self.assertRaises(r.raise_for_status) self.assertRaises(requests.exceptions.HTTPError, r.raise_for_status)
s = self.login('newuser', 'ibt2') s = self.login('newuser', 'ibt2')
r = s.put(BASE_URL + 'users/' + id_, json={'email': 'test@example.com'}) r = s.put(BASE_URL + 'users/' + id_, json={'email': 'test@example.com'})
r.raise_for_status() r.raise_for_status()
self.assertEqual(r.json().get('email'), 'test@example.com') self.assertEqual(r.json().get('email'), 'test@example.com')
r.connection.close()
r = s.put(BASE_URL + 'users/' + id2_, json={'email': 'test@example.com'}) r = s.put(BASE_URL + 'users/' + id2_, json={'email': 'test@example.com'})
self.assertRaises(r.raise_for_status) self.assertRaises(requests.exceptions.HTTPError, r.raise_for_status)
r.connection.close()
s = self.login('admin', 'ibt2') s = self.login('admin', 'ibt2')
r = s.put(BASE_URL + 'users/' + id_, json={'email': 'test2@example.com'}) r = s.put(BASE_URL + 'users/' + id_, json={'email': 'test2@example.com'})
r.raise_for_status() r.raise_for_status()
self.assertEqual(r.json().get('email'), 'test2@example.com') self.assertEqual(r.json().get('email'), 'test2@example.com')
r.connection.close()
def test_delete_user(self): def test_delete_user(self):
r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'}) r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'})
@ -150,26 +157,31 @@ class Ibt2Tests(unittest.TestCase):
r.raise_for_status() r.raise_for_status()
id2_ = r.json()['_id'] id2_ = r.json()['_id']
r = requests.delete(BASE_URL + 'users/' + id_) r = requests.delete(BASE_URL + 'users/' + id_)
self.assertRaises(r.raise_for_status) self.assertRaises(requests.exceptions.HTTPError, r.raise_for_status)
r.connection.close()
s = self.login('newuser', 'ibt2') s = self.login('newuser', 'ibt2')
r = s.delete(BASE_URL + 'users/' + id_) r = s.delete(BASE_URL + 'users/' + id_)
r.raise_for_status() r.raise_for_status()
r.connection.close()
r = s.delete(BASE_URL + 'users/' + id2_) r = s.delete(BASE_URL + 'users/' + id2_)
self.assertRaises(r.raise_for_status) self.assertRaises(requests.exceptions.HTTPError, r.raise_for_status)
r.connection.close()
s = self.login('admin', 'ibt2') s = self.login('admin', 'ibt2')
r = s.delete(BASE_URL + 'users/' + id2_) r = s.delete(BASE_URL + 'users/' + id2_)
r.raise_for_status() r.raise_for_status()
r.connection.close()
def test_duplicate_user(self): def test_duplicate_user(self):
r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'}) r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt2'})
r.raise_for_status() r.raise_for_status()
r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt3'}) r = requests.post(BASE_URL + 'users', json={'username': 'newuser', 'password': 'ibt3'})
self.assertRaises(r.raise_for_status) self.assertRaises(requests.exceptions.HTTPError, r.raise_for_status)
def login(self, username, password): def login(self, username, password):
s = requests.Session() s = requests.Session()
r = s.post(BASE_URL + 'login', json={'username': username, 'password': password}) r = s.post(BASE_URL + 'login', json={'username': username, 'password': password})
r.raise_for_status() r.raise_for_status()
r.connection.close()
return s return s
def test_created_by(self): def test_created_by(self):
@ -177,12 +189,14 @@ class Ibt2Tests(unittest.TestCase):
r = s.get(BASE_URL + 'users/current') r = s.get(BASE_URL + 'users/current')
r.raise_for_status() r.raise_for_status()
user_id = r.json()['_id'] user_id = r.json()['_id']
r.connection.close()
attendee = {'day': '2017-01-15', 'name': 'A name', 'group': 'group A'} attendee = {'day': '2017-01-15', 'name': 'A name', 'group': 'group A'}
r = s.post('%sattendees' % BASE_URL, json=attendee) r = s.post('%sattendees' % BASE_URL, json=attendee)
r.raise_for_status() r.raise_for_status()
rj = r.json() rj = r.json()
self.assertEqual(user_id, rj['created_by']) self.assertEqual(user_id, rj['created_by'])
self.assertEqual(user_id, rj['updated_by']) self.assertEqual(user_id, rj['updated_by'])
r.connection.close()
def test_put_day(self): def test_put_day(self):
day = {'day': '2017-01-16', 'notes': 'A day note'} day = {'day': '2017-01-16', 'notes': 'A day note'}
@ -214,10 +228,12 @@ class Ibt2Tests(unittest.TestCase):
r = s.delete(BASE_URL + 'days/2017-01-16/groups/A group', params={'day': '2017-01-16', 'group': 'A group'}) r = s.delete(BASE_URL + 'days/2017-01-16/groups/A group', params={'day': '2017-01-16', 'group': 'A group'})
r.raise_for_status() r.raise_for_status()
rj = r.json() rj = r.json()
r.connection.close()
r = requests.get(BASE_URL + 'days/2017-01-16') r = requests.get(BASE_URL + 'days/2017-01-16')
r.raise_for_status() r.raise_for_status()
rj = r.json() rj = r.json()
self.assertTrue(rj == {}) self.assertTrue(rj == {})
r.connection.close()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main(verbosity=2) unittest.main(verbosity=2)

View file

@ -36,15 +36,22 @@ def hash_password(password, salt=None):
:rtype: str""" :rtype: str"""
if salt is None: if salt is None:
salt_pool = string.ascii_letters + string.digits salt_pool = string.ascii_letters + string.digits
salt = ''.join(random.choice(salt_pool) for x in xrange(32)) salt = ''.join(random.choice(salt_pool) for x in range(32))
hash_ = hashlib.sha512('%s%s' % (salt, password)) pass_and_salt = '%s%s' % (salt, password)
pass_and_salt = pass_and_salt.encode('utf-8', 'ignore')
hash_ = hashlib.sha512(pass_and_salt)
return '$%s$%s' % (salt, hash_.hexdigest()) return '$%s$%s' % (salt, hash_.hexdigest())
class ImprovedEncoder(json.JSONEncoder): class ImprovedEncoder(json.JSONEncoder):
"""Enhance the default JSON encoder to serialize datetime and ObjectId instances.""" """Enhance the default JSON encoder to serialize datetime and ObjectId instances."""
def default(self, o): def default(self, o):
if isinstance(o, (datetime.datetime, datetime.date, if isinstance(o, bytes):
try:
return o.decode('utf-8')
except:
pass
elif isinstance(o, (datetime.datetime, datetime.date,
datetime.time, datetime.timedelta, ObjectId)): datetime.time, datetime.timedelta, ObjectId)):
try: try:
return str(o) return str(o)