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:
commit
2b3b63ef27
31 changed files with 3102 additions and 1612 deletions
7
sources/.travis.yml
Normal file
7
sources/.travis.yml
Normal 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
|
|
@ -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
|
||||
=================
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")]
|
||||
)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
707
sources/budget/static/css/bootstrap-datepicker3.standalone.css
vendored
Normal file
707
sources/budget/static/css/bootstrap-datepicker3.standalone.css
vendored
Normal 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 */
|
694
sources/budget/static/css/bootstrap.min.css
vendored
694
sources/budget/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
|
@ -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;}
|
||||
|
|
2276
sources/budget/static/js/bootstrap-datepicker.js
vendored
2276
sources/budget/static/js/bootstrap-datepicker.js
vendored
File diff suppressed because it is too large
Load diff
13
sources/budget/static/js/bootstrap.min.js
vendored
13
sources/budget/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
4
sources/budget/static/js/jquery-1.7.2.min.js
vendored
4
sources/budget/static/js/jquery-1.7.2.min.js
vendored
File diff suppressed because one or more lines are too long
4
sources/budget/static/js/jquery-3.1.1.min.js
vendored
Normal file
4
sources/budget/static/js/jquery-3.1.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
sources/budget/static/js/locales/bootstrap-datepicker.fr.min.js
vendored
Normal file
2
sources/budget/static/js/locales/bootstrap-datepicker.fr.min.js
vendored
Normal 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);
|
1
sources/budget/static/js/tether.min.js
vendored
Normal file
1
sources/budget/static/js/tether.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -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,9 +35,8 @@
|
|||
<button class="btn" type="submit">{{ _("let's get started") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">×</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>
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
||||
|
|
14
sources/budget/templates/sidebar_table_layout.html
Normal file
14
sources/budget/templates/sidebar_table_layout.html
Normal 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 %}
|
|
@ -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)
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue