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

Merge branch 'upstream'

Conflicts:
	sources/budget/static/css/datepicker.css
	sources/budget/static/js/jquery-1.7.2.min.js
This commit is contained in:
Jocelyn Delalande 2017-03-18 17:58:12 +01:00
commit 2b3b63ef27
31 changed files with 3102 additions and 1612 deletions

7
sources/.travis.yml Normal file
View file

@ -0,0 +1,7 @@
language: python
python:
- "2.7"
# command to install dependencies
install: "pip install -r budget/requirements.txt"
# command to run tests
script: cd budget && python tests.py

View file

@ -1,6 +1,10 @@
Budget-manager
##############
.. image:: https://travis-ci.org/spiral-project/ihatemoney.svg?branch=master
:target: https://travis-ci.org/spiral-project/ihatemoney
:alt: Travis CI Build Status
This is a really tiny app to ease the shared houses budget management. Keep
track of who bought what, when, and for who to then compute the balance of each
person.
@ -59,7 +63,7 @@ How about the REST API?
=======================
Yep, you're right, there is a REST API with this. Head to the `api
documentation <http://readthedocs.org/docs/ihatemoney/en/latest/api.html>`_ to know more.
documentation <https://ihatemoney.readthedocs.io/en/latest/api.html>`_ to know more.
How to contribute
=================

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from flask import Blueprint, request
from flask.ext.rest import RESTResource, need_auth
from flask_rest import RESTResource, need_auth
from models import db, Project, Person, Bill
from forms import (ProjectForm, EditProjectForm, MemberForm,

View file

@ -1,7 +1,7 @@
from flask.ext.wtf import DateField, DecimalField, Email, Form, PasswordField, \
from flask_wtf import DateField, DecimalField, Email, Form, PasswordField, \
Required, SelectField, SelectMultipleField, SubmitField, TextAreaField, \
TextField, ValidationError
from flask.ext.babel import lazy_gettext as _
from flask_babel import lazy_gettext as _
from flask import request
from wtforms.widgets import html_params
@ -10,28 +10,6 @@ from datetime import datetime
from jinja2 import Markup
from utils import slugify
def select_multi_checkbox(field, ul_class='', **kwargs):
kwargs.setdefault('type', 'checkbox')
field_id = kwargs.pop('id', field.id)
html = [u'<ul %s>' % html_params(id=field_id, class_="inputs-list")]
choice_id = u'toggleField'
js_function = u'toggle();'
options = dict(kwargs, id=choice_id, onclick=js_function)
html.append(u'<p><a id="selectall" onclick="selectall()">%s</a> | <a id="selectnone" onclick="selectnone()">%s</a></p>'% (_("Select all"), _("Select none")))
for value, label, checked in field.iter_choices():
choice_id = u'%s-%s' % (field_id, value)
options = dict(kwargs, name=field.name, value=value, id=choice_id)
if checked:
options['checked'] = 'checked'
html.append(u'<p><label for="%s">%s<span>%s</span></label></p>'
% (choice_id, '<input %s /> ' % html_params(**options), label))
html.append(u'</ul>')
return u''.join(html)
def get_billform_for(project, set_default=True, **kwargs):
"""Return an instance of BillForm configured for a particular project.
@ -118,7 +96,7 @@ class BillForm(Form):
payer = SelectField(_("Payer"), validators=[Required()], coerce=int)
amount = CommaDecimalField(_("Amount paid"), validators=[Required()])
payed_for = SelectMultipleField(_("For whom?"),
validators=[Required()], widget=select_multi_checkbox, coerce=int)
validators=[Required()], coerce=int)
submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one"))
@ -143,9 +121,7 @@ class BillForm(Form):
self.payed_for.data = self.payed_for.default
def validate_amount(self, field):
if field.data < 0:
field.data = abs(field.data)
elif field.data == 0:
if field.data == 0:
raise ValidationError(_("Bills can't be null"))
@ -198,3 +174,16 @@ class CreateArchiveForm(Form):
name = TextField(_("Name for this archive (optional)"), validators=[])
start_date = DateField(_("Start date"), validators=[Required()])
end_date = DateField(_("End date"), validators=[Required()], default=datetime.now)
class ExportForm(Form):
export_type = SelectField(_("What do you want to download ?"),
validators=[Required()],
coerce=str,
choices=[("bills", _("bills")), ("transactions", _("transactions"))]
)
export_format = SelectField(_("Export file format"),
validators=[Required()],
coerce=str,
choices=[("csv", "csv"), ("json", "json")]
)

View file

@ -55,7 +55,7 @@ def run_migrations_online():
# This callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema.
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
# reference: https://alembic.readthedocs.io/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]

View file

@ -1,7 +1,7 @@
from collections import defaultdict
from datetime import datetime
from flask.ext.sqlalchemy import SQLAlchemy, BaseQuery
from flask_sqlalchemy import SQLAlchemy, BaseQuery
from flask import g
from sqlalchemy import orm
@ -37,7 +37,7 @@ class Project(db.Model):
# for each person
for person in self.members:
# get the list of bills he has to pay
bills = Bill.query.filter(Bill.owers.contains(person))
bills = Bill.query.options(orm.subqueryload(Bill.owers)).filter(Bill.owers.contains(person))
for bill in bills.all():
if person != bill.payer:
share = bill.pay_each() * person.weight
@ -54,16 +54,28 @@ class Project(db.Model):
def uses_weights(self):
return len([i for i in self.members if i.weight != 1]) > 0
def get_transactions_to_settle_bill(self):
def get_transactions_to_settle_bill(self, pretty_output=False):
"""Return a list of transactions that could be made to settle the bill"""
def prettify(transactions, pretty_output):
""" Return pretty transactions
"""
if not pretty_output:
return transactions
pretty_transactions = []
for transaction in transactions:
pretty_transactions.append({'ower': transaction['ower'].name,
'receiver': transaction['receiver'].name,
'amount': round(transaction['amount'], 2)})
return pretty_transactions
#cache value for better performance
balance = self.balance
credits, debts, transactions = [],[],[]
# Create lists of credits and debts
for person in self.members:
if balance[person.id] > 0:
if round(balance[person.id], 2) > 0:
credits.append({"person": person, "balance": balance[person.id]})
elif balance[person.id] < 0:
elif round(balance[person.id], 2) < 0:
debts.append({"person": person, "balance": -balance[person.id]})
# Try and find exact matches
for credit in credits:
@ -83,7 +95,8 @@ class Project(db.Model):
transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": credits[0]["balance"]})
debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"]
del credits[0]
return transactions
return prettify(transactions, pretty_output)
def exactmatch(self, credit, debts):
"""Recursively try and find subsets of 'debts' whose sum is equal to credit"""
@ -111,7 +124,25 @@ class Project(db.Model):
.filter(Bill.payer_id == Person.id)\
.filter(Person.project_id == Project.id)\
.filter(Project.id == self.id)\
.order_by(Bill.date.desc())
.order_by(Bill.date.desc())\
.order_by(Bill.id.desc())
def get_pretty_bills(self, export_format="json"):
"""Return a list of project's bills with pretty formatting"""
bills = self.get_bills()
pretty_bills = []
for bill in bills:
if export_format == "json":
owers = [ower.name for ower in bill.owers]
else:
owers = ', '.join([ower.name for ower in bill.owers])
pretty_bills.append({"what": bill.what,
"amount": round(bill.amount, 2),
"date": str(bill.date),
"payer_name": Person.query.get(bill.payer_id).name,
"payer_weight": Person.query.get(bill.payer_id).weight,
"owers": owers})
return pretty_bills
def remove_member(self, member_id):
"""Remove a member from the project.

View file

@ -2,8 +2,8 @@ import os
import warnings
from flask import Flask, g, request, session
from flask.ext.babel import Babel
from flask.ext.migrate import Migrate, upgrade, stamp
from flask_babel import Babel
from flask_migrate import Migrate, upgrade, stamp
from raven.contrib.flask import Sentry
from web import main, db, mail

View file

@ -0,0 +1,707 @@
/*!
* Datepicker for Bootstrap v1.6.4 (https://github.com/eternicode/bootstrap-datepicker)
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
.datepicker {
border-radius: 4px;
direction: ltr;
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
padding: 4px;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid rgba(0, 0, 0, 0.15);
border-top: 0;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-top: 0;
position: absolute;
}
.datepicker-dropdown.datepicker-orient-left:before {
left: 6px;
}
.datepicker-dropdown.datepicker-orient-left:after {
left: 7px;
}
.datepicker-dropdown.datepicker-orient-right:before {
right: 6px;
}
.datepicker-dropdown.datepicker-orient-right:after {
right: 7px;
}
.datepicker-dropdown.datepicker-orient-bottom:before {
top: -7px;
}
.datepicker-dropdown.datepicker-orient-bottom:after {
top: -6px;
}
.datepicker-dropdown.datepicker-orient-top:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid rgba(0, 0, 0, 0.15);
}
.datepicker-dropdown.datepicker-orient-top:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid #fff;
}
.datepicker table {
margin: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.datepicker table tr td,
.datepicker table tr th {
text-align: center;
width: 30px;
height: 30px;
border-radius: 4px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #777777;
}
.datepicker table tr td.day:hover,
.datepicker table tr td.focused {
background: #eeeeee;
cursor: pointer;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
background: none;
color: #777777;
cursor: default;
}
.datepicker table tr td.highlighted {
color: #000;
background-color: #d9edf7;
border-color: #85c5e5;
border-radius: 0;
}
.datepicker table tr td.highlighted:focus,
.datepicker table tr td.highlighted.focus {
color: #000;
background-color: #afd9ee;
border-color: #298fc2;
}
.datepicker table tr td.highlighted:hover {
color: #000;
background-color: #afd9ee;
border-color: #52addb;
}
.datepicker table tr td.highlighted:active,
.datepicker table tr td.highlighted.active {
color: #000;
background-color: #afd9ee;
border-color: #52addb;
}
.datepicker table tr td.highlighted:active:hover,
.datepicker table tr td.highlighted.active:hover,
.datepicker table tr td.highlighted:active:focus,
.datepicker table tr td.highlighted.active:focus,
.datepicker table tr td.highlighted:active.focus,
.datepicker table tr td.highlighted.active.focus {
color: #000;
background-color: #91cbe8;
border-color: #298fc2;
}
.datepicker table tr td.highlighted.disabled:hover,
.datepicker table tr td.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.highlighted:hover,
.datepicker table tr td.highlighted.disabled:focus,
.datepicker table tr td.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.highlighted:focus,
.datepicker table tr td.highlighted.disabled.focus,
.datepicker table tr td.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.highlighted.focus {
background-color: #d9edf7;
border-color: #85c5e5;
}
.datepicker table tr td.highlighted.focused {
background: #afd9ee;
}
.datepicker table tr td.highlighted.disabled,
.datepicker table tr td.highlighted.disabled:active {
background: #d9edf7;
color: #777777;
}
.datepicker table tr td.today {
color: #000;
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today:focus,
.datepicker table tr td.today.focus {
color: #000;
background-color: #ffc966;
border-color: #b37400;
}
.datepicker table tr td.today:hover {
color: #000;
background-color: #ffc966;
border-color: #f59e00;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today.active {
color: #000;
background-color: #ffc966;
border-color: #f59e00;
}
.datepicker table tr td.today:active:hover,
.datepicker table tr td.today.active:hover,
.datepicker table tr td.today:active:focus,
.datepicker table tr td.today.active:focus,
.datepicker table tr td.today:active.focus,
.datepicker table tr td.today.active.focus {
color: #000;
background-color: #ffbc42;
border-color: #b37400;
}
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today[disabled]:hover,
fieldset[disabled] .datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled:focus,
.datepicker table tr td.today[disabled]:focus,
fieldset[disabled] .datepicker table tr td.today:focus,
.datepicker table tr td.today.disabled.focus,
.datepicker table tr td.today[disabled].focus,
fieldset[disabled] .datepicker table tr td.today.focus {
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today.focused {
background: #ffc966;
}
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:active {
background: #ffdb99;
color: #777777;
}
.datepicker table tr td.range {
color: #000;
background-color: #eeeeee;
border-color: #bbbbbb;
border-radius: 0;
}
.datepicker table tr td.range:focus,
.datepicker table tr td.range.focus {
color: #000;
background-color: #d5d5d5;
border-color: #7c7c7c;
}
.datepicker table tr td.range:hover {
color: #000;
background-color: #d5d5d5;
border-color: #9d9d9d;
}
.datepicker table tr td.range:active,
.datepicker table tr td.range.active {
color: #000;
background-color: #d5d5d5;
border-color: #9d9d9d;
}
.datepicker table tr td.range:active:hover,
.datepicker table tr td.range.active:hover,
.datepicker table tr td.range:active:focus,
.datepicker table tr td.range.active:focus,
.datepicker table tr td.range:active.focus,
.datepicker table tr td.range.active.focus {
color: #000;
background-color: #c3c3c3;
border-color: #7c7c7c;
}
.datepicker table tr td.range.disabled:hover,
.datepicker table tr td.range[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled:focus,
.datepicker table tr td.range[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range:focus,
.datepicker table tr td.range.disabled.focus,
.datepicker table tr td.range[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.focus {
background-color: #eeeeee;
border-color: #bbbbbb;
}
.datepicker table tr td.range.focused {
background: #d5d5d5;
}
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:active {
background: #eeeeee;
color: #777777;
}
.datepicker table tr td.range.highlighted {
color: #000;
background-color: #e4eef3;
border-color: #9dc1d3;
}
.datepicker table tr td.range.highlighted:focus,
.datepicker table tr td.range.highlighted.focus {
color: #000;
background-color: #c1d7e3;
border-color: #4b88a6;
}
.datepicker table tr td.range.highlighted:hover {
color: #000;
background-color: #c1d7e3;
border-color: #73a6c0;
}
.datepicker table tr td.range.highlighted:active,
.datepicker table tr td.range.highlighted.active {
color: #000;
background-color: #c1d7e3;
border-color: #73a6c0;
}
.datepicker table tr td.range.highlighted:active:hover,
.datepicker table tr td.range.highlighted.active:hover,
.datepicker table tr td.range.highlighted:active:focus,
.datepicker table tr td.range.highlighted.active:focus,
.datepicker table tr td.range.highlighted:active.focus,
.datepicker table tr td.range.highlighted.active.focus {
color: #000;
background-color: #a8c8d8;
border-color: #4b88a6;
}
.datepicker table tr td.range.highlighted.disabled:hover,
.datepicker table tr td.range.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range.highlighted:hover,
.datepicker table tr td.range.highlighted.disabled:focus,
.datepicker table tr td.range.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range.highlighted:focus,
.datepicker table tr td.range.highlighted.disabled.focus,
.datepicker table tr td.range.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
background-color: #e4eef3;
border-color: #9dc1d3;
}
.datepicker table tr td.range.highlighted.focused {
background: #c1d7e3;
}
.datepicker table tr td.range.highlighted.disabled,
.datepicker table tr td.range.highlighted.disabled:active {
background: #e4eef3;
color: #777777;
}
.datepicker table tr td.range.today {
color: #000;
background-color: #f7ca77;
border-color: #f1a417;
}
.datepicker table tr td.range.today:focus,
.datepicker table tr td.range.today.focus {
color: #000;
background-color: #f4b747;
border-color: #815608;
}
.datepicker table tr td.range.today:hover {
color: #000;
background-color: #f4b747;
border-color: #bf800c;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today.active {
color: #000;
background-color: #f4b747;
border-color: #bf800c;
}
.datepicker table tr td.range.today:active:hover,
.datepicker table tr td.range.today.active:hover,
.datepicker table tr td.range.today:active:focus,
.datepicker table tr td.range.today.active:focus,
.datepicker table tr td.range.today:active.focus,
.datepicker table tr td.range.today.active.focus {
color: #000;
background-color: #f2aa25;
border-color: #815608;
}
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled:focus,
.datepicker table tr td.range.today[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range.today:focus,
.datepicker table tr td.range.today.disabled.focus,
.datepicker table tr td.range.today[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.today.focus {
background-color: #f7ca77;
border-color: #f1a417;
}
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:active {
background: #f7ca77;
color: #777777;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected.highlighted {
color: #fff;
background-color: #777777;
border-color: #555555;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:focus,
.datepicker table tr td.selected.highlighted:focus,
.datepicker table tr td.selected.focus,
.datepicker table tr td.selected.highlighted.focus {
color: #fff;
background-color: #5e5e5e;
border-color: #161616;
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.highlighted:hover {
color: #fff;
background-color: #5e5e5e;
border-color: #373737;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected.highlighted:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected.highlighted.active {
color: #fff;
background-color: #5e5e5e;
border-color: #373737;
}
.datepicker table tr td.selected:active:hover,
.datepicker table tr td.selected.highlighted:active:hover,
.datepicker table tr td.selected.active:hover,
.datepicker table tr td.selected.highlighted.active:hover,
.datepicker table tr td.selected:active:focus,
.datepicker table tr td.selected.highlighted:active:focus,
.datepicker table tr td.selected.active:focus,
.datepicker table tr td.selected.highlighted.active:focus,
.datepicker table tr td.selected:active.focus,
.datepicker table tr td.selected.highlighted:active.focus,
.datepicker table tr td.selected.active.focus,
.datepicker table tr td.selected.highlighted.active.focus {
color: #fff;
background-color: #4c4c4c;
border-color: #161616;
}
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.highlighted.disabled:hover,
.datepicker table tr td.selected[disabled]:hover,
.datepicker table tr td.selected.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.selected:hover,
fieldset[disabled] .datepicker table tr td.selected.highlighted:hover,
.datepicker table tr td.selected.disabled:focus,
.datepicker table tr td.selected.highlighted.disabled:focus,
.datepicker table tr td.selected[disabled]:focus,
.datepicker table tr td.selected.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.selected:focus,
fieldset[disabled] .datepicker table tr td.selected.highlighted:focus,
.datepicker table tr td.selected.disabled.focus,
.datepicker table tr td.selected.highlighted.disabled.focus,
.datepicker table tr td.selected[disabled].focus,
.datepicker table tr td.selected.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.selected.focus,
fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
background-color: #777777;
border-color: #555555;
}
.datepicker table tr td.active,
.datepicker table tr td.active.highlighted {
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:focus,
.datepicker table tr td.active.highlighted:focus,
.datepicker table tr td.active.focus,
.datepicker table tr td.active.highlighted.focus {
color: #fff;
background-color: #286090;
border-color: #122b40;
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active.highlighted:hover {
color: #fff;
background-color: #286090;
border-color: #204d74;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active.highlighted:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active.highlighted.active {
color: #fff;
background-color: #286090;
border-color: #204d74;
}
.datepicker table tr td.active:active:hover,
.datepicker table tr td.active.highlighted:active:hover,
.datepicker table tr td.active.active:hover,
.datepicker table tr td.active.highlighted.active:hover,
.datepicker table tr td.active:active:focus,
.datepicker table tr td.active.highlighted:active:focus,
.datepicker table tr td.active.active:focus,
.datepicker table tr td.active.highlighted.active:focus,
.datepicker table tr td.active:active.focus,
.datepicker table tr td.active.highlighted:active.focus,
.datepicker table tr td.active.active.focus,
.datepicker table tr td.active.highlighted.active.focus {
color: #fff;
background-color: #204d74;
border-color: #122b40;
}
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.highlighted.disabled:hover,
.datepicker table tr td.active[disabled]:hover,
.datepicker table tr td.active.highlighted[disabled]:hover,
fieldset[disabled] .datepicker table tr td.active:hover,
fieldset[disabled] .datepicker table tr td.active.highlighted:hover,
.datepicker table tr td.active.disabled:focus,
.datepicker table tr td.active.highlighted.disabled:focus,
.datepicker table tr td.active[disabled]:focus,
.datepicker table tr td.active.highlighted[disabled]:focus,
fieldset[disabled] .datepicker table tr td.active:focus,
fieldset[disabled] .datepicker table tr td.active.highlighted:focus,
.datepicker table tr td.active.disabled.focus,
.datepicker table tr td.active.highlighted.disabled.focus,
.datepicker table tr td.active[disabled].focus,
.datepicker table tr td.active.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.active.focus,
fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
background-color: #337ab7;
border-color: #2e6da4;
}
.datepicker table tr td span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
border-radius: 4px;
}
.datepicker table tr td span:hover,
.datepicker table tr td span.focused {
background: #eeeeee;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
background: none;
color: #777777;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:focus,
.datepicker table tr td span.active:hover:focus,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active.focus,
.datepicker table tr td span.active:hover.focus,
.datepicker table tr td span.active.disabled.focus,
.datepicker table tr td span.active.disabled:hover.focus {
color: #fff;
background-color: #286090;
border-color: #122b40;
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover {
color: #fff;
background-color: #286090;
border-color: #204d74;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active {
color: #fff;
background-color: #286090;
border-color: #204d74;
}
.datepicker table tr td span.active:active:hover,
.datepicker table tr td span.active:hover:active:hover,
.datepicker table tr td span.active.disabled:active:hover,
.datepicker table tr td span.active.disabled:hover:active:hover,
.datepicker table tr td span.active.active:hover,
.datepicker table tr td span.active:hover.active:hover,
.datepicker table tr td span.active.disabled.active:hover,
.datepicker table tr td span.active.disabled:hover.active:hover,
.datepicker table tr td span.active:active:focus,
.datepicker table tr td span.active:hover:active:focus,
.datepicker table tr td span.active.disabled:active:focus,
.datepicker table tr td span.active.disabled:hover:active:focus,
.datepicker table tr td span.active.active:focus,
.datepicker table tr td span.active:hover.active:focus,
.datepicker table tr td span.active.disabled.active:focus,
.datepicker table tr td span.active.disabled:hover.active:focus,
.datepicker table tr td span.active:active.focus,
.datepicker table tr td span.active:hover:active.focus,
.datepicker table tr td span.active.disabled:active.focus,
.datepicker table tr td span.active.disabled:hover:active.focus,
.datepicker table tr td span.active.active.focus,
.datepicker table tr td span.active:hover.active.focus,
.datepicker table tr td span.active.disabled.active.focus,
.datepicker table tr td span.active.disabled:hover.active.focus {
color: #fff;
background-color: #204d74;
border-color: #122b40;
}
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active:hover.disabled:hover,
.datepicker table tr td span.active.disabled.disabled:hover,
.datepicker table tr td span.active.disabled:hover.disabled:hover,
.datepicker table tr td span.active[disabled]:hover,
.datepicker table tr td span.active:hover[disabled]:hover,
.datepicker table tr td span.active.disabled[disabled]:hover,
.datepicker table tr td span.active.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td span.active:hover,
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active:hover.disabled:focus,
.datepicker table tr td span.active.disabled.disabled:focus,
.datepicker table tr td span.active.disabled:hover.disabled:focus,
.datepicker table tr td span.active[disabled]:focus,
.datepicker table tr td span.active:hover[disabled]:focus,
.datepicker table tr td span.active.disabled[disabled]:focus,
.datepicker table tr td span.active.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td span.active:focus,
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active.disabled.focus,
.datepicker table tr td span.active:hover.disabled.focus,
.datepicker table tr td span.active.disabled.disabled.focus,
.datepicker table tr td span.active.disabled:hover.disabled.focus,
.datepicker table tr td span.active[disabled].focus,
.datepicker table tr td span.active:hover[disabled].focus,
.datepicker table tr td span.active.disabled[disabled].focus,
.datepicker table tr td span.active.disabled:hover[disabled].focus,
fieldset[disabled] .datepicker table tr td span.active.focus,
fieldset[disabled] .datepicker table tr td span.active:hover.focus,
fieldset[disabled] .datepicker table tr td span.active.disabled.focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus {
background-color: #337ab7;
border-color: #2e6da4;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #777777;
}
.datepicker .datepicker-switch {
width: 145px;
}
.datepicker .datepicker-switch,
.datepicker .prev,
.datepicker .next,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker .datepicker-switch:hover,
.datepicker .prev:hover,
.datepicker .next:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.input-group.date .input-group-addon {
cursor: pointer;
}
.input-daterange {
width: 100%;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
border-radius: 3px 0 0 3px;
}
.input-daterange input:last-child {
border-radius: 0 3px 3px 0;
}
.input-daterange .input-group-addon {
width: auto;
min-width: 16px;
padding: 4px 5px;
line-height: 1.42857143;
text-shadow: 0 1px 0 #fff;
border-width: 1px 0;
margin-left: -5px;
margin-right: -5px;
}
.datepicker.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
list-style: none;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
color: #333333;
font-size: 13px;
line-height: 1.42857143;
}
.datepicker.dropdown-menu th,
.datepicker.datepicker-inline th,
.datepicker.dropdown-menu td,
.datepicker.datepicker-inline td {
padding: 0px 5px;
}
/*# sourceMappingURL=bootstrap-datepicker3.standalone.css.map */

File diff suppressed because one or more lines are too long

View file

@ -1,224 +0,0 @@
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
top: 0;
left: 0;
padding: 4px;
margin-top: 1px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
}
.datepicker:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
top: -7px;
left: 6px;
}
.datepicker:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #ffffff;
position: absolute;
top: -6px;
left: 7px;
}
.datepicker > div {
display: none;
}
.datepicker.days div.datepicker-days {
display: block;
}
.datepicker.months div.datepicker-months {
display: block;
}
.datepicker.years div.datepicker-years {
display: block;
}
.datepicker table {
width: 100%;
margin: 0;
}
.datepicker td,
.datepicker th {
text-align: center;
width: 20px;
height: 20px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker td.day:hover {
background: #eeeeee;
cursor: pointer;
}
.datepicker td.old,
.datepicker td.new {
color: #999999;
}
.datepicker td.disabled,
.datepicker td.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker td.active,
.datepicker td.active:hover,
.datepicker td.active.disabled,
.datepicker td.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:dximagetransform.microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker td.active:hover,
.datepicker td.active:hover:hover,
.datepicker td.active.disabled:hover,
.datepicker td.active.disabled:hover:hover,
.datepicker td.active:active,
.datepicker td.active:hover:active,
.datepicker td.active.disabled:active,
.datepicker td.active.disabled:hover:active,
.datepicker td.active.active,
.datepicker td.active:hover.active,
.datepicker td.active.disabled.active,
.datepicker td.active.disabled:hover.active,
.datepicker td.active.disabled,
.datepicker td.active:hover.disabled,
.datepicker td.active.disabled.disabled,
.datepicker td.active.disabled:hover.disabled,
.datepicker td.active[disabled],
.datepicker td.active:hover[disabled],
.datepicker td.active.disabled[disabled],
.datepicker td.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker td.active:active,
.datepicker td.active:hover:active,
.datepicker td.active.disabled:active,
.datepicker td.active.disabled:hover:active,
.datepicker td.active.active,
.datepicker td.active:hover.active,
.datepicker td.active.disabled.active,
.datepicker td.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker td span {
display: block;
width: 47px;
height: 54px;
line-height: 54px;
float: left;
margin: 2px;
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker td span:hover {
background: #eeeeee;
}
.datepicker td span.disabled,
.datepicker td span.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker td span.active,
.datepicker td span.active:hover,
.datepicker td span.active.disabled,
.datepicker td span.active.disabled:hover {
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(top, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
filter: progid:dximagetransform.microsoft.gradient(enabled=false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker td span.active:hover,
.datepicker td span.active:hover:hover,
.datepicker td span.active.disabled:hover,
.datepicker td span.active.disabled:hover:hover,
.datepicker td span.active:active,
.datepicker td span.active:hover:active,
.datepicker td span.active.disabled:active,
.datepicker td span.active.disabled:hover:active,
.datepicker td span.active.active,
.datepicker td span.active:hover.active,
.datepicker td span.active.disabled.active,
.datepicker td span.active.disabled:hover.active,
.datepicker td span.active.disabled,
.datepicker td span.active:hover.disabled,
.datepicker td span.active.disabled.disabled,
.datepicker td span.active.disabled:hover.disabled,
.datepicker td span.active[disabled],
.datepicker td span.active:hover[disabled],
.datepicker td span.active.disabled[disabled],
.datepicker td span.active.disabled:hover[disabled] {
background-color: #0044cc;
}
.datepicker td span.active:active,
.datepicker td span.active:hover:active,
.datepicker td span.active.disabled:active,
.datepicker td span.active.disabled:hover:active,
.datepicker td span.active.active,
.datepicker td span.active:hover.active,
.datepicker td span.active.disabled.active,
.datepicker td span.active.disabled:hover.active {
background-color: #003399 \9;
}
.datepicker td span.old {
color: #999999;
}
.datepicker th.switch {
width: 145px;
}
.datepicker thead tr:first-child th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover {
background: #eeeeee;
}
.input-append.date .add-on i,
.input-prepend.date .add-on i {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}

View file

@ -1,19 +1,29 @@
@import "bootstrap.min.css";
@import "datepicker.css";
@import "bootstrap-datepicker3.standalone.css";
@import "../fonts/fontfaces.css";
/* General */
body {
margin-top: 40px;
/* For fixed navbar */
padding-top: 3.5rem;
padding-bottom: 2rem;
}
/* Navbar */
.navbar h1{ margin-left: 75px; }
.navbar h1 {
font-size: 1rem;
margin: 0;
padding: 0;
}
.navbar .primary-nav { padding-left: 75px; }
.navbar .secondary-nav { padding-right: 75px; }
.brand{ font-family: 'Lobster', arial, serif; }
.navbar .secondary-nav {
text-align: right;
flex-direction: row-reverse;
}
.navbar-brand{ font-family: 'Lobster', arial, serif; }
/* Header */
@ -36,6 +46,14 @@ body {
#header .tryout {
margin-right: 10em;
color: #fff;
background-color: #414141;
border-color: #414141;
}
#header .tryout:hover {
background-color: #606060;
border-color: #606060;
}
#header .additional-content {
@ -55,28 +73,20 @@ body {
background-position: center bottom;
background-repeat: no-repeat;
height: 100%;
width: 230px;
padding-left: 10px;
padding-right: 20px;
padding-top: 10px;
margin-left: -20px;
margin-top: -10px;
margin-right: 15px;
color: black;
position: fixed;
}
#add-member-form { padding-top: 1em; padding-bottom: 1em; }
#add-member-form input[type="text"] { width: 60%; }
#add-member-form button { width: 35%; }
#table_overflow { overflow-y: auto; overflow-x: hidden; width: 235px; }
#table_overflow { overflow-y: auto; overflow-x: hidden;}
/* Content */
.content {
padding-top: 1em;
padding-left: 250px;
margin-top: 1rem;
}
/* Home */
@ -94,7 +104,9 @@ body {
height: 100px;
}
#footer{
footer{
margin-left: -15px;
margin-right: -15px;
margin-top: 30px;
position: fixed;
bottom: 0px;
@ -109,6 +121,16 @@ body {
float: right;
}
#new-bill, .identifier {
margin-top: 16px;
margin-bottom: 16px;
}
/* Avoid text color flickering when it loose focus as the modal appears */
.btn-primary[data-toggle="modal"] {
color: #fff;
}
.password-reminder{
float: right;
margin-right: 20px;
@ -186,6 +208,10 @@ tr.payer_line .balance-name{
color: white;
}
.balance.table {
table-layout: fixed;
}
#bill-form > fieldset {
margin-top: 10px;
}
@ -206,10 +232,6 @@ tr:hover .extra-info {
display: inline;
}
.modal-body {
max-height:455px;
}
/* Fluid Offsets for Boostrap */
.row-fluid > [class*="span"]:not([class*="offset"]):first-child{margin-left:0;}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
!function(a){a.fn.datepicker.dates.fr={days:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],daysShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],daysMin:["d","l","ma","me","j","v","s"],months:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthsShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],today:"Aujourd'hui",monthsTitle:"Mois",clear:"Effacer",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);
console.log("foo", jQuery.fn.datepicker.dates);

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
{% for field_name, field_errors in form.errors.items() if field_errors %}
{% for error in field_errors %}
<p class="error">{{ form[field_name].label.text }}: {{ error }}</p>
<p class="alert alert-danger"><strong>{{ form[field_name].label.text }}:</strong> {{ error }}</p>
{% endfor %}
{% endfor %}

View file

@ -10,6 +10,10 @@
{% block content %}
<h2>{{ _("Edit this project") }}</h2>
<form class="form-horizontal" method="post">
{{ forms.edit_project(form) }}
{{ forms.edit_project(edit_form) }}
</form></br>
<h2>{{ _("Download this project's data") }}</h2>
<form class="form-horizontal" method="post">
{{ forms.export_project(export_form) }}
</form>
{% endblock %}

View file

@ -1,9 +1,13 @@
{% macro input(field, multiple=False, class=None) -%}
<div class="control-group">
{% macro input(field, multiple=False, class='form-control', inline=False) -%}
<div class="form-group{% if inline %} row{% endif %}">
{% if field.type != "SubmitField" %}
{{ field.label(class="control-label") }}
{% if inline %}
{{ field.label(class="col-3") }}
{% else %}
{{ field.label() }}
{% endif %}
<div class="controls">
{% endif %}
<div class="controls{% if inline %} col-9{% endif %}">
{% if multiple == True %}
{{ field(multiple=True, class=class) }}
{% else %}
@ -77,11 +81,22 @@
{% if title %}<legend>{% if edit %}{{ _("Edit this bill") }} {% else %}{{ _("Add a bill") }} {% endif %}</legend>{% endif %}
{% include "display_errors.html" %}
{{ form.hidden_tag() }}
{{ input(form.date, class="datepicker") }}
{{ input(form.what) }}
{{ input(form.payer) }}
{{ input(form.amount) }}
{{ input(form.payed_for) }}
{{ input(form.date, class="form-control datepicker", inline=True) }}
{{ input(form.what, inline=True) }}
{{ input(form.payer, inline=True, class="form-control custom-select") }}
{{ input(form.amount, inline=True) }}
<div class="form-group row">
<label class="col-3" for="payed_for">{{ _("For whom?") }}</label>
<div class="controls col-9">
<ul id="payed_for" class="inputs-list">
<p><a href="#" id="selectall" onclick="selectall()">{{ _("Select all") }}</a> | <a href="#" id="selectnone" onclick="selectnone()">{{_("Select none")}}</a></p>
{% for key, value, checked in form.payed_for.iter_choices() %}
<p class="form-check"><label for="payed_for-{{key}}" class="form-check-label"><input name="payed_for" type="checkbox" {% if checked %}checked{% endif %} class="form-check-input" value="{{key}}" id="payed_for-{{key}}"/><span>{{value}}</span></label></p>
{% endfor %}
</ul>
</div>
</div>
</fieldset>
<div class="actions">
{{ form.submit(class="btn btn-primary") }}
@ -92,7 +107,11 @@
{% macro add_member(form) %}
{{ form.hidden_tag() }}
{{ form.name(placeholder=_("Type user name here")) }}<button class="btn">{{ _("Add") }}</button>
<div class="input-group">
<label class="sr-only" for="name">_("Type user name here")</label>
{{ form.name(placeholder=_("Type user name here"), class="form-control") }}
<button class=" input-group-addon btn">{{ _("Add") }}</button>
</div>
{% endmacro %}
{% macro edit_member(form, title=True) %}
@ -131,6 +150,17 @@
</div>
{% endmacro %}
{% macro export_project(form) %}
<fieldset>
{{ form.hidden_tag() }}
{{ input(form.export_type) }}
{{ input(form.export_format) }}
</fieldset>
<div class="actions">
<button class="btn btn-primary">{{ _("Download") }}</button>
</div>
{% endmacro %}
{% macro remind_password(form) %}
{% include "display_errors.html" %}

View file

@ -1,26 +1,21 @@
{% extends "layout.html" %}
{% block header %}
<div id="header" class="container-fluid">
<div class="row-fluid">
<div class="span5 offset2">
{% block body %}
<header id="header" class="row">
<div class="col-5 offset-md-2">
<h2>{{ _("Manage your shared <br>expenses, easily") }}</h2>
<a href="{{ url_for(".demo") }}" class="tryout btn btn-inverse pull-right">{{ _("Try out the demo") }}</a>
<a href="{{ url_for(".demo") }}" class="tryout btn">{{ _("Try out the demo") }}</a>
</div>
<div class="span4">
<div class="col-4">
<p class="additional-content">{{ _("You're sharing a house?") }}<br /> {{ _("Going on holidays with friends?") }}<br /> {{ _("Simply sharing money with others?") }} <br /><strong>{{ _("We can help!") }}</strong></p>
</div>
</div>
</div>
{% endblock %}
</header>
{% block body %}
<div class="container-fluid">
<div class="row-fluid home">
<div class="span4 offset2">
<main class="row home">
<div class="col-4 offset-md-2">
<form id="authentication-form" class="form-horizontal" action="{{ url_for(".authenticate") }}" method="post">
<fieldset>
<fieldset class="form-group">
<legend>{{ _("Log to an existing project") }}...</legend>
{{ forms.authenticate(auth_form, home=True) }}
</fieldset>
@ -30,9 +25,9 @@
</div>
</form>
</div>
<div class="span4">
<div class="col-3 offset-md-1">
<form id="creation-form" class="form-horizontal" action="{{ url_for(".create_project") }}" method="post">
<fieldset>
<fieldset class="form-group">
<legend>...{{ _("or create a new one") }}</legend>
{{ forms.create_project(project_form, home=True) }}
</fieldset>
@ -40,8 +35,7 @@
<button class="btn" type="submit">{{ _("let's get started") }}</button>
</div>
</form>
</div>
</div>
</main>
</div>
{% endblock %}

View file

@ -5,15 +5,15 @@
<title>{{ _("Account manager") }}{% block title %}{% endblock %}</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}">
<script src="{{ url_for("static", filename="js/jquery-1.7.2.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/jquery-3.1.1.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/ihatemoney.js") }}"></script>
<script src="{{ url_for("static", filename="js/tether.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/bootstrap.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/bootstrap-datepicker.js") }}"></script>
{% block head %}{% endblock %}
<script type="text/javascript" charset="utf-8">
$(document).ready(function(){
var left = window.innerWidth/2-$('.flash').width()/2;
$(".flash").css({ "left": left+"px", "top":"45px" });
$(".flash").css({ "left": left+"px", "top":"0.6rem" });
setTimeout(function(){
$(".flash").fadeOut("slow", function () {
$(".flash").remove();
@ -35,62 +35,58 @@
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<h1><a class="brand" href="{{ url_for(".home") }}">#! money?</a></h1>
<nav class="navbar navbar-toggleable-md navbar fixed-top navbar-inverse bg-inverse">
<h1 class="col-2"><a class="navbar-brand" href="{{ url_for(".home") }}">#! money?</a></h1>
<ul class="navbar-nav col-5 offset-md-1">
{% if g.project %}
<ul class="nav primary-nav">
{% block navbar %}
<li class="active"><a href="{{ url_for(".list_bills") }}">{{ _("Bills") }}</a></li>
<li><a href="{{ url_for(".settle_bill") }}">{{ _("Settle") }}</a></li>
<li class="nav-item{% if current_view == 'list_bills' %} active{% endif %}"><a class="nav-link" href="{{ url_for(".list_bills") }}">{{ _("Bills") }}</a></li>
<li class="nav-item{% if current_view == 'settle_bill' %} active{% endif %}"><a class="nav-link" href="{{ url_for(".settle_bill") }}">{{ _("Settle") }}</a></li>
{% endblock %}
</ul>
{% endif %}
<ul class="nav pull-right secondary-nav">
</ul>
<ul class="navbar-nav secondary-nav col-4">
{% if g.project %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><strong>{{ g.project.name }}</strong> {{ _("options") }} <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{{ url_for(".edit_project") }}">{{ _("Project settings") }}</a></li>
<li class="divider"></li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><strong>{{ g.project.name }}</strong> {{ _("options") }} <b class="caret"></b></a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><a class="dropdown-item" href="{{ url_for(".edit_project") }}">{{ _("Project settings") }}</a></li>
<li class="dropdown-divider"></li>
{% for id, name in session['projects'] %}
{% if id != g.project.id %}
<li><a href="{{ url_for(".list_bills", project_id=id) }}">{{ _("switch to") }} {{ name }}</a></li>
<li><a class="dropdown-item" href="{{ url_for(".list_bills", project_id=id) }}">{{ _("switch to") }} {{ name }}</a></li>
{% endif %}
{% endfor %}
<li><a href="{{ url_for(".create_project") }}">{{ _("Start a new project") }}</a></li>
<li class="divider"></li>
<li><a href="{{ url_for(".exit") }}">{{ _("Logout") }}</a></li>
<li><a class="dropdown-item" href="{{ url_for(".create_project") }}">{{ _("Start a new project") }}</a></li>
<li class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for(".exit") }}">{{ _("Logout") }}</a></li>
</ul>
</li>
{% endif %}
<li{% if g.lang == "fr" %} class="active"{% endif %}><a href="{{ url_for(".change_lang", lang="fr") }}">fr</a></li>
<li{% if g.lang == "en" %} class="active"{% endif %}><a href="{{ url_for(".change_lang", lang="en") }}">en</a></li>
<li class="nav-item{% if g.lang == "fr" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".change_lang", lang="fr") }}">fr</a></li>
<li class="nav-item{% if g.lang == "en" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".change_lang", lang="en") }}">en</a></li>
</ul>
</div>
</div>
</div>
{% block header %}{% endblock %}
{% block body %}
</nav>
<div class="container-fluid">
{% block body %}
{% block sidebar %}{% endblock %}
<div class="content">
<main class="content offset-1 col-10">
{% block content %}{% endblock %}
</div>
</main>
</div>
{% endblock %}
{% for message in get_flashed_messages() %}
<div class="flash alert alert-success"><p>{{ message }}</p></div>
<div class="flash alert alert-success">{{ message }}</div>
{% endfor %}
{% block footer %}
<div id="footer">
<footer>
<p><a href="https://github.com/spiral-project/ihatemoney">{{ _("This is a free software") }}</a>, {{ _("you can contribute and improve it!") }}</p>
</div>
</footer>
{% endblock %}
</body>

View file

@ -1,16 +1,11 @@
{% extends "layout.html" %}
{% extends "sidebar_table_layout.html" %}
{% block title %} - {{ g.project.name }}{% endblock %}
{% block head %}
<script src="{{ url_for("static", filename="js/bootstrap-datepicker.js") }}"></script>
<script src="{{ url_for("static", filename="js/locales/bootstrap-datepicker.fr.min.js") }}" charset="utf-8"></script>
{% endblock %}
{% block js %}
$(window).resize(function() {
$("#sidebar").height( window.innerHeight-50 );
$("#table_overflow").height( $("#sidebar").height()-120 );
});
{% if add_bill %} $('#new-bill').click(); {% endif %}
// Hide all members actions
@ -56,16 +51,14 @@
{% endblock %}
{% block sidebar %}
<div id="sidebar" class="sidebar">
<form id="add-member-form" action="{{ url_for(".add_member") }}" method="post" class="form-inline input-append">
<form id="add-member-form" action="{{ url_for(".add_member") }}" method="post" class="form-inline">
{{ forms.add_member(member_form) }}
</form>
<div id="table_overflow">
<table class="balance table">
{% set balance = g.project.balance %}
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id] != 0 %}
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
<td class="balance-name">{{ member.name }}
<span class="light{% if not g.project.uses_weights %} extra-info{% endif %}">(x{{ member.weight|minimal_round(1) }})</span>
@ -82,37 +75,39 @@
<form class="action reactivate" action="{{ url_for(".reactivate", member_id=member.id) }}" method="POST">
<button type="submit">{{ _("reactivate") }}</button></form></td>
{% endif %}
<td class="balance-value {% if balance[member.id] > 0 %}positive{% elif balance[member.id] < 0 %}negative{% endif %}">
{% if balance[member.id] > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}
{% block content %}
<div class="identifier">{{ _("The project identifier is") }} <a href="{{ url_for(".list_bills") }}">{{ g.project.id }}</a>, {{ _("remember it!") }}</div>
<a id="new-bill" href="{{ url_for(".add_bill") }}" class="btn btn-primary" data-toggle="modal" data-target="#bill-form">{{ _("Add a new bill") }}</a>
<div id="bill-form" class="modal hide">
<div id="bill-form" class="modal fade show" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ _('Add a bill') }}</h3>
<a href="#" class="close" data-dismiss="modal">&times;</a>
<h3>{{ _('Add a bill') }}</h3>
</div>
<form action="{{ url_for(".add_bill") }}" method="post" class="modal-body form-horizontal">
<form action="{{ url_for(".add_bill") }}" method="post" class="modal-body container">
{{ forms.add_bill(bill_form, title=False) }}
</form>
</div>
</div>
</div>
{% if bills.count() > 0 %}
<table id="bill_table" class="table table-striped">
<table id="bill_table" class="col table table-striped table-hover">
<thead><tr><th>{{ _("When?") }}</th><th>{{ _("Who paid?") }}</th><th>{{ _("For what?") }}</th><th>{{ _("For whom?") }}</th><th>{{ _("How much?") }}</th><th>{{ _("Actions") }}</th></tr></thead>
<tbody>
{% for bill in bills %}
<tr class="{{ loop.cycle("odd", "even") }}" owers={{bill.owers|join(',','id')}} payer={{bill.payer.id}}>
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
<td>{{ bill.date }}</td>
<td>{{ bill.payer }}</td>
<td>{{ bill.what }}</td>

View file

@ -1,28 +1,19 @@
{% extends "layout.html" %}
{% block navbar %}
<li><a href="{{ url_for(".list_bills") }}">{{ _("Bills") }}</a></li>
<li class="active"><a href="{{ url_for(".settle_bill") }}">{{ _("Settle") }}</a></li>
{% endblock %}
{% extends "sidebar_table_layout.html" %}
{% block sidebar %}
<div id="sidebar" class="sidebar">
<div id="table_overflow">
<table class="balance table">
{% set balance = g.project.balance %}
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id] != 0 %}
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
<td class="balance-name">{{ member.name }}</td>
<td class="balance-value {% if balance[member.id] > 0 %}positive{% elif balance[member.id] < 0 %}negative{% endif %}">
{% if balance[member.id] > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "layout.html" %}
{% block body %}
<div class="row" style="height: 100%">
<aside id="sidebar" class="sidebar col-3 " style="height: 100%">
{% block sidebar %}{% endblock %}
</aside>
<main class="offset-md-3 col-9">
{% block content %}{% endblock %}
</main>
</div>
{% endblock %}

View file

@ -398,12 +398,10 @@ class BudgetTestCase(TestCase):
'what': u'fromage à raclette',
'payer': members_ids[0],
'payed_for': members_ids,
# bill with a negative value should be converted to a positive
# value
'amount': '-25'
})
bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0]
self.assertEqual(bill.amount, 25)
self.assertEqual(bill.amount, -25)
#add a bill with a comma
self.app.post("/raclette/add", data={
@ -500,7 +498,10 @@ class BudgetTestCase(TestCase):
result[models.Project.query.get("raclette").members[0].id] = 8.12
result[models.Project.query.get("raclette").members[1].id] = 0.0
result[models.Project.query.get("raclette").members[2].id] = -8.12
self.assertDictEqual(balance, result)
# 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.
for key, value in balance.iteritems():
self.assertEqual(round(value, 2), result[key])
def test_edit_project(self):
# A project should be editable
@ -582,6 +583,156 @@ class BudgetTestCase(TestCase):
self.assertEqual(a, balance[m.id])
return
def test_settle_zero(self):
self.post_project("raclette")
# add members
self.app.post("/raclette/members/add", data={'name': 'alexis'})
self.app.post("/raclette/members/add", data={'name': 'fred'})
self.app.post("/raclette/members/add", data={'name': 'tata'})
# create bills
self.app.post("/raclette/add", data={
'date': '2016-12-31',
'what': u'fromage à raclette',
'payer': 1,
'payed_for': [1, 2, 3],
'amount': '10.0',
})
self.app.post("/raclette/add", data={
'date': '2016-12-31',
'what': u'red wine',
'payer': 2,
'payed_for': [1, 3],
'amount': '20',
})
self.app.post("/raclette/add", data={
'date': '2017-01-01',
'what': u'refund',
'payer': 3,
'payed_for': [2],
'amount': '13.33',
})
project = models.Project.query.get('raclette')
transactions = project.get_transactions_to_settle_bill()
members = defaultdict(int)
# There should not be any zero-amount transfer after rounding
for t in transactions:
rounded_amount = round(t['amount'], 2)
self.assertNotEqual(0.0, rounded_amount,
msg='%f is equal to zero after rounding' % t['amount'])
def test_export(self):
self.post_project("raclette")
# add members
self.app.post("/raclette/members/add", data={'name': 'alexis', 'weight': 2})
self.app.post("/raclette/members/add", data={'name': 'fred'})
self.app.post("/raclette/members/add", data={'name': 'tata'})
self.app.post("/raclette/members/add", data={'name': 'pépé'})
# create bills
self.app.post("/raclette/add", data={
'date': '2016-12-31',
'what': u'fromage à raclette',
'payer': 1,
'payed_for': [1, 2, 3, 4],
'amount': '10.0',
})
self.app.post("/raclette/add", data={
'date': '2016-12-31',
'what': u'red wine',
'payer': 2,
'payed_for': [1, 3],
'amount': '200',
})
self.app.post("/raclette/add", data={
'date': '2017-01-01',
'what': u'refund',
'payer': 3,
'payed_for': [2],
'amount': '13.33',
})
# generate json export of bills
resp = self.app.post("/raclette/edit", data={
'export_format': 'json',
'export_type': 'bills'
})
expected = [{u'date': u'2017-01-01', u'what': u'refund',
u'amount': 13.33, u'payer_name': u'tata', u'payer_weight': 1.0, u'owers': [u'fred']},
{u'date': u'2016-12-31', u'what': u'red wine',
u'amount': 200.0, u'payer_name': u'fred', u'payer_weight': 1.0, u'owers': [u'alexis', u'tata']},
{u'date': u'2016-12-31', u'what': u'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']}]
self.assertEqual(json.loads(resp.data), expected)
# generate csv export of bills
resp = self.app.post("/raclette/edit", data={
'export_format': 'csv',
'export_type': 'bills'
})
expected = ["date,what,amount,payer_name,payer_weight,owers",
"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,fromage à raclette,10.0,alexis,2.0,\"alexis, fred, tata, pépé\""]
received_lines = resp.data.split("\n")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")),
set(received_lines[i].strip("\r").split(","))
)
# generate json export of transactions
resp = self.app.post("/raclette/edit", data={
'export_format': 'json',
'export_type': 'transactions'
})
expected = [{u"amount": 127.33, u"receiver": u"fred", u"ower": u"alexis"},
{u"amount": 55.34, u"receiver": u"fred", u"ower": u"tata"},
{u"amount": 2.00, u"receiver": u"fred", u"ower": u"p\xe9p\xe9"}]
self.assertEqual(json.loads(resp.data), expected)
# generate csv export of transactions
resp = self.app.post("/raclette/edit", data={
'export_format': 'csv',
'export_type': 'transactions'
})
expected = ["amount,receiver,ower",
"127.33,fred,alexis",
"55.34,fred,tata",
"2.0,fred,pépé"]
received_lines = resp.data.split("\n")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")),
set(received_lines[i].strip("\r").split(","))
)
# wrong export_format should return a 200 and export form
resp = self.app.post("/raclette/edit", data={
'export_format': 'wrong_export_format',
'export_type': 'transactions'
})
self.assertEqual(resp.status_code, 200)
self.assertIn('id="export_format" name="export_format"', resp.data)
# wrong export_type should return a 200 and export form
resp = self.app.post("/raclette/edit", data={
'export_format': 'json',
'export_type': 'wrong_export_type'
})
self.assertEqual(resp.status_code, 200)
self.assertIn('id="export_format" name="export_format"', resp.data)
class APITestCase(TestCase):
@ -870,6 +1021,18 @@ class APITestCase(TestCase):
headers=self.get_auth("raclette"))
self.assertStatus(404, req)
def test_username_xss(self):
# create a project
#self.api_create("raclette")
self.post_project("raclette")
self.login("raclette")
# add members
self.api_add_member("raclette", "<script>")
result = self.app.get('/raclette/')
self.assertNotIn("<script>", result.data)
def test_weighted_bills(self):
# create a project
self.api_create("raclette")
@ -936,6 +1099,7 @@ class ServerTestCase(APITestCase):
super(ServerTestCase, self).setUp()
def test_unprefixed(self):
run.app.config['APPLICATION_ROOT'] = '/'
req = self.app.get("/foo/")
self.assertStatus(303, req)

View file

@ -107,7 +107,7 @@ msgstr "Nom"
#: forms.py:155
msgid "Weight"
msgstr "Poids"
msgstr "Parts"
#: forms.py:155 templates/forms.html:95
msgid "Add"
@ -146,6 +146,22 @@ msgstr "Date de départ"
msgid "End date"
msgstr "Date de fin"
#: forms.py:202
msgid "What do you want to download ?"
msgstr "Que voulez-vous télécharger ?"
#: forms.py:205
msgid "bills"
msgstr "factures"
#: forms.py:205
msgid "transactions"
msgstr "remboursements"
#: forms.py:206
msgid "Export file format"
msgstr "Format du fichier d'export"
#: web.py:95
msgid "This private code is not the right one"
msgstr "Le code que vous avez entré n'est pas correct"
@ -292,6 +308,10 @@ msgstr "Ajouter une facture"
msgid "Type user name here"
msgstr "Nouveau participant"
#: templates/forms.html:100
msgid "Edit this member"
msgstr "Éditer ce participant"
#: templates/forms.html:102
msgid "Send the invitations"
msgstr "Envoyer les invitations"
@ -308,6 +328,14 @@ msgstr "Créer une archive"
msgid "Create the archive"
msgstr "Créer l'archive"
#: templates/forms.html:136
msgid "Download this project's data"
msgstr "Télécharger les données de ce projet"
#: templates/forms.html:136
msgid "Download"
msgstr "Télécharger"
#: templates/home.html:8
msgid "Manage your shared <br>expenses, easily"
msgstr "Gérez vos dépenses<br> partagées, facilement"

View file

@ -2,8 +2,12 @@ import re
import inspect
from jinja2 import filters
from json import dumps
from flask import redirect
from werkzeug.routing import HTTPException, RoutingException
from io import BytesIO
import csv
def slugify(value):
@ -77,3 +81,30 @@ def minimal_round(*args, **kw):
# return depending on it
ires = int(res)
return (res if res != ires else ires)
def list_of_dicts2json(dict_to_convert):
"""Take a list of dictionnaries and turns it into
a json in-memory file
"""
bytes_io = BytesIO()
bytes_io.write(dumps(dict_to_convert))
bytes_io.seek(0)
return bytes_io
def list_of_dicts2csv(dict_to_convert):
"""Take a list of dictionnaries and turns it into
a csv in-memory file, assume all dict have the same keys
"""
bytes_io = BytesIO()
try:
csv_data = [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):
csv_data = []
writer = csv.writer(bytes_io)
writer.writerows(csv_data)
bytes_io.seek(0)
return bytes_io

View file

@ -10,18 +10,19 @@ and `add_project_id` for a quick overview)
"""
from flask import Blueprint, current_app, flash, g, redirect, \
render_template, request, session, url_for
from flask.ext.mail import Mail, Message
from flask.ext.babel import get_locale, gettext as _
render_template, request, session, url_for, send_file
from flask_mail import Mail, Message
from flask_babel import get_locale, gettext as _
from smtplib import SMTPRecipientsRefused
import werkzeug
from sqlalchemy import orm
# 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
InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for, \
ExportForm
from utils import Redirect303, list_of_dicts2json, list_of_dicts2csv
main = Blueprint("main", __name__)
mail = Mail()
@ -196,20 +197,43 @@ def remind_password():
@main.route("/<project_id>/edit", methods=["GET", "POST"])
def edit_project():
form = EditProjectForm()
edit_form = EditProjectForm()
export_form = ExportForm()
if request.method == "POST":
if form.validate():
project = form.update(g.project)
if edit_form.validate():
project = edit_form.update(g.project)
db.session.commit()
session[project.id] = project.password
return redirect(url_for(".list_bills"))
else:
form.name.data = g.project.name
form.password.data = g.project.password
form.contact_email.data = g.project.contact_email
return render_template("edit_project.html", form=form)
if export_form.validate():
export_format = export_form.export_format.data
export_type = export_form.export_type.data
if export_type == 'transactions':
export = g.project.get_transactions_to_settle_bill(
pretty_output=True)
if export_type == "bills":
export = g.project.get_pretty_bills(
export_format=export_format)
if export_format == "json":
file2export = list_of_dicts2json(export)
if export_format == "csv":
file2export = list_of_dicts2csv(export)
return send_file(file2export,
attachment_filename="%s-%s.%s" %
(g.project.name, export_type, export_format),
as_attachment=True
)
else:
edit_form.name.data = g.project.name
edit_form.password.data = g.project.password
edit_form.contact_email.data = g.project.contact_email
return render_template("edit_project.html", edit_form=edit_form, export_form=export_form)
@main.route("/<project_id>/delete")
@ -277,12 +301,14 @@ def list_bills():
# set the last selected payer as default choice if exists
if 'last_selected_payer' in session:
bill_form.payer.data = session['last_selected_payer']
bills = g.project.get_bills()
# Preload the "owers" relationship for all bills
bills = g.project.get_bills().options(orm.subqueryload(Bill.owers))
return render_template("list_bills.html",
bills=bills, member_form=MemberForm(g.project),
bill_form=bill_form,
add_bill=request.values.get('add_bill', False)
add_bill=request.values.get('add_bill', False),
current_view="list_bills",
)
@ -412,7 +438,11 @@ def change_lang(lang):
def settle_bill():
"""Compute the sum each one have to pay to each other and display it"""
bills = g.project.get_transactions_to_settle_bill()
return render_template("settle_bills.html", bills=bills)
return render_template(
"settle_bills.html",
bills=bills,
current_view='settle_bill',
)
@main.route("/<project_id>/archives/create", methods=["GET", "POST"])

View file

@ -23,12 +23,9 @@ To interact with bills and members, and to do something else than creating
a project, you need to be authenticated. The only way to authenticate yourself
currently is using the "basic" HTTP authentication.
If you don't want your credentials to pass in clear trought the network, you
can use the ssl endpoint at https://ihatemoney.notmyidea.org
For instance, here is how to see the what's in a project, using curl::
$ curl --basic -u demo:demo http://ihatemoney.notmyidea.org/api/projects/demo
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo
Projects
--------
@ -50,7 +47,7 @@ A project needs the following arguments:
::
$ curl -X POST https://ihatemoney.notmyidea.org/api/projects \
$ curl -X POST https://ihatemoney.org/api/projects \
-d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org'
"yay"
@ -62,7 +59,7 @@ Getting information about the project
Getting information about the project::
$ curl --basic -u demo:demo http://ihatemoney.notmyidea.org/api/projects/demo
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo
{
"name": "demonstration",
"contact_email": "demo@notmyidea.org",
@ -85,7 +82,7 @@ Updating a project
Updating a project is done with the `PUT` verb::
$ curl --basic -u yay:yay -X PUT\
http://ihatemoney.notmyidea.org/api/projects/yay -d\
https://ihatemoney.org/api/projects/yay -d\
'name=yay&id=yay&password=yay&contact_email=youpi@notmyidea.org'
Deleting a project
@ -93,14 +90,14 @@ Deleting a project
Just send a DELETE request ont the project URI ::
$ curl --basic -u demo:demo -X DELETE http://ihatemoney.notmyidea.org/api/projects/demo
$ curl --basic -u demo:demo -X DELETE https://ihatemoney.org/api/projects/demo
Members
-------
You can get all the members with a `GET` on `/api/projects/<id>/members`::
$ curl --basic -u demo:demo http://ihatemoney.notmyidea.org/api/projects/demo/members\
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/members\
[{"activated": true, "id": 31, "name": "Arnaud"},
{"activated": true, "id": 32, "name": "Alexis"},
{"activated": true, "id": 33, "name": "Olivier"},
@ -109,20 +106,20 @@ You can get all the members with a `GET` on `/api/projects/<id>/members`::
Add a member with a `POST` request on `/api/projects/<id>/members`::
$ curl --basic -u demo:demo -X POST\
http://ihatemoney.notmyidea.org/api/projects/demo/members -d 'name=tatayoyo'
https://ihatemoney.org/api/projects/demo/members -d 'name=tatayoyo'
35
You can also `PUT` a new version of a member (changing its name)::
$ curl --basic -u demo:demo -X PUT\
http://ihatemoney.notmyidea.org/api/projects/demo/members/36\
https://ihatemoney.org/api/projects/demo/members/36\
-d 'name=yeaaaaah'
{"activated": true, "id": 36, "name": "yeaaaaah"}
Delete a member with a `DELETE` request on `/api/projects/<id>/members/<member-id>`::
$ curl --basic -u demo:demo -X DELETE\
http://ihatemoney.notmyidea.org/api/projects/demo/members/35
https://ihatemoney.org/api/projects/demo/members/35
"OK
Bills
@ -130,21 +127,21 @@ Bills
You can get the list of bills by doing a `GET` on `/api/projects/<id>/bills` ::
$ curl --basic -u demo:demo http://ihatemoney.notmyidea.org/api/projects/demo/bills
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/bills
Add a bill with a `POST` query on `/api/projects/<id>/bills`. you need the
following params:
* `date`: the date of the bill. (yy-mm-dd)
* `date`: the date of the bill; defaults to current date if not provided. (yy-mm-dd)
* `what`: what have been payed
* `payer`: by who ? (id)
* `payed_for`: list of ids
* `payed_for`: for who ? (id, repeat the parameter to set multiple id)
* `amount`: amount payed
Returns the id of the created bill ::
$ curl --basic -u demo:demo -X POST\
http://ihatemoney.notmyidea.org/api/projects/demo/bills\
https://ihatemoney.org/api/projects/demo/bills\
-d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=200"
80
@ -152,12 +149,12 @@ You can also `PUT` a new version of the bill at
`/api/projects/<id>/bills/<bill-id>`::
$ curl --basic -u demo:demo -X PUT\
http://ihatemoney.notmyidea.org/api/projects/demo/bills/80\
https://ihatemoney.org/api/projects/demo/bills/80\
-d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=250"
80
And you can of course `DELETE` them at `/api/projects/<id>/bills/<bill-id>`::
$ curl --basic -u demo:demo -X DELETE\
http://ihatemoney.notmyidea.org/api/projects/demo/bills/80\
https://ihatemoney.org/api/projects/demo/bills/80\
"OK"