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
|
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
|
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
|
track of who bought what, when, and for who to then compute the balance of each
|
||||||
person.
|
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
|
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
|
How to contribute
|
||||||
=================
|
=================
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from flask import Blueprint, request
|
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 models import db, Project, Person, Bill
|
||||||
from forms import (ProjectForm, EditProjectForm, MemberForm,
|
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, \
|
Required, SelectField, SelectMultipleField, SubmitField, TextAreaField, \
|
||||||
TextField, ValidationError
|
TextField, ValidationError
|
||||||
from flask.ext.babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from wtforms.widgets import html_params
|
from wtforms.widgets import html_params
|
||||||
|
@ -10,28 +10,6 @@ from datetime import datetime
|
||||||
from jinja2 import Markup
|
from jinja2 import Markup
|
||||||
from utils import slugify
|
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):
|
def get_billform_for(project, set_default=True, **kwargs):
|
||||||
"""Return an instance of BillForm configured for a particular project.
|
"""Return an instance of BillForm configured for a particular project.
|
||||||
|
|
||||||
|
@ -118,7 +96,7 @@ class BillForm(Form):
|
||||||
payer = SelectField(_("Payer"), validators=[Required()], coerce=int)
|
payer = SelectField(_("Payer"), validators=[Required()], coerce=int)
|
||||||
amount = CommaDecimalField(_("Amount paid"), validators=[Required()])
|
amount = CommaDecimalField(_("Amount paid"), validators=[Required()])
|
||||||
payed_for = SelectMultipleField(_("For whom?"),
|
payed_for = SelectMultipleField(_("For whom?"),
|
||||||
validators=[Required()], widget=select_multi_checkbox, coerce=int)
|
validators=[Required()], coerce=int)
|
||||||
submit = SubmitField(_("Submit"))
|
submit = SubmitField(_("Submit"))
|
||||||
submit2 = SubmitField(_("Submit and add a new one"))
|
submit2 = SubmitField(_("Submit and add a new one"))
|
||||||
|
|
||||||
|
@ -143,9 +121,7 @@ class BillForm(Form):
|
||||||
self.payed_for.data = self.payed_for.default
|
self.payed_for.data = self.payed_for.default
|
||||||
|
|
||||||
def validate_amount(self, field):
|
def validate_amount(self, field):
|
||||||
if field.data < 0:
|
if field.data == 0:
|
||||||
field.data = abs(field.data)
|
|
||||||
elif field.data == 0:
|
|
||||||
raise ValidationError(_("Bills can't be null"))
|
raise ValidationError(_("Bills can't be null"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -198,3 +174,16 @@ class CreateArchiveForm(Form):
|
||||||
name = TextField(_("Name for this archive (optional)"), validators=[])
|
name = TextField(_("Name for this archive (optional)"), validators=[])
|
||||||
start_date = DateField(_("Start date"), validators=[Required()])
|
start_date = DateField(_("Start date"), validators=[Required()])
|
||||||
end_date = DateField(_("End date"), validators=[Required()], default=datetime.now)
|
end_date = DateField(_("End date"), validators=[Required()], default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportForm(Form):
|
||||||
|
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
|
# This callback is used to prevent an auto-migration from being generated
|
||||||
# when there are no changes to the schema.
|
# 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):
|
def process_revision_directives(context, revision, directives):
|
||||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
script = directives[0]
|
script = directives[0]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy, BaseQuery
|
from flask_sqlalchemy import SQLAlchemy, BaseQuery
|
||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
@ -37,7 +37,7 @@ class Project(db.Model):
|
||||||
# for each person
|
# for each person
|
||||||
for person in self.members:
|
for person in self.members:
|
||||||
# get the list of bills he has to pay
|
# 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():
|
for bill in bills.all():
|
||||||
if person != bill.payer:
|
if person != bill.payer:
|
||||||
share = bill.pay_each() * person.weight
|
share = bill.pay_each() * person.weight
|
||||||
|
@ -54,16 +54,28 @@ class Project(db.Model):
|
||||||
def uses_weights(self):
|
def uses_weights(self):
|
||||||
return len([i for i in self.members if i.weight != 1]) > 0
|
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"""
|
"""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
|
#cache value for better performance
|
||||||
balance = self.balance
|
balance = self.balance
|
||||||
credits, debts, transactions = [],[],[]
|
credits, debts, transactions = [],[],[]
|
||||||
# Create lists of credits and debts
|
# Create lists of credits and debts
|
||||||
for person in self.members:
|
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]})
|
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]})
|
debts.append({"person": person, "balance": -balance[person.id]})
|
||||||
# Try and find exact matches
|
# Try and find exact matches
|
||||||
for credit in credits:
|
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"]})
|
transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": credits[0]["balance"]})
|
||||||
debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"]
|
debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"]
|
||||||
del credits[0]
|
del credits[0]
|
||||||
return transactions
|
|
||||||
|
return prettify(transactions, pretty_output)
|
||||||
|
|
||||||
def exactmatch(self, credit, debts):
|
def exactmatch(self, credit, debts):
|
||||||
"""Recursively try and find subsets of 'debts' whose sum is equal to credit"""
|
"""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(Bill.payer_id == Person.id)\
|
||||||
.filter(Person.project_id == Project.id)\
|
.filter(Person.project_id == Project.id)\
|
||||||
.filter(Project.id == self.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):
|
def remove_member(self, member_id):
|
||||||
"""Remove a member from the project.
|
"""Remove a member from the project.
|
||||||
|
|
|
@ -2,8 +2,8 @@ import os
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from flask import Flask, g, request, session
|
from flask import Flask, g, request, session
|
||||||
from flask.ext.babel import Babel
|
from flask_babel import Babel
|
||||||
from flask.ext.migrate import Migrate, upgrade, stamp
|
from flask_migrate import Migrate, upgrade, stamp
|
||||||
from raven.contrib.flask import Sentry
|
from raven.contrib.flask import Sentry
|
||||||
|
|
||||||
from web import main, db, mail
|
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 "bootstrap.min.css";
|
||||||
@import "datepicker.css";
|
@import "bootstrap-datepicker3.standalone.css";
|
||||||
@import "../fonts/fontfaces.css";
|
@import "../fonts/fontfaces.css";
|
||||||
|
|
||||||
/* General */
|
/* General */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin-top: 40px;
|
/* For fixed navbar */
|
||||||
|
padding-top: 3.5rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
|
|
||||||
.navbar h1{ margin-left: 75px; }
|
.navbar h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.navbar .primary-nav { padding-left: 75px; }
|
.navbar .primary-nav { padding-left: 75px; }
|
||||||
.navbar .secondary-nav { padding-right: 75px; }
|
.navbar .secondary-nav {
|
||||||
.brand{ font-family: 'Lobster', arial, serif; }
|
text-align: right;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
}
|
||||||
|
.navbar-brand{ font-family: 'Lobster', arial, serif; }
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
|
|
||||||
|
@ -36,6 +46,14 @@ body {
|
||||||
|
|
||||||
#header .tryout {
|
#header .tryout {
|
||||||
margin-right: 10em;
|
margin-right: 10em;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #414141;
|
||||||
|
border-color: #414141;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header .tryout:hover {
|
||||||
|
background-color: #606060;
|
||||||
|
border-color: #606060;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header .additional-content {
|
#header .additional-content {
|
||||||
|
@ -55,28 +73,20 @@ body {
|
||||||
background-position: center bottom;
|
background-position: center bottom;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 230px;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 20px;
|
|
||||||
padding-top: 10px;
|
|
||||||
margin-left: -20px;
|
|
||||||
margin-top: -10px;
|
|
||||||
margin-right: 15px;
|
|
||||||
color: black;
|
color: black;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-member-form { padding-top: 1em; padding-bottom: 1em; }
|
#add-member-form { padding-top: 1em; padding-bottom: 1em; }
|
||||||
#add-member-form input[type="text"] { width: 60%; }
|
#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 */
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding-top: 1em;
|
margin-top: 1rem;
|
||||||
padding-left: 250px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Home */
|
/* Home */
|
||||||
|
@ -94,7 +104,9 @@ body {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer{
|
footer{
|
||||||
|
margin-left: -15px;
|
||||||
|
margin-right: -15px;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
@ -109,6 +121,16 @@ body {
|
||||||
float: right;
|
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{
|
.password-reminder{
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
|
@ -186,6 +208,10 @@ tr.payer_line .balance-name{
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.balance.table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
#bill-form > fieldset {
|
#bill-form > fieldset {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
@ -206,10 +232,6 @@ tr:hover .extra-info {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
max-height:455px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fluid Offsets for Boostrap */
|
/* Fluid Offsets for Boostrap */
|
||||||
|
|
||||||
.row-fluid > [class*="span"]:not([class*="offset"]):first-child{margin-left:0;}
|
.row-fluid > [class*="span"]:not([class*="offset"]):first-child{margin-left:0;}
|
||||||
|
|
2238
sources/budget/static/js/bootstrap-datepicker.js
vendored
2238
sources/budget/static/js/bootstrap-datepicker.js
vendored
File diff suppressed because it is too large
Load diff
11
sources/budget/static/js/bootstrap.min.js
vendored
11
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 field_name, field_errors in form.errors.items() if field_errors %}
|
||||||
{% for error in 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 %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{{ _("Edit this project") }}</h2>
|
<h2>{{ _("Edit this project") }}</h2>
|
||||||
<form class="form-horizontal" method="post">
|
<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>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
{% macro input(field, multiple=False, class=None) -%}
|
{% macro input(field, multiple=False, class='form-control', inline=False) -%}
|
||||||
<div class="control-group">
|
<div class="form-group{% if inline %} row{% endif %}">
|
||||||
{% if field.type != "SubmitField" %}
|
{% if field.type != "SubmitField" %}
|
||||||
{{ field.label(class="control-label") }}
|
{% if inline %}
|
||||||
|
{{ field.label(class="col-3") }}
|
||||||
|
{% else %}
|
||||||
|
{{ field.label() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="controls">
|
{% endif %}
|
||||||
|
<div class="controls{% if inline %} col-9{% endif %}">
|
||||||
{% if multiple == True %}
|
{% if multiple == True %}
|
||||||
{{ field(multiple=True, class=class) }}
|
{{ field(multiple=True, class=class) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -77,11 +81,22 @@
|
||||||
{% if title %}<legend>{% if edit %}{{ _("Edit this bill") }} {% else %}{{ _("Add a bill") }} {% endif %}</legend>{% endif %}
|
{% if title %}<legend>{% if edit %}{{ _("Edit this bill") }} {% else %}{{ _("Add a bill") }} {% endif %}</legend>{% endif %}
|
||||||
{% include "display_errors.html" %}
|
{% include "display_errors.html" %}
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ input(form.date, class="datepicker") }}
|
{{ input(form.date, class="form-control datepicker", inline=True) }}
|
||||||
{{ input(form.what) }}
|
{{ input(form.what, inline=True) }}
|
||||||
{{ input(form.payer) }}
|
{{ input(form.payer, inline=True, class="form-control custom-select") }}
|
||||||
{{ input(form.amount) }}
|
{{ input(form.amount, inline=True) }}
|
||||||
{{ input(form.payed_for) }}
|
|
||||||
|
<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>
|
</fieldset>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{{ form.submit(class="btn btn-primary") }}
|
{{ form.submit(class="btn btn-primary") }}
|
||||||
|
@ -92,7 +107,11 @@
|
||||||
|
|
||||||
{% macro add_member(form) %}
|
{% macro add_member(form) %}
|
||||||
{{ form.hidden_tag() }}
|
{{ 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 %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro edit_member(form, title=True) %}
|
{% macro edit_member(form, title=True) %}
|
||||||
|
@ -131,6 +150,17 @@
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% 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) %}
|
{% macro remind_password(form) %}
|
||||||
|
|
||||||
{% include "display_errors.html" %}
|
{% include "display_errors.html" %}
|
||||||
|
|
|
@ -1,26 +1,21 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
|
||||||
{% block header %}
|
{% block body %}
|
||||||
<div id="header" class="container-fluid">
|
<header id="header" class="row">
|
||||||
<div class="row-fluid">
|
<div class="col-5 offset-md-2">
|
||||||
<div class="span5 offset2">
|
|
||||||
<h2>{{ _("Manage your shared <br>expenses, easily") }}</h2>
|
<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>
|
||||||
<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>
|
<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>
|
</header>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
<main class="row home">
|
||||||
<div class="container-fluid">
|
<div class="col-4 offset-md-2">
|
||||||
<div class="row-fluid home">
|
|
||||||
<div class="span4 offset2">
|
|
||||||
<form id="authentication-form" class="form-horizontal" action="{{ url_for(".authenticate") }}" method="post">
|
<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>
|
<legend>{{ _("Log to an existing project") }}...</legend>
|
||||||
{{ forms.authenticate(auth_form, home=True) }}
|
{{ forms.authenticate(auth_form, home=True) }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -30,9 +25,9 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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">
|
<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>
|
<legend>...{{ _("or create a new one") }}</legend>
|
||||||
{{ forms.create_project(project_form, home=True) }}
|
{{ forms.create_project(project_form, home=True) }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -40,8 +35,7 @@
|
||||||
<button class="btn" type="submit">{{ _("let's get started") }}</button>
|
<button class="btn" type="submit">{{ _("let's get started") }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,15 @@
|
||||||
<title>{{ _("Account manager") }}{% block title %}{% endblock %}</title>
|
<title>{{ _("Account manager") }}{% block title %}{% endblock %}</title>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<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') }}">
|
<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/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.min.js") }}"></script>
|
||||||
<script src="{{ url_for("static", filename="js/bootstrap-datepicker.js") }}"></script>
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
<script type="text/javascript" charset="utf-8">
|
<script type="text/javascript" charset="utf-8">
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
var left = window.innerWidth/2-$('.flash').width()/2;
|
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(){
|
setTimeout(function(){
|
||||||
$(".flash").fadeOut("slow", function () {
|
$(".flash").fadeOut("slow", function () {
|
||||||
$(".flash").remove();
|
$(".flash").remove();
|
||||||
|
@ -35,62 +35,58 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="navbar navbar-fixed-top">
|
<nav class="navbar navbar-toggleable-md navbar fixed-top navbar-inverse bg-inverse">
|
||||||
<div class="navbar-inner">
|
<h1 class="col-2"><a class="navbar-brand" href="{{ url_for(".home") }}">#! money?</a></h1>
|
||||||
<div class="container-fluid">
|
<ul class="navbar-nav col-5 offset-md-1">
|
||||||
<h1><a class="brand" href="{{ url_for(".home") }}">#! money?</a></h1>
|
|
||||||
{% if g.project %}
|
{% if g.project %}
|
||||||
<ul class="nav primary-nav">
|
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
<li class="active"><a href="{{ url_for(".list_bills") }}">{{ _("Bills") }}</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><a href="{{ url_for(".settle_bill") }}">{{ _("Settle") }}</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 %}
|
{% endblock %}
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul class="nav pull-right secondary-nav">
|
</ul>
|
||||||
|
<ul class="navbar-nav secondary-nav col-4">
|
||||||
{% if g.project %}
|
{% if g.project %}
|
||||||
<li class="dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><strong>{{ g.project.name }}</strong> {{ _("options") }} <b class="caret"></b></a>
|
<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">
|
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||||
<li><a href="{{ url_for(".edit_project") }}">{{ _("Project settings") }}</a></li>
|
<li><a class="dropdown-item" href="{{ url_for(".edit_project") }}">{{ _("Project settings") }}</a></li>
|
||||||
<li class="divider"></li>
|
<li class="dropdown-divider"></li>
|
||||||
{% for id, name in session['projects'] %}
|
{% for id, name in session['projects'] %}
|
||||||
{% if id != g.project.id %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li><a href="{{ url_for(".create_project") }}">{{ _("Start a new project") }}</a></li>
|
<li><a class="dropdown-item" href="{{ url_for(".create_project") }}">{{ _("Start a new project") }}</a></li>
|
||||||
<li class="divider"></li>
|
<li class="dropdown-divider"></li>
|
||||||
<li><a href="{{ url_for(".exit") }}">{{ _("Logout") }}</a></li>
|
<li><a class="dropdown-item" href="{{ url_for(".exit") }}">{{ _("Logout") }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li{% if g.lang == "fr" %} class="active"{% endif %}><a href="{{ url_for(".change_lang", lang="fr") }}">fr</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{% 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 == "en" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".change_lang", lang="en") }}">en</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% block header %}{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
</nav>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
{% block body %}
|
||||||
{% block sidebar %}{% endblock %}
|
{% block sidebar %}{% endblock %}
|
||||||
<div class="content">
|
<main class="content offset-1 col-10">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% for message in get_flashed_messages() %}
|
{% 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 %}
|
{% endfor %}
|
||||||
|
|
||||||
{% block footer %}
|
{% 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>
|
<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 %}
|
{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "sidebar_table_layout.html" %}
|
||||||
|
|
||||||
{% block title %} - {{ g.project.name }}{% endblock %}
|
{% block title %} - {{ g.project.name }}{% endblock %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{{ url_for("static", filename="js/bootstrap-datepicker.js") }}"></script>
|
<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 %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|
||||||
$(window).resize(function() {
|
|
||||||
$("#sidebar").height( window.innerHeight-50 );
|
|
||||||
$("#table_overflow").height( $("#sidebar").height()-120 );
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if add_bill %} $('#new-bill').click(); {% endif %}
|
{% if add_bill %} $('#new-bill').click(); {% endif %}
|
||||||
|
|
||||||
// Hide all members actions
|
// Hide all members actions
|
||||||
|
@ -56,16 +51,14 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<div id="sidebar" class="sidebar">
|
<form id="add-member-form" action="{{ url_for(".add_member") }}" method="post" class="form-inline">
|
||||||
|
|
||||||
<form id="add-member-form" action="{{ url_for(".add_member") }}" method="post" class="form-inline input-append">
|
|
||||||
{{ forms.add_member(member_form) }}
|
{{ forms.add_member(member_form) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="table_overflow">
|
<div id="table_overflow">
|
||||||
<table class="balance table">
|
<table class="balance table">
|
||||||
{% set balance = g.project.balance %}
|
{% 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 %}>
|
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
|
||||||
<td class="balance-name">{{ member.name }}
|
<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>
|
<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">
|
<form class="action reactivate" action="{{ url_for(".reactivate", member_id=member.id) }}" method="POST">
|
||||||
<button type="submit">{{ _("reactivate") }}</button></form></td>
|
<button type="submit">{{ _("reactivate") }}</button></form></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="balance-value {% if balance[member.id] > 0 %}positive{% elif balance[member.id] < 0 %}negative{% endif %}">
|
<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] > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
|
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="identifier">{{ _("The project identifier is") }} <a href="{{ url_for(".list_bills") }}">{{ g.project.id }}</a>, {{ _("remember it!") }}</div>
|
<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>
|
<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">
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">{{ _('Add a bill') }}</h3>
|
||||||
<a href="#" class="close" data-dismiss="modal">×</a>
|
<a href="#" class="close" data-dismiss="modal">×</a>
|
||||||
<h3>{{ _('Add a bill') }}</h3>
|
|
||||||
</div>
|
</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) }}
|
{{ forms.add_bill(bill_form, title=False) }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if bills.count() > 0 %}
|
{% 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>
|
<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>
|
<tbody>
|
||||||
{% for bill in bills %}
|
{% 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.date }}</td>
|
||||||
<td>{{ bill.payer }}</td>
|
<td>{{ bill.payer }}</td>
|
||||||
<td>{{ bill.what }}</td>
|
<td>{{ bill.what }}</td>
|
||||||
|
|
|
@ -1,28 +1,19 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "sidebar_table_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 %}
|
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<div id="sidebar" class="sidebar">
|
|
||||||
|
|
||||||
<div id="table_overflow">
|
<div id="table_overflow">
|
||||||
<table class="balance table">
|
<table class="balance table">
|
||||||
{% set balance = g.project.balance %}
|
{% 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 %}>
|
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
|
||||||
<td class="balance-name">{{ member.name }}</td>
|
<td class="balance-name">{{ member.name }}</td>
|
||||||
<td class="balance-value {% if balance[member.id] > 0 %}positive{% elif balance[member.id] < 0 %}negative{% endif %}">
|
<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] > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
|
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% 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',
|
'what': u'fromage à raclette',
|
||||||
'payer': members_ids[0],
|
'payer': members_ids[0],
|
||||||
'payed_for': members_ids,
|
'payed_for': members_ids,
|
||||||
# bill with a negative value should be converted to a positive
|
|
||||||
# value
|
|
||||||
'amount': '-25'
|
'amount': '-25'
|
||||||
})
|
})
|
||||||
bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0]
|
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
|
#add a bill with a comma
|
||||||
self.app.post("/raclette/add", data={
|
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[0].id] = 8.12
|
||||||
result[models.Project.query.get("raclette").members[1].id] = 0.0
|
result[models.Project.query.get("raclette").members[1].id] = 0.0
|
||||||
result[models.Project.query.get("raclette").members[2].id] = -8.12
|
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):
|
def test_edit_project(self):
|
||||||
# A project should be editable
|
# A project should be editable
|
||||||
|
@ -582,6 +583,156 @@ class BudgetTestCase(TestCase):
|
||||||
self.assertEqual(a, balance[m.id])
|
self.assertEqual(a, balance[m.id])
|
||||||
return
|
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):
|
class APITestCase(TestCase):
|
||||||
|
@ -870,6 +1021,18 @@ class APITestCase(TestCase):
|
||||||
headers=self.get_auth("raclette"))
|
headers=self.get_auth("raclette"))
|
||||||
self.assertStatus(404, req)
|
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):
|
def test_weighted_bills(self):
|
||||||
# create a project
|
# create a project
|
||||||
self.api_create("raclette")
|
self.api_create("raclette")
|
||||||
|
@ -936,6 +1099,7 @@ class ServerTestCase(APITestCase):
|
||||||
super(ServerTestCase, self).setUp()
|
super(ServerTestCase, self).setUp()
|
||||||
|
|
||||||
def test_unprefixed(self):
|
def test_unprefixed(self):
|
||||||
|
run.app.config['APPLICATION_ROOT'] = '/'
|
||||||
req = self.app.get("/foo/")
|
req = self.app.get("/foo/")
|
||||||
self.assertStatus(303, req)
|
self.assertStatus(303, req)
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -107,7 +107,7 @@ msgstr "Nom"
|
||||||
|
|
||||||
#: forms.py:155
|
#: forms.py:155
|
||||||
msgid "Weight"
|
msgid "Weight"
|
||||||
msgstr "Poids"
|
msgstr "Parts"
|
||||||
|
|
||||||
#: forms.py:155 templates/forms.html:95
|
#: forms.py:155 templates/forms.html:95
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
|
@ -146,6 +146,22 @@ msgstr "Date de départ"
|
||||||
msgid "End date"
|
msgid "End date"
|
||||||
msgstr "Date de fin"
|
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
|
#: web.py:95
|
||||||
msgid "This private code is not the right one"
|
msgid "This private code is not the right one"
|
||||||
msgstr "Le code que vous avez entré n'est pas correct"
|
msgstr "Le code que vous avez entré n'est pas correct"
|
||||||
|
@ -292,6 +308,10 @@ msgstr "Ajouter une facture"
|
||||||
msgid "Type user name here"
|
msgid "Type user name here"
|
||||||
msgstr "Nouveau participant"
|
msgstr "Nouveau participant"
|
||||||
|
|
||||||
|
#: templates/forms.html:100
|
||||||
|
msgid "Edit this member"
|
||||||
|
msgstr "Éditer ce participant"
|
||||||
|
|
||||||
#: templates/forms.html:102
|
#: templates/forms.html:102
|
||||||
msgid "Send the invitations"
|
msgid "Send the invitations"
|
||||||
msgstr "Envoyer les invitations"
|
msgstr "Envoyer les invitations"
|
||||||
|
@ -308,6 +328,14 @@ msgstr "Créer une archive"
|
||||||
msgid "Create the archive"
|
msgid "Create the archive"
|
||||||
msgstr "Créer l'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
|
#: templates/home.html:8
|
||||||
msgid "Manage your shared <br>expenses, easily"
|
msgid "Manage your shared <br>expenses, easily"
|
||||||
msgstr "Gérez vos dépenses<br> partagées, facilement"
|
msgstr "Gérez vos dépenses<br> partagées, facilement"
|
||||||
|
|
|
@ -2,8 +2,12 @@ import re
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from jinja2 import filters
|
from jinja2 import filters
|
||||||
|
from json import dumps
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
from werkzeug.routing import HTTPException, RoutingException
|
from werkzeug.routing import HTTPException, RoutingException
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
def slugify(value):
|
def slugify(value):
|
||||||
|
@ -77,3 +81,30 @@ def minimal_round(*args, **kw):
|
||||||
# return depending on it
|
# return depending on it
|
||||||
ires = int(res)
|
ires = int(res)
|
||||||
return (res if res != ires else ires)
|
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, \
|
from flask import Blueprint, current_app, flash, g, redirect, \
|
||||||
render_template, request, session, url_for
|
render_template, request, session, url_for, send_file
|
||||||
from flask.ext.mail import Mail, Message
|
from flask_mail import Mail, Message
|
||||||
from flask.ext.babel import get_locale, gettext as _
|
from flask_babel import get_locale, gettext as _
|
||||||
from smtplib import SMTPRecipientsRefused
|
from smtplib import SMTPRecipientsRefused
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
# local modules
|
# local modules
|
||||||
from models import db, Project, Person, Bill
|
from models import db, Project, Person, Bill
|
||||||
from forms import AuthenticationForm, CreateArchiveForm, EditProjectForm, \
|
from forms import AuthenticationForm, CreateArchiveForm, EditProjectForm, \
|
||||||
InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for
|
InviteForm, MemberForm, PasswordReminder, ProjectForm, get_billform_for, \
|
||||||
from utils import Redirect303
|
ExportForm
|
||||||
|
from utils import Redirect303, list_of_dicts2json, list_of_dicts2csv
|
||||||
|
|
||||||
main = Blueprint("main", __name__)
|
main = Blueprint("main", __name__)
|
||||||
mail = Mail()
|
mail = Mail()
|
||||||
|
@ -196,20 +197,43 @@ def remind_password():
|
||||||
|
|
||||||
@main.route("/<project_id>/edit", methods=["GET", "POST"])
|
@main.route("/<project_id>/edit", methods=["GET", "POST"])
|
||||||
def edit_project():
|
def edit_project():
|
||||||
form = EditProjectForm()
|
edit_form = EditProjectForm()
|
||||||
|
export_form = ExportForm()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if form.validate():
|
if edit_form.validate():
|
||||||
project = form.update(g.project)
|
project = edit_form.update(g.project)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
session[project.id] = project.password
|
session[project.id] = project.password
|
||||||
|
|
||||||
return redirect(url_for(".list_bills"))
|
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")
|
@main.route("/<project_id>/delete")
|
||||||
|
@ -277,12 +301,14 @@ def list_bills():
|
||||||
# set the last selected payer as default choice if exists
|
# set the last selected payer as default choice if exists
|
||||||
if 'last_selected_payer' in session:
|
if 'last_selected_payer' in session:
|
||||||
bill_form.payer.data = session['last_selected_payer']
|
bill_form.payer.data = session['last_selected_payer']
|
||||||
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",
|
return render_template("list_bills.html",
|
||||||
bills=bills, member_form=MemberForm(g.project),
|
bills=bills, member_form=MemberForm(g.project),
|
||||||
bill_form=bill_form,
|
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():
|
def settle_bill():
|
||||||
"""Compute the sum each one have to pay to each other and display it"""
|
"""Compute the sum each one have to pay to each other and display it"""
|
||||||
bills = g.project.get_transactions_to_settle_bill()
|
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"])
|
@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
|
a project, you need to be authenticated. The only way to authenticate yourself
|
||||||
currently is using the "basic" HTTP authentication.
|
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::
|
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
|
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'
|
-d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org'
|
||||||
"yay"
|
"yay"
|
||||||
|
|
||||||
|
@ -62,7 +59,7 @@ Getting information about the project
|
||||||
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",
|
"name": "demonstration",
|
||||||
"contact_email": "demo@notmyidea.org",
|
"contact_email": "demo@notmyidea.org",
|
||||||
|
@ -85,7 +82,7 @@ Updating a project
|
||||||
Updating a project is done with the `PUT` verb::
|
Updating a project is done with the `PUT` verb::
|
||||||
|
|
||||||
$ curl --basic -u yay:yay -X PUT\
|
$ 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'
|
'name=yay&id=yay&password=yay&contact_email=youpi@notmyidea.org'
|
||||||
|
|
||||||
Deleting a project
|
Deleting a project
|
||||||
|
@ -93,14 +90,14 @@ Deleting a project
|
||||||
|
|
||||||
Just send a DELETE request ont the project URI ::
|
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
|
Members
|
||||||
-------
|
-------
|
||||||
|
|
||||||
You can get all the members with a `GET` on `/api/projects/<id>/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": 31, "name": "Arnaud"},
|
||||||
{"activated": true, "id": 32, "name": "Alexis"},
|
{"activated": true, "id": 32, "name": "Alexis"},
|
||||||
{"activated": true, "id": 33, "name": "Olivier"},
|
{"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`::
|
Add a member with a `POST` request on `/api/projects/<id>/members`::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X POST\
|
$ 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
|
35
|
||||||
|
|
||||||
You can also `PUT` a new version of a member (changing its name)::
|
You can also `PUT` a new version of a member (changing its name)::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X PUT\
|
$ 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'
|
-d 'name=yeaaaaah'
|
||||||
{"activated": true, "id": 36, "name": "yeaaaaah"}
|
{"activated": true, "id": 36, "name": "yeaaaaah"}
|
||||||
|
|
||||||
Delete a member with a `DELETE` request on `/api/projects/<id>/members/<member-id>`::
|
Delete a member with a `DELETE` request on `/api/projects/<id>/members/<member-id>`::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X DELETE\
|
$ 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
|
"OK
|
||||||
|
|
||||||
Bills
|
Bills
|
||||||
|
@ -130,21 +127,21 @@ Bills
|
||||||
|
|
||||||
You can get the list of bills by doing a `GET` on `/api/projects/<id>/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
|
Add a bill with a `POST` query on `/api/projects/<id>/bills`. you need the
|
||||||
following params:
|
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
|
* `what`: what have been payed
|
||||||
* `payer`: by who ? (id)
|
* `payer`: by who ? (id)
|
||||||
* `payed_for`: list of ids
|
* `payed_for`: for who ? (id, repeat the parameter to set multiple id)
|
||||||
* `amount`: amount payed
|
* `amount`: amount payed
|
||||||
|
|
||||||
Returns the id of the created bill ::
|
Returns the id of the created bill ::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X POST\
|
$ 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"
|
-d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=200"
|
||||||
80
|
80
|
||||||
|
|
||||||
|
@ -152,12 +149,12 @@ You can also `PUT` a new version of the bill at
|
||||||
`/api/projects/<id>/bills/<bill-id>`::
|
`/api/projects/<id>/bills/<bill-id>`::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X PUT\
|
$ 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"
|
-d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=250"
|
||||||
80
|
80
|
||||||
|
|
||||||
And you can of course `DELETE` them at `/api/projects/<id>/bills/<bill-id>`::
|
And you can of course `DELETE` them at `/api/projects/<id>/bills/<bill-id>`::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X DELETE\
|
$ 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"
|
"OK"
|
||||||
|
|
Loading…
Reference in a new issue