1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/ihatemoney_ynh.git synced 2024-09-03 19:26:15 +02:00

Merge pull request #82 from feth/master

review, pep8 and star imports
This commit is contained in:
Alexis Metaireau 2012-03-07 02:41:29 -08:00
commit 757e86baa1
6 changed files with 106 additions and 73 deletions

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import * from flask import Blueprint, request
from flask_rest import RESTResource, need_auth
from models import db, Project, Person, Bill from models import db, Project, Person, Bill
from forms import (ProjectForm, EditProjectForm, MemberForm, BillForm, from forms import (ProjectForm, EditProjectForm, MemberForm,
get_billform_for) get_billform_for)
from utils import for_all_methods
from flask_rest import RESTResource, need_auth
from werkzeug import Response
api = Blueprint("api", __name__, url_prefix="/api") api = Blueprint("api", __name__, url_prefix="/api")

View file

@ -1,9 +1,11 @@
from flaskext.wtf import * from flaskext.wtf import DateField, DecimalField, Email, Form, PasswordField, \
Required, SelectField, SelectMultipleField, SubmitField, TextAreaField, \
TextField, ValidationError
from flaskext.babel import lazy_gettext as _ from flaskext.babel import lazy_gettext as _
from flask import request from flask import request
from wtforms.widgets import html_params from wtforms.widgets import html_params
from models import Project, Person, Bill, db from models import Project, Person
from datetime import datetime from datetime import datetime
from jinja2 import Markup from jinja2 import Markup
from utils import slugify from utils import slugify
@ -18,14 +20,16 @@ def select_multi_checkbox(field, ul_class='', **kwargs):
js_function = u'toggle();' js_function = u'toggle();'
options = dict(kwargs, id=choice_id, onclick=js_function) options = dict(kwargs, id=choice_id, onclick=js_function)
label = _("Select All/None") label = _("Select All/None")
html.append(u'<li><label for="%s">%s<span>%s</span></label></li>' % (choice_id, '<input %s /> ' % html_params(**options), label)) html.append(u'<li><label for="%s">%s<span>%s</span></label></li>'
% (choice_id, '<input %s /> ' % html_params(**options), label))
for value, label, checked in field.iter_choices(): for value, label, checked in field.iter_choices():
choice_id = u'%s-%s' % (field_id, value) choice_id = u'%s-%s' % (field_id, value)
options = dict(kwargs, name=field.name, value=value, id=choice_id) options = dict(kwargs, name=field.name, value=value, id=choice_id)
if checked: if checked:
options['checked'] = 'checked' options['checked'] = 'checked'
html.append(u'<li><label for="%s">%s<span>%s</span></label></li>' % (choice_id, '<input %s /> ' % html_params(**options), label)) html.append(u'<li><label for="%s">%s<span>%s</span></label></li>'
% (choice_id, '<input %s /> ' % html_params(**options), label))
html.append(u'</ul>') html.append(u'</ul>')
return u''.join(html) return u''.join(html)
@ -38,13 +42,15 @@ def get_billform_for(project, set_default=True, **kwargs):
""" """
form = BillForm(**kwargs) form = BillForm(**kwargs)
form.payed_for.choices = form.payer.choices = [(m.id, m.name) for m in project.active_members] form.payed_for.choices = form.payer.choices = [(m.id, m.name)
for m in project.active_members]
form.payed_for.default = [m.id for m in project.active_members] form.payed_for.default = [m.id for m in project.active_members]
if set_default and request.method == "GET": if set_default and request.method == "GET":
form.set_default() form.set_default()
return form return form
class CommaDecimalField(DecimalField): class CommaDecimalField(DecimalField):
"""A class to deal with comma in Decimal Field""" """A class to deal with comma in Decimal Field"""
def process_formdata(self, value): def process_formdata(self, value):
@ -85,7 +91,12 @@ class ProjectForm(EditProjectForm):
def validate_id(form, field): def validate_id(form, field):
form.id.data = slugify(field.data) form.id.data = slugify(field.data)
if Project.query.get(form.id.data): if Project.query.get(form.id.data):
raise ValidationError(Markup(_("The project identifier is used to log in and for the URL of the project. We tried to generate an identifier for you but a project with this identifier already exists. Please create a new identifier you will be able to remember."))) raise ValidationError(Markup(_("The project identifier is used "
"to log in and for the URL of the project. "
"We tried to generate an identifier for you but a project "
"with this identifier already exists. "
"Please create a new identifier "
"that you will be able to remember.")))
class AuthenticationForm(Form): class AuthenticationForm(Form):
@ -114,11 +125,12 @@ class BillForm(Form):
submit2 = SubmitField(_("Submit and add a new one")) submit2 = SubmitField(_("Submit and add a new one"))
def save(self, bill, project): def save(self, bill, project):
bill.payer_id=self.payer.data bill.payer_id = self.payer.data
bill.amount=self.amount.data bill.amount = self.amount.data
bill.what=self.what.data bill.what = self.what.data
bill.date=self.date.data bill.date = self.date.data
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for.data] bill.owers = [Person.query.get(ower, project)
for ower in self.payed_for.data]
return bill return bill
@ -141,7 +153,8 @@ class BillForm(Form):
class MemberForm(Form): class MemberForm(Form):
name = TextField(_("Name"), validators=[Required()], default=_("Type user name here")) name = TextField(_("Name"), validators=[Required()],
default=_("Type user name here"))
submit = SubmitField(_("Add")) submit = SubmitField(_("Add"))
def __init__(self, project, *args, **kwargs): def __init__(self, project, *args, **kwargs):
@ -163,6 +176,7 @@ class MemberForm(Form):
return person return person
class InviteForm(Form): class InviteForm(Form):
emails = TextAreaField(_("People to notify")) emails = TextAreaField(_("People to notify"))
submit = SubmitField(_("Send invites")) submit = SubmitField(_("Send invites"))
@ -176,6 +190,6 @@ class InviteForm(Form):
class CreateArchiveForm(Form): class CreateArchiveForm(Form):
start_date = DateField(_("Start date"), validators=[Required(),]) start_date = DateField(_("Start date"), validators=[Required(), ])
end_date = DateField(_("End date"), validators=[Required(),]) end_date = DateField(_("End date"), validators=[Required(), ])
name = TextField(_("Name for this archive (optional)")) name = TextField(_("Name for this archive (optional)"))

View file

@ -8,7 +8,10 @@ from sqlalchemy import orm
db = SQLAlchemy() db = SQLAlchemy()
# define models # define models
class Project(db.Model): class Project(db.Model):
_to_serialize = ("id", "name", "password", "contact_email", _to_serialize = ("id", "name", "password", "contact_email",
@ -28,7 +31,8 @@ class Project(db.Model):
@property @property
def balance(self): def balance(self):
balances, should_pay, should_receive = defaultdict(int), defaultdict(int), defaultdict(int) balances, should_pay, should_receive = (defaultdict(int)
for time in (1, 2, 3))
# for each person # for each person
for person in self.members: for person in self.members:
@ -40,13 +44,14 @@ class Project(db.Model):
should_receive[bill.payer] += bill.pay_each() should_receive[bill.payer] += bill.pay_each()
for person in self.members: for person in self.members:
balances[person.id] = round(should_receive[person] - should_pay[person], 2) balance = should_receive[person] - should_pay[person]
balances[person.id] = round(balance, 2)
return balances return balances
def has_bills(self): def has_bills(self):
"""return if the project do have bills or not""" """return if the project do have bills or not"""
return self.get_bills().count() != 0 return self.get_bills().count() > 0
def get_bills(self): def get_bills(self):
"""Return the list of bills related to this project""" """Return the list of bills related to this project"""
@ -95,7 +100,6 @@ class Person(db.Model):
return Person.query.filter(Person.id == id)\ return Person.query.filter(Person.id == id)\
.filter(Project.id == project.id).one() .filter(Project.id == project.id).one()
query_class = PersonQuery query_class = PersonQuery
_to_serialize = ("id", "name", "activated") _to_serialize = ("id", "name", "activated")
@ -126,6 +130,7 @@ billowers = db.Table('billowers',
db.Column('person_id', db.Integer, db.ForeignKey('person.id')), db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
) )
class Bill(db.Model): class Bill(db.Model):
class BillQuery(BaseQuery): class BillQuery(BaseQuery):
@ -169,6 +174,7 @@ class Bill(db.Model):
return "<Bill of %s from %s for %s>" % (self.amount, return "<Bill of %s from %s for %s>" % (self.amount,
self.payer, ", ".join([o.name for o in self.owers])) self.payer, ", ".join([o.name for o in self.owers]))
class Archive(db.Model): class Archive(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.String, db.ForeignKey("project.id")) project_id = db.Column(db.String, db.ForeignKey("project.id"))

View file

@ -1,9 +1,9 @@
from flask import Flask, g, request, session
from flaskext.babel import Babel
from web import main, db, mail from web import main, db, mail
from api import api from api import api
import os
from flask import *
from flaskext.babel import Babel
app = Flask(__name__) app = Flask(__name__)
app.config.from_object("default_settings") app.config.from_object("default_settings")

View file

@ -1,10 +1,10 @@
import re import re
from functools import wraps
import inspect import inspect
from flask import redirect, url_for, session, request from flask import redirect
from werkzeug.routing import HTTPException, RoutingException from werkzeug.routing import HTTPException, RoutingException
def slugify(value): def slugify(value):
"""Normalizes string, converts to lowercase, removes non-alpha characters, """Normalizes string, converts to lowercase, removes non-alpha characters,
and converts spaces to hyphens. and converts spaces to hyphens.
@ -32,11 +32,3 @@ class Redirect303(HTTPException, RoutingException):
def get_response(self, environ): def get_response(self, environ):
return redirect(self.new_url, 303) return redirect(self.new_url, 303)
def for_all_methods(decorator):
"""Apply a decorator to all the methods of a class"""
def decorate(cls):
for name, method in inspect.getmembers(cls, inspect.ismethod):
setattr(cls, name, decorator(method))
return cls
return decorate

View file

@ -1,16 +1,3 @@
from collections import defaultdict
from flask import *
from flaskext.mail import Mail, Message
from flaskext.babel import get_locale, gettext as _
from smtplib import SMTPRecipientsRefused
import werkzeug
# local modules
from models import db, Project, Person, Bill
from forms import *
from utils import Redirect303
""" """
The blueprint for the web interface. The blueprint for the web interface.
@ -22,9 +9,24 @@ some shortcuts to make your life better when coding (see `pull_project`
and `add_project_id` for a quick overview) and `add_project_id` for a quick overview)
""" """
from flask import Blueprint, current_app, flash, g, redirect, \
render_template, request, session, url_for
from flaskext.mail import Mail, Message
from flaskext.babel import get_locale, gettext as _
from smtplib import SMTPRecipientsRefused
import werkzeug
# local modules
from models import db, Project, Person, Bill
from forms import AuthenticationForm, CreateArchiveForm, EditProjectForm, \
InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for
from utils import Redirect303
main = Blueprint("main", __name__) main = Blueprint("main", __name__)
mail = Mail() mail = Mail()
@main.url_defaults @main.url_defaults
def add_project_id(endpoint, values): def add_project_id(endpoint, values):
"""Add the project id to the url calls if it is expected. """Add the project id to the url calls if it is expected.
@ -36,6 +38,7 @@ def add_project_id(endpoint, values):
if current_app.url_map.is_endpoint_expecting(endpoint, 'project_id'): if current_app.url_map.is_endpoint_expecting(endpoint, 'project_id'):
values['project_id'] = g.project.id values['project_id'] = g.project.id
@main.url_value_preprocessor @main.url_value_preprocessor
def pull_project(endpoint, values): def pull_project(endpoint, values):
"""When a request contains a project_id value, transform it directly """When a request contains a project_id value, transform it directly
@ -51,7 +54,8 @@ def pull_project(endpoint, values):
if project_id: if project_id:
project = Project.query.get(project_id) project = Project.query.get(project_id)
if not project: if not project:
raise Redirect303(url_for(".create_project", project_id=project_id)) raise Redirect303(url_for(".create_project",
project_id=project_id))
if project.id in session and session[project.id] == project.password: if project.id in session and session[project.id] == project.password:
# add project into kwargs and call the original function # add project into kwargs and call the original function
g.project = project g.project = project
@ -60,6 +64,7 @@ def pull_project(endpoint, values):
raise Redirect303( raise Redirect303(
url_for(".authenticate", project_id=project_id)) url_for(".authenticate", project_id=project_id))
@main.route("/authenticate", methods=["GET", "POST"]) @main.route("/authenticate", methods=["GET", "POST"])
def authenticate(project_id=None): def authenticate(project_id=None):
"""Authentication form""" """Authentication form"""
@ -87,7 +92,8 @@ def authenticate(project_id=None):
if request.method == "POST": if request.method == "POST":
if form.validate(): if form.validate():
if not form.password.data == project.password: if not form.password.data == project.password:
form.errors['password'] = [_("This private code is not the right one")] msg = _("This private code is not the right one")
form.errors['password'] = [msg]
else: else:
# maintain a list of visited projects # maintain a list of visited projects
if "projects" not in session: if "projects" not in session:
@ -102,6 +108,7 @@ def authenticate(project_id=None):
return render_template("authenticate.html", form=form, return render_template("authenticate.html", form=form,
create_project=create_project) create_project=create_project)
@main.route("/") @main.route("/")
def home(): def home():
project_form = ProjectForm() project_form = ProjectForm()
@ -109,6 +116,7 @@ def home():
return render_template("home.html", project_form=project_form, return render_template("home.html", project_form=project_form,
auth_form=auth_form, session=session) auth_form=auth_form, session=session)
@main.route("/create", methods=["GET", "POST"]) @main.route("/create", methods=["GET", "POST"])
def create_project(): def create_project():
form = ProjectForm() form = ProjectForm()
@ -117,9 +125,10 @@ def create_project():
if request.method == "POST": if request.method == "POST":
# At first, we don't want the user to bother with the identifier # At first, we don't want the user to bother with the identifier
# so it will automatically be missing because not displayed into the form # so it will automatically be missing because not displayed into
# Thus we fill it with the same value as the filled name, the validation will # the form
# take care of the slug # Thus we fill it with the same value as the filled name,
# the validation will take care of the slug
if not form.id.data: if not form.id.data:
form.id.data = form.name.data form.id.data = form.name.data
if form.validate(): if form.validate():
@ -135,10 +144,11 @@ def create_project():
# send reminder email # send reminder email
g.project = project g.project = project
message_title = _("You have just created '%(project)s' to share your expenses", message_title = _("You have just created '%(project)s' "
project=g.project.name) "to share your expenses", project=g.project.name)
message_body = render_template("reminder_mail.%s" % get_locale().language) message_body = render_template("reminder_mail.%s" %
get_locale().language)
msg = Message(message_title, msg = Message(message_title,
body=message_body, body=message_body,
@ -158,6 +168,7 @@ def create_project():
return render_template("create_project.html", form=form) return render_template("create_project.html", form=form)
@main.route("/password-reminder", methods=["GET", "POST"]) @main.route("/password-reminder", methods=["GET", "POST"])
def remind_password(): def remind_password():
form = PasswordReminder() form = PasswordReminder()
@ -167,9 +178,9 @@ def remind_password():
project = Project.query.get(form.id.data) project = Project.query.get(form.id.data)
# send the password reminder # send the password reminder
password_reminder = "password_reminder.%s" % get_locale().language
mail.send(Message("password recovery", mail.send(Message("password recovery",
body=render_template("password_reminder.%s" % get_locale().language, body=render_template(password_reminder, project=project),
project=project),
recipients=[project.contact_email])) recipients=[project.contact_email]))
flash(_("a mail has been sent to you with the password")) flash(_("a mail has been sent to you with the password"))
@ -193,18 +204,21 @@ def edit_project():
return render_template("edit_project.html", form=form) return render_template("edit_project.html", form=form)
@main.route("/<project_id>/delete", methods=["POST"]) @main.route("/<project_id>/delete", methods=["POST"])
def remove_project(): def remove_project():
g.project.remove_project() g.project.remove_project()
return redirect(url_for(".home")) return redirect(url_for(".home"))
@main.route("/exit") @main.route("/exit")
def exit(): def exit():
# delete the session # delete the session
session.clear() session.clear()
return redirect(url_for(".home")) return redirect(url_for(".home"))
@main.route("/demo") @main.route("/demo")
def demo(): def demo():
""" """
@ -222,6 +236,7 @@ def demo():
session[project.id] = project.password session[project.id] = project.password
return redirect(url_for(".list_bills", project_id=project.id)) return redirect(url_for(".list_bills", project_id=project.id))
@main.route("/<project_id>/invite", methods=["GET", "POST"]) @main.route("/<project_id>/invite", methods=["GET", "POST"])
def invite(): def invite():
"""Send invitations for this particular project""" """Send invitations for this particular project"""
@ -232,10 +247,11 @@ def invite():
if form.validate(): if form.validate():
# send the email # send the email
message_body = render_template("invitation_mail.%s" % get_locale().language) message_body = render_template("invitation_mail.%s" %
get_locale().language)
message_title = _("You have been invited to share your expenses for %(project)s", message_title = _("You have been invited to share your "
project=g.project.name) "expenses for %(project)s", project=g.project.name)
msg = Message(message_title, msg = Message(message_title,
body=message_body, body=message_body,
recipients=[email.strip() recipients=[email.strip()
@ -246,9 +262,10 @@ def invite():
return render_template("send_invites.html", form=form) return render_template("send_invites.html", form=form)
@main.route("/<project_id>/") @main.route("/<project_id>/")
def list_bills(): def list_bills():
bill_form=get_billform_for(g.project) bill_form = get_billform_for(g.project)
# set the last selected payer as default choice if exists # set the last selected payer as default choice if exists
if 'last_selected_payer' in session: if 'last_selected_payer' in session:
bill_form.payer.data = session['last_selected_payer'] bill_form.payer.data = session['last_selected_payer']
@ -260,6 +277,7 @@ def list_bills():
add_bill=request.values.get('add_bill', False) add_bill=request.values.get('add_bill', False)
) )
@main.route("/<project_id>/members/add", methods=["GET", "POST"]) @main.route("/<project_id>/members/add", methods=["GET", "POST"])
def add_member(): def add_member():
# FIXME manage form errors on the list_bills page # FIXME manage form errors on the list_bills page
@ -273,6 +291,7 @@ def add_member():
return render_template("add_member.html", form=form) return render_template("add_member.html", form=form)
@main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"]) @main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"])
def reactivate(member_id): def reactivate(member_id):
person = Person.query.filter(Person.id == member_id)\ person = Person.query.filter(Person.id == member_id)\
@ -294,6 +313,7 @@ def remove_member(member_id):
return redirect(url_for(".list_bills")) return redirect(url_for(".list_bills"))
@main.route("/<project_id>/add", methods=["GET", "POST"]) @main.route("/<project_id>/add", methods=["GET", "POST"])
def add_bill(): def add_bill():
form = get_billform_for(g.project) form = get_billform_for(g.project)
@ -353,6 +373,7 @@ def edit_bill(bill_id):
return render_template("add_bill.html", form=form, edit=True) return render_template("add_bill.html", form=form, edit=True)
@main.route("/lang/<lang>") @main.route("/lang/<lang>")
def change_lang(lang): def change_lang(lang):
session['lang'] = lang session['lang'] = lang
@ -360,11 +381,13 @@ def change_lang(lang):
return redirect(request.headers.get('Referer') or url_for('.home')) return redirect(request.headers.get('Referer') or url_for('.home'))
@main.route("/<project_id>/compute") @main.route("/<project_id>/compute")
def compute_bills(): def compute_bills():
"""Compute the sum each one have to pay to each other and display it""" """Compute the sum each one have to pay to each other and display it"""
return render_template("compute_bills.html") return render_template("compute_bills.html")
@main.route("/<project_id>/archives/create") @main.route("/<project_id>/archives/create")
def create_archive(): def create_archive():
form = CreateArchiveForm() form = CreateArchiveForm()
@ -375,6 +398,7 @@ def create_archive():
return render_template("create_archive.html", form=form) return render_template("create_archive.html", form=form)
@main.route("/dashboard") @main.route("/dashboard")
def dashboard(): def dashboard():
return render_template("dashboard.html", projects=Project.query.all()) return render_template("dashboard.html", projects=Project.query.all())