Make ihatemoney Py2/3 compatible

Flask-wtf>=0.13 is now required and Form is replaced by FlaskForm
Py2/3 compatibility is assured by six
This commit is contained in:
0livd 2017-03-12 23:13:52 +01:00
parent 10a16a3b5c
commit 59a050e020
4 changed files with 122 additions and 108 deletions

View file

@ -1,6 +1,8 @@
from flask_wtf import DateField, DecimalField, Email, Form, PasswordField, \ from flask_wtf.form import FlaskForm
Required, SelectField, SelectMultipleField, SubmitField, TextAreaField, \ from wtforms.fields.core import SelectField, SelectMultipleField
TextField, ValidationError from wtforms.fields.html5 import DateField, DecimalField
from wtforms.fields.simple import PasswordField, SubmitField, TextAreaField, TextField
from wtforms.validators import Email, Required, ValidationError
from flask_babel import lazy_gettext as _ from flask_babel import lazy_gettext as _
from flask import request from flask import request
@ -35,7 +37,7 @@ class CommaDecimalField(DecimalField):
return super(CommaDecimalField, self).process_formdata(value) return super(CommaDecimalField, self).process_formdata(value)
class EditProjectForm(Form): class EditProjectForm(FlaskForm):
name = TextField(_("Project name"), validators=[Required()]) name = TextField(_("Project name"), validators=[Required()])
password = TextField(_("Private code"), validators=[Required()]) password = TextField(_("Private code"), validators=[Required()])
contact_email = TextField(_("Email"), validators=[Required(), Email()]) contact_email = TextField(_("Email"), validators=[Required(), Email()])
@ -75,13 +77,13 @@ class ProjectForm(EditProjectForm):
"that you will be able to remember."))) "that you will be able to remember.")))
class AuthenticationForm(Form): class AuthenticationForm(FlaskForm):
id = TextField(_("Project identifier"), validators=[Required()]) id = TextField(_("Project identifier"), validators=[Required()])
password = PasswordField(_("Private code"), validators=[Required()]) password = PasswordField(_("Private code"), validators=[Required()])
submit = SubmitField(_("Get in")) submit = SubmitField(_("Get in"))
class PasswordReminder(Form): class PasswordReminder(FlaskForm):
id = TextField(_("Project identifier"), validators=[Required()]) id = TextField(_("Project identifier"), validators=[Required()])
submit = SubmitField(_("Send me the code by email")) submit = SubmitField(_("Send me the code by email"))
@ -90,7 +92,7 @@ class PasswordReminder(Form):
raise ValidationError(_("This project does not exists")) raise ValidationError(_("This project does not exists"))
class BillForm(Form): class BillForm(FlaskForm):
date = DateField(_("Date"), validators=[Required()], default=datetime.now) date = DateField(_("Date"), validators=[Required()], default=datetime.now)
what = TextField(_("What?"), validators=[Required()]) what = TextField(_("What?"), validators=[Required()])
payer = SelectField(_("Payer"), validators=[Required()], coerce=int) payer = SelectField(_("Payer"), validators=[Required()], coerce=int)
@ -125,7 +127,7 @@ class BillForm(Form):
raise ValidationError(_("Bills can't be null")) raise ValidationError(_("Bills can't be null"))
class MemberForm(Form): class MemberForm(FlaskForm):
name = TextField(_("Name"), validators=[Required()]) name = TextField(_("Name"), validators=[Required()])
weight = CommaDecimalField(_("Weight"), default=1) weight = CommaDecimalField(_("Weight"), default=1)
@ -158,7 +160,7 @@ class MemberForm(Form):
self.weight.data = member.weight self.weight.data = member.weight
class InviteForm(Form): class InviteForm(FlaskForm):
emails = TextAreaField(_("People to notify")) emails = TextAreaField(_("People to notify"))
submit = SubmitField(_("Send invites")) submit = SubmitField(_("Send invites"))
@ -170,13 +172,13 @@ class InviteForm(Form):
email=email)) email=email))
class CreateArchiveForm(Form): class CreateArchiveForm(FlaskForm):
name = TextField(_("Name for this archive (optional)"), validators=[]) name = TextField(_("Name for this archive (optional)"), validators=[])
start_date = DateField(_("Start date"), validators=[Required()]) start_date = DateField(_("Start date"), validators=[Required()])
end_date = DateField(_("End date"), validators=[Required()], default=datetime.now) end_date = DateField(_("End date"), validators=[Required()], default=datetime.now)
class ExportForm(Form): class ExportForm(FlaskForm):
export_type = SelectField(_("What do you want to download ?"), export_type = SelectField(_("What do you want to download ?"),
validators=[Required()], validators=[Required()],
coerce=str, coerce=str,

View file

@ -1,10 +1,11 @@
flask>=0.9 flask>=0.11
flask-wtf==0.8 flask-wtf>=0.13
flask-sqlalchemy flask-sqlalchemy
flask-mail>=0.8 flask-mail>=0.8
Flask-Migrate==1.8.0 Flask-Migrate>=1.8.0
flask-babel flask-babel
flask-rest flask-rest
jinja2==2.6 jinja2>=2.6
raven raven
blinker blinker
six>=1.10

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
try: try:
import unittest2 as unittest import unittest2 as unittest
except ImportError: except ImportError:
@ -8,6 +9,7 @@ import base64
import os import os
import json import json
from collections import defaultdict from collections import defaultdict
import six
os.environ['FLASK_SETTINGS_MODULE'] = 'default_settings' os.environ['FLASK_SETTINGS_MODULE'] = 'default_settings'
@ -23,7 +25,7 @@ class TestCase(unittest.TestCase):
run.app.config['TESTING'] = True run.app.config['TESTING'] = True
run.app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///memory" run.app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///memory"
run.app.config['CSRF_ENABLED'] = False # simplify the tests run.app.config['WTF_CSRF_ENABLED'] = False # simplify the tests
self.app = run.app.test_client() self.app = run.app.test_client()
try: try:
models.db.init_app(run.app) models.db.init_app(run.app)
@ -56,7 +58,7 @@ class TestCase(unittest.TestCase):
}) })
def create_project(self, name): def create_project(self, name):
models.db.session.add(models.Project(id=name, name=unicode(name), models.db.session.add(models.Project(id=name, name=six.text_type(name),
password=name, contact_email="%s@notmyidea.org" % name)) password=name, contact_email="%s@notmyidea.org" % name))
models.db.session.commit() models.db.session.commit()
@ -96,7 +98,7 @@ class BudgetTestCase(TestCase):
response = self.app.post("/raclette/invite", response = self.app.post("/raclette/invite",
data={"emails": "toto"}) data={"emails": "toto"})
self.assertEqual(len(outbox), 0) # no message sent self.assertEqual(len(outbox), 0) # no message sent
self.assertIn("The email toto is not valid", response.data) self.assertIn("The email toto is not valid", response.data.decode('utf-8'))
# mixing good and wrong adresses shouldn't send any messages # mixing good and wrong adresses shouldn't send any messages
with run.mail.record_messages() as outbox: with run.mail.record_messages() as outbox:
@ -192,7 +194,7 @@ class BudgetTestCase(TestCase):
# check fred is present in the bills page # check fred is present in the bills page
result = self.app.get("/raclette/") result = self.app.get("/raclette/")
self.assertIn("fred", result.data) self.assertIn("fred", result.data.decode('utf-8'))
# remove fred # remove fred
self.app.post("/raclette/members/%s/delete" % self.app.post("/raclette/members/%s/delete" %
@ -208,7 +210,7 @@ class BudgetTestCase(TestCase):
# bound him to a bill # bound him to a bill
result = self.app.post("/raclette/add", data={ result = self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': fred_id, 'payer': fred_id,
'payed_for': [fred_id, ], 'payed_for': [fred_id, ],
'amount': '25', 'amount': '25',
@ -225,10 +227,10 @@ class BudgetTestCase(TestCase):
# as fred is now deactivated, check that he is not listed when adding # as fred is now deactivated, check that he is not listed when adding
# a bill or displaying the balance # a bill or displaying the balance
result = self.app.get("/raclette/") result = self.app.get("/raclette/")
self.assertNotIn("/raclette/members/%s/delete" % fred_id, result.data) self.assertNotIn(("/raclette/members/%s/delete" % fred_id), result.data.decode('utf-8'))
result = self.app.get("/raclette/add") result = self.app.get("/raclette/add")
self.assertNotIn("fred", result.data) self.assertNotIn("fred", result.data.decode('utf-8'))
# adding him again should reactivate him # adding him again should reactivate him
self.app.post("/raclette/members/add", data={'name': 'fred'}) self.app.post("/raclette/members/add", data={'name': 'fred'})
@ -257,7 +259,7 @@ class BudgetTestCase(TestCase):
# bound him to a bill # bound him to a bill
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': alexis.id, 'payer': alexis.id,
'payed_for': [alexis.id, ], 'payed_for': [alexis.id, ],
'amount': '25', 'amount': '25',
@ -295,7 +297,7 @@ class BudgetTestCase(TestCase):
# try to authenticate without credentials should redirect # try to authenticate without credentials should redirect
# to the authentication page # to the authentication page
resp = self.app.post("/authenticate") resp = self.app.post("/authenticate")
self.assertIn("Authentication", resp.data) self.assertIn("Authentication", resp.data.decode('utf-8'))
# raclette that the login / logout process works # raclette that the login / logout process works
self.create_project("raclette") self.create_project("raclette")
@ -303,14 +305,14 @@ class BudgetTestCase(TestCase):
# try to see the project while not being authenticated should redirect # try to see the project while not being authenticated should redirect
# to the authentication page # to the authentication page
resp = self.app.get("/raclette", follow_redirects=True) resp = self.app.get("/raclette", follow_redirects=True)
self.assertIn("Authentication", resp.data) self.assertIn("Authentication", resp.data.decode('utf-8'))
# try to connect with wrong credentials should not work # try to connect with wrong credentials should not work
with run.app.test_client() as c: with run.app.test_client() as c:
resp = c.post("/authenticate", resp = c.post("/authenticate",
data={'id': 'raclette', 'password': 'nope'}) data={'id': 'raclette', 'password': 'nope'})
self.assertIn("Authentication", resp.data) self.assertIn("Authentication", resp.data.decode('utf-8'))
self.assertNotIn('raclette', session) self.assertNotIn('raclette', session)
# try to connect with the right credentials should work # try to connect with the right credentials should work
@ -318,7 +320,7 @@ class BudgetTestCase(TestCase):
resp = c.post("/authenticate", resp = c.post("/authenticate",
data={'id': 'raclette', 'password': 'raclette'}) data={'id': 'raclette', 'password': 'raclette'})
self.assertNotIn("Authentication", resp.data) self.assertNotIn("Authentication", resp.data.decode('utf-8'))
self.assertIn('raclette', session) self.assertIn('raclette', session)
self.assertEqual(session['raclette'], 'raclette') self.assertEqual(session['raclette'], 'raclette')
@ -339,7 +341,7 @@ class BudgetTestCase(TestCase):
# create a bill # create a bill
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[0], 'payer': members_ids[0],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '25', 'amount': '25',
@ -351,7 +353,7 @@ class BudgetTestCase(TestCase):
# edit the bill # edit the bill
self.app.post("/raclette/edit/%s" % bill.id, data={ self.app.post("/raclette/edit/%s" % bill.id, data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[0], 'payer': members_ids[0],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '10', 'amount': '10',
@ -367,7 +369,7 @@ class BudgetTestCase(TestCase):
# test balance # test balance
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[0], 'payer': members_ids[0],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '19', 'amount': '19',
@ -375,7 +377,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[1], 'payer': members_ids[1],
'payed_for': members_ids[0], 'payed_for': members_ids[0],
'amount': '20', 'amount': '20',
@ -383,7 +385,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[1], 'payer': members_ids[1],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '17', 'amount': '17',
@ -395,7 +397,7 @@ class BudgetTestCase(TestCase):
#Bill with negative amount #Bill with negative amount
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-12', 'date': '2011-08-12',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[0], 'payer': members_ids[0],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '-25' 'amount': '-25'
@ -406,7 +408,7 @@ class BudgetTestCase(TestCase):
#add a bill with a comma #add a bill with a comma
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-01', 'date': '2011-08-01',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[0], 'payer': members_ids[0],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '25,02', 'amount': '25,02',
@ -427,7 +429,7 @@ class BudgetTestCase(TestCase):
# test balance # test balance
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': members_ids[0], 'payer': members_ids[0],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '10', 'amount': '10',
@ -435,7 +437,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'pommes de terre', 'what': 'pommes de terre',
'payer': members_ids[1], 'payer': members_ids[1],
'payed_for': members_ids, 'payed_for': members_ids,
'amount': '10', 'amount': '10',
@ -452,12 +454,12 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/members/add", data={'name': 'tata', 'weight': 1}) self.app.post("/raclette/members/add", data={'name': 'tata', 'weight': 1})
resp = self.app.get("/raclette/") resp = self.app.get("/raclette/")
self.assertIn('extra-info', resp.data) self.assertIn('extra-info', resp.data.decode('utf-8'))
self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4})
resp = self.app.get("/raclette/") resp = self.app.get("/raclette/")
self.assertNotIn('extra-info', resp.data) self.assertNotIn('extra-info', resp.data.decode('utf-8'))
def test_rounding(self): def test_rounding(self):
@ -471,7 +473,7 @@ class BudgetTestCase(TestCase):
# create bills # create bills
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': 1, 'payer': 1,
'payed_for': [1, 2, 3], 'payed_for': [1, 2, 3],
'amount': '24.36', 'amount': '24.36',
@ -479,7 +481,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'red wine', 'what': 'red wine',
'payer': 2, 'payer': 2,
'payed_for': [1], 'payed_for': [1],
'amount': '19.12', 'amount': '19.12',
@ -487,7 +489,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'delicatessen', 'what': 'delicatessen',
'payer': 1, 'payer': 1,
'payed_for': [1, 2], 'payed_for': [1, 2],
'amount': '22', 'amount': '22',
@ -500,7 +502,7 @@ class BudgetTestCase(TestCase):
result[models.Project.query.get("raclette").members[2].id] = -8.12 result[models.Project.query.get("raclette").members[2].id] = -8.12
# Since we're using floating point to store currency, we can have some rounding issues that prevent test from working. # Since we're using floating point to store currency, we can have some rounding issues that prevent test from working.
# However, we should obtain the same values as the theorical ones if we round to 2 decimals, like in the UI. # However, we should obtain the same values as the theorical ones if we round to 2 decimals, like in the UI.
for key, value in balance.iteritems(): for key, value in six.iteritems(balance):
self.assertEqual(round(value, 2), result[key]) self.assertEqual(round(value, 2), result[key])
def test_edit_project(self): def test_edit_project(self):
@ -526,7 +528,7 @@ class BudgetTestCase(TestCase):
resp = self.app.post("/raclette/edit", data=new_data, resp = self.app.post("/raclette/edit", data=new_data,
follow_redirects=True) follow_redirects=True)
self.assertIn("Invalid email address", resp.data) self.assertIn("Invalid email address", resp.data.decode('utf-8'))
def test_dashboard(self): def test_dashboard(self):
response = self.app.get("/dashboard") response = self.app.get("/dashboard")
@ -550,7 +552,7 @@ class BudgetTestCase(TestCase):
# create bills # create bills
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': 1, 'payer': 1,
'payed_for': [1, 2, 3], 'payed_for': [1, 2, 3],
'amount': '10.0', 'amount': '10.0',
@ -558,7 +560,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'red wine', 'what': 'red wine',
'payer': 2, 'payer': 2,
'payed_for': [1], 'payed_for': [1],
'amount': '20', 'amount': '20',
@ -566,7 +568,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'delicatessen', 'what': 'delicatessen',
'payer': 1, 'payer': 1,
'payed_for': [1, 2], 'payed_for': [1, 2],
'amount': '10', 'amount': '10',
@ -594,7 +596,7 @@ class BudgetTestCase(TestCase):
# create bills # create bills
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2016-12-31', 'date': '2016-12-31',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': 1, 'payer': 1,
'payed_for': [1, 2, 3], 'payed_for': [1, 2, 3],
'amount': '10.0', 'amount': '10.0',
@ -602,7 +604,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2016-12-31', 'date': '2016-12-31',
'what': u'red wine', 'what': 'red wine',
'payer': 2, 'payer': 2,
'payed_for': [1, 3], 'payed_for': [1, 3],
'amount': '20', 'amount': '20',
@ -610,7 +612,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2017-01-01', 'date': '2017-01-01',
'what': u'refund', 'what': 'refund',
'payer': 3, 'payer': 3,
'payed_for': [2], 'payed_for': [2],
'amount': '13.33', 'amount': '13.33',
@ -636,7 +638,7 @@ class BudgetTestCase(TestCase):
# create bills # create bills
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2016-12-31', 'date': '2016-12-31',
'what': u'fromage à raclette', 'what': 'fromage à raclette',
'payer': 1, 'payer': 1,
'payed_for': [1, 2, 3, 4], 'payed_for': [1, 2, 3, 4],
'amount': '10.0', 'amount': '10.0',
@ -644,7 +646,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2016-12-31', 'date': '2016-12-31',
'what': u'red wine', 'what': 'red wine',
'payer': 2, 'payer': 2,
'payed_for': [1, 3], 'payed_for': [1, 3],
'amount': '200', 'amount': '200',
@ -652,7 +654,7 @@ class BudgetTestCase(TestCase):
self.app.post("/raclette/add", data={ self.app.post("/raclette/add", data={
'date': '2017-01-01', 'date': '2017-01-01',
'what': u'refund', 'what': 'refund',
'payer': 3, 'payer': 3,
'payed_for': [2], 'payed_for': [2],
'amount': '13.33', 'amount': '13.33',
@ -663,13 +665,13 @@ class BudgetTestCase(TestCase):
'export_format': 'json', 'export_format': 'json',
'export_type': 'bills' 'export_type': 'bills'
}) })
expected = [{u'date': u'2017-01-01', u'what': u'refund', expected = [{'date': '2017-01-01', 'what': 'refund',
u'amount': 13.33, u'payer_name': u'tata', u'payer_weight': 1.0, u'owers': [u'fred']}, 'amount': 13.33, 'payer_name': 'tata', 'payer_weight': 1.0, 'owers': ['fred']},
{u'date': u'2016-12-31', u'what': u'red wine', {'date': '2016-12-31', 'what': 'red wine',
u'amount': 200.0, u'payer_name': u'fred', u'payer_weight': 1.0, u'owers': [u'alexis', u'tata']}, 'amount': 200.0, 'payer_name': 'fred', 'payer_weight': 1.0, 'owers': ['alexis', 'tata']},
{u'date': u'2016-12-31', u'what': u'fromage \xe0 raclette', {'date': '2016-12-31', 'what': 'fromage \xe0 raclette',
u'amount': 10.0, u'payer_name': u'alexis', u'payer_weight': 2.0, u'owers': [u'alexis', u'fred', u'tata', u'p\xe9p\xe9']}] 'amount': 10.0, 'payer_name': 'alexis', 'payer_weight': 2.0, 'owers': ['alexis', 'fred', 'tata', 'p\xe9p\xe9']}]
self.assertEqual(json.loads(resp.data), expected) self.assertEqual(json.loads(resp.data.decode('utf-8')), expected)
# generate csv export of bills # generate csv export of bills
resp = self.app.post("/raclette/edit", data={ resp = self.app.post("/raclette/edit", data={
@ -680,7 +682,7 @@ class BudgetTestCase(TestCase):
"2017-01-01,refund,13.33,tata,1.0,fred", "2017-01-01,refund,13.33,tata,1.0,fred",
"2016-12-31,red wine,200.0,fred,1.0,\"alexis, tata\"", "2016-12-31,red wine,200.0,fred,1.0,\"alexis, tata\"",
"2016-12-31,fromage à raclette,10.0,alexis,2.0,\"alexis, fred, tata, pépé\""] "2016-12-31,fromage à raclette,10.0,alexis,2.0,\"alexis, fred, tata, pépé\""]
received_lines = resp.data.split("\n") received_lines = resp.data.decode('utf-8').split("\n")
for i, line in enumerate(expected): for i, line in enumerate(expected):
self.assertEqual( self.assertEqual(
@ -693,10 +695,10 @@ class BudgetTestCase(TestCase):
'export_format': 'json', 'export_format': 'json',
'export_type': 'transactions' 'export_type': 'transactions'
}) })
expected = [{u"amount": 127.33, u"receiver": u"fred", u"ower": u"alexis"}, expected = [{"amount": 127.33, "receiver": "fred", "ower": "alexis"},
{u"amount": 55.34, u"receiver": u"fred", u"ower": u"tata"}, {"amount": 55.34, "receiver": "fred", "ower": "tata"},
{u"amount": 2.00, u"receiver": u"fred", u"ower": u"p\xe9p\xe9"}] {"amount": 2.00, "receiver": "fred", "ower": "p\xe9p\xe9"}]
self.assertEqual(json.loads(resp.data), expected) self.assertEqual(json.loads(resp.data.decode('utf-8')), expected)
# generate csv export of transactions # generate csv export of transactions
resp = self.app.post("/raclette/edit", data={ resp = self.app.post("/raclette/edit", data={
@ -708,7 +710,7 @@ class BudgetTestCase(TestCase):
"127.33,fred,alexis", "127.33,fred,alexis",
"55.34,fred,tata", "55.34,fred,tata",
"2.0,fred,pépé"] "2.0,fred,pépé"]
received_lines = resp.data.split("\n") received_lines = resp.data.decode('utf-8').split("\n")
for i, line in enumerate(expected): for i, line in enumerate(expected):
self.assertEqual( self.assertEqual(
@ -723,7 +725,7 @@ class BudgetTestCase(TestCase):
}) })
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertIn('id="export_format" name="export_format"', resp.data) self.assertIn('id="export_format" name="export_format"', resp.data.decode('utf-8'))
# wrong export_type should return a 200 and export form # wrong export_type should return a 200 and export form
resp = self.app.post("/raclette/edit", data={ resp = self.app.post("/raclette/edit", data={
@ -732,7 +734,7 @@ class BudgetTestCase(TestCase):
}) })
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertIn('id="export_format" name="export_format"', resp.data) self.assertIn('id="export_format" name="export_format"', resp.data.decode('utf-8'))
class APITestCase(TestCase): class APITestCase(TestCase):
@ -758,7 +760,7 @@ class APITestCase(TestCase):
def get_auth(self, username, password=None): def get_auth(self, username, password=None):
password = password or username password = password or username
base64string = base64.encodestring( base64string = base64.encodestring(
'%s:%s' % (username, password)).replace('\n', '') ('%s:%s' % (username, password)).encode('utf-8')).decode('utf-8').replace('\n', '')
return {"Authorization": "Basic %s" % base64string} return {"Authorization": "Basic %s" % base64string}
def assertStatus(self, expected, resp, url=""): def assertStatus(self, expected, resp, url=""):
@ -802,7 +804,7 @@ class APITestCase(TestCase):
self.assertTrue(400, resp.status_code) self.assertTrue(400, resp.status_code)
self.assertEqual('{"contact_email": ["Invalid email address."]}', self.assertEqual('{"contact_email": ["Invalid email address."]}',
resp.data) resp.data.decode('utf-8'))
# create it # create it
resp = self.api_create("raclette") resp = self.api_create("raclette")
@ -812,7 +814,7 @@ class APITestCase(TestCase):
resp = self.api_create("raclette") resp = self.api_create("raclette")
self.assertTrue(400, resp.status_code) self.assertTrue(400, resp.status_code)
self.assertIn('id', json.loads(resp.data)) self.assertIn('id', json.loads(resp.data.decode('utf-8')))
# get information about it # get information about it
resp = self.app.get("/api/projects/raclette", resp = self.app.get("/api/projects/raclette",
@ -828,7 +830,7 @@ class APITestCase(TestCase):
"id": "raclette", "id": "raclette",
"balance": {}, "balance": {},
} }
self.assertDictEqual(json.loads(resp.data), expected) self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected)
# edit should work # edit should work
resp = self.app.put("/api/projects/raclette", data={ resp = self.app.put("/api/projects/raclette", data={
@ -852,7 +854,7 @@ class APITestCase(TestCase):
"id": "raclette", "id": "raclette",
"balance": {}, "balance": {},
} }
self.assertDictEqual(json.loads(resp.data), expected) self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected)
# delete should work # delete should work
resp = self.app.delete("/api/projects/raclette", resp = self.app.delete("/api/projects/raclette",
@ -874,7 +876,7 @@ class APITestCase(TestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual('[]', req.data) self.assertEqual('[]', req.data.decode('utf-8'))
# add a member # add a member
req = self.app.post("/api/projects/raclette/members", data={ req = self.app.post("/api/projects/raclette/members", data={
@ -883,14 +885,14 @@ class APITestCase(TestCase):
# the id of the new member should be returned # the id of the new member should be returned
self.assertStatus(201, req) self.assertStatus(201, req)
self.assertEqual("1", req.data) self.assertEqual("1", req.data.decode('utf-8'))
# the list of members should contain one member # the list of members should contain one member
req = self.app.get("/api/projects/raclette/members", req = self.app.get("/api/projects/raclette/members",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual(len(json.loads(req.data)), 1) self.assertEqual(len(json.loads(req.data.decode('utf-8'))), 1)
# edit this member # edit this member
req = self.app.put("/api/projects/raclette/members/1", data={ req = self.app.put("/api/projects/raclette/members/1", data={
@ -904,7 +906,7 @@ class APITestCase(TestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("Fred", json.loads(req.data)["name"]) self.assertEqual("Fred", json.loads(req.data.decode('utf-8'))["name"])
# delete a member # delete a member
@ -919,7 +921,7 @@ class APITestCase(TestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual('[]', req.data) self.assertEqual('[]', req.data.decode('utf-8'))
def test_bills(self): def test_bills(self):
# create a project # create a project
@ -935,12 +937,12 @@ class APITestCase(TestCase):
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("[]", req.data) self.assertEqual("[]", req.data.decode('utf-8'))
# add a bill # add a bill
req = self.app.post("/api/projects/raclette/bills", data={ req = self.app.post("/api/projects/raclette/bills", data={
'date': '2011-08-10', 'date': '2011-08-10',
'what': u'fromage', 'what': 'fromage',
'payer': "1", 'payer': "1",
'payed_for': ["1", "2"], 'payed_for': ["1", "2"],
'amount': '25', 'amount': '25',
@ -948,7 +950,7 @@ class APITestCase(TestCase):
# should return the id # should return the id
self.assertStatus(201, req) self.assertStatus(201, req)
self.assertEqual(req.data, "1") self.assertEqual(req.data.decode('utf-8'), "1")
# get this bill details # get this bill details
req = self.app.get("/api/projects/raclette/bills/1", req = self.app.get("/api/projects/raclette/bills/1",
@ -966,30 +968,30 @@ class APITestCase(TestCase):
"date": "2011-08-10", "date": "2011-08-10",
"id": 1} "id": 1}
self.assertDictEqual(expected, json.loads(req.data)) self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
# the list of bills should lenght 1 # the list of bills should lenght 1
req = self.app.get("/api/projects/raclette/bills", req = self.app.get("/api/projects/raclette/bills",
headers=self.get_auth("raclette")) headers=self.get_auth("raclette"))
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual(1, len(json.loads(req.data))) self.assertEqual(1, len(json.loads(req.data.decode('utf-8'))))
# edit with errors should return an error # edit with errors should return an error
req = self.app.put("/api/projects/raclette/bills/1", data={ req = self.app.put("/api/projects/raclette/bills/1", data={
'date': '201111111-08-10', # not a date 'date': '201111111-08-10', # not a date
'what': u'fromage', 'what': 'fromage',
'payer': "1", 'payer': "1",
'payed_for': ["1", "2"], 'payed_for': ["1", "2"],
'amount': '25', 'amount': '25',
}, headers=self.get_auth("raclette")) }, headers=self.get_auth("raclette"))
self.assertStatus(400, req) self.assertStatus(400, req)
self.assertEqual('{"date": ["This field is required."]}', req.data) self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8'))
# edit a bill # edit a bill
req = self.app.put("/api/projects/raclette/bills/1", data={ req = self.app.put("/api/projects/raclette/bills/1", data={
'date': '2011-09-10', 'date': '2011-09-10',
'what': u'beer', 'what': 'beer',
'payer': "2", 'payer': "2",
'payed_for': ["1", "2"], 'payed_for': ["1", "2"],
'amount': '25', 'amount': '25',
@ -1009,7 +1011,7 @@ class APITestCase(TestCase):
"date": "2011-09-10", "date": "2011-09-10",
"id": 1} "id": 1}
self.assertDictEqual(expected, json.loads(req.data)) self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
# delete a bill # delete a bill
req = self.app.delete("/api/projects/raclette/bills/1", req = self.app.delete("/api/projects/raclette/bills/1",
@ -1031,7 +1033,7 @@ class APITestCase(TestCase):
self.api_add_member("raclette", "<script>") self.api_add_member("raclette", "<script>")
result = self.app.get('/raclette/') result = self.app.get('/raclette/')
self.assertNotIn("<script>", result.data) self.assertNotIn("<script>", result.data.decode('utf-8'))
def test_weighted_bills(self): def test_weighted_bills(self):
# create a project # create a project
@ -1066,7 +1068,7 @@ class APITestCase(TestCase):
"amount": 25.0, "amount": 25.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1} "id": 1}
self.assertDictEqual(expected, json.loads(req.data)) self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
# getting it should return a 404 # getting it should return a 404
req = self.app.get("/api/projects/raclette", req = self.app.get("/api/projects/raclette",
@ -1091,7 +1093,7 @@ class APITestCase(TestCase):
"password": "raclette"} "password": "raclette"}
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual(expected, json.loads(req.data)) self.assertEqual(expected, json.loads(req.data.decode('utf-8')))
class ServerTestCase(APITestCase): class ServerTestCase(APITestCase):
def setUp(self): def setUp(self):
@ -1108,6 +1110,5 @@ class ServerTestCase(APITestCase):
req = self.app.get("/foo/") req = self.app.get("/foo/")
self.assertStatus(200, req) self.assertStatus(200, req)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -1,11 +1,12 @@
import re import re
import inspect import inspect
from io import BytesIO, StringIO
from jinja2 import filters from jinja2 import filters
from json import dumps from json import dumps
from flask import redirect from flask import redirect
from werkzeug.routing import HTTPException, RoutingException from werkzeug.routing import HTTPException, RoutingException
from io import BytesIO import six
import csv import csv
@ -16,13 +17,14 @@ def slugify(value):
Copy/Pasted from ametaireau/pelican/utils itself took from django sources. Copy/Pasted from ametaireau/pelican/utils itself took from django sources.
""" """
if type(value) == unicode: if isinstance(value, six.text_type):
import unicodedata import unicodedata
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = unicodedata.normalize('NFKD', value)
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) if six.PY2:
value = value.encode('ascii', 'ignore')
value = six.text_type(re.sub('[^\w\s-]', '', value).strip().lower())
return re.sub('[-\s]+', '-', value) return re.sub('[-\s]+', '-', value)
class Redirect303(HTTPException, RoutingException): class Redirect303(HTTPException, RoutingException):
"""Raise if the map requests a redirect. This is for example the case if """Raise if the map requests a redirect. This is for example the case if
`strict_slashes` are activated and an url that requires a trailing slash. `strict_slashes` are activated and an url that requires a trailing slash.
@ -86,25 +88,33 @@ def list_of_dicts2json(dict_to_convert):
"""Take a list of dictionnaries and turns it into """Take a list of dictionnaries and turns it into
a json in-memory file a json in-memory file
""" """
bytes_io = BytesIO() return BytesIO(dumps(dict_to_convert).encode('utf-8'))
bytes_io.write(dumps(dict_to_convert))
bytes_io.seek(0)
return bytes_io
def list_of_dicts2csv(dict_to_convert): def list_of_dicts2csv(dict_to_convert):
"""Take a list of dictionnaries and turns it into """Take a list of dictionnaries and turns it into
a csv in-memory file, assume all dict have the same keys a csv in-memory file, assume all dict have the same keys
""" """
bytes_io = BytesIO() # CSV writer has a different behavior in PY2 and PY3
# http://stackoverflow.com/a/37974772
try: try:
csv_data = [dict_to_convert[0].keys()] if six.PY3:
for dic in dict_to_convert: csv_file = StringIO()
csv_data.append([dic[h].encode('utf8') csv_data = [dict_to_convert[0].keys()]
if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8') for dic in dict_to_convert:
for h in dict_to_convert[0].keys()]) csv_data.append([dic[h] for h in dict_to_convert[0].keys()])
else:
csv_file = BytesIO()
csv_data = []
csv_data.append([key.encode('utf-8') for key in dict_to_convert[0].keys()])
for dic in dict_to_convert:
csv_data.append([dic[h].encode('utf8')
if isinstance(dic[h], unicode) else str(dic[h]).encode('utf8')
for h in dict_to_convert[0].keys()])
except (KeyError, IndexError): except (KeyError, IndexError):
csv_data = [] csv_data = []
writer = csv.writer(bytes_io) writer = csv.writer(csv_file)
writer.writerows(csv_data) writer.writerows(csv_data)
bytes_io.seek(0) csv_file.seek(0)
return bytes_io if six.PY3:
csv_file = BytesIO(csv_file.getvalue().encode('utf-8'))
return csv_file