diff options
| -rw-r--r-- | AUTHORS | 1 | ||||
| -rw-r--r-- | README.rst | 6 | ||||
| -rw-r--r-- | budget/run.py | 22 | ||||
| -rw-r--r-- | budget/templates/add_bill.html | 2 | ||||
| -rw-r--r-- | budget/templates/add_member.html | 2 | ||||
| -rw-r--r-- | budget/templates/authenticate.html | 2 | ||||
| -rw-r--r-- | budget/templates/edit_bill.html | 2 | ||||
| -rw-r--r-- | budget/templates/forms.html | 2 | ||||
| -rw-r--r-- | budget/templates/home.html | 6 | ||||
| -rw-r--r-- | budget/templates/invitation_mail | 2 | ||||
| -rw-r--r-- | budget/templates/layout.html | 10 | ||||
| -rw-r--r-- | budget/templates/list_bills.html | 14 | ||||
| -rw-r--r-- | budget/templates/send_invites.html | 4 | ||||
| -rw-r--r-- | budget/tests.py | 32 | ||||
| -rw-r--r-- | budget/web.py | 93 | ||||
| -rw-r--r-- | conf/supervisord.conf | 2 |
16 files changed, 107 insertions, 95 deletions
@@ -0,0 +1 @@ +The project has been started by Alexis Métaireau and Frédéric Sureau. Friends arehelping since that in the person of Arnaud Bos and Quentin Roy. @@ -1,10 +1,6 @@ Budget-manager ############## -:author: Alexis Métaireau, Frédéric Sureau -:date: 10/03/2010 -:technologies: Python, Flask, SQLAlchemy, WTForm - This is a really tiny app to ease the shared houses budget management. Keep track of who bought what, when, and for who to then compute the balance of each person. @@ -18,7 +14,7 @@ To make it run, you just have to do something like:: $ source venv/bin/activate $ pip install -r requirements.txt $ cd budget - $ python web.py + $ python run.py Deploy it ========= diff --git a/budget/run.py b/budget/run.py new file mode 100644 index 0000000..b1fad19 --- /dev/null +++ b/budget/run.py @@ -0,0 +1,22 @@ +from web import main, db, mail +import api + +from flask import * + +app = Flask(__name__) +app.config.from_object("default_settings") +app.register_blueprint(main) + +# db +db.init_app(app) +db.app = app +db.create_all() + +# mail +mail.init_app(app) + +def main(): + app.run(host="0.0.0.0", debug=True) + +if __name__ == '__main__': + main() diff --git a/budget/templates/add_bill.html b/budget/templates/add_bill.html index 0b575d7..3b29896 100644 --- a/budget/templates/add_bill.html +++ b/budget/templates/add_bill.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block top_menu %} -<a href="{{ url_for('list_bills') }}">Back to the list</a> +<a href="{{ url_for(".list_bills") }}">Back to the list</a> {% endblock %} {% block content %} diff --git a/budget/templates/add_member.html b/budget/templates/add_member.html index 5739791..62fcf9e 100644 --- a/budget/templates/add_member.html +++ b/budget/templates/add_member.html @@ -1,6 +1,6 @@ {% extends "layout.html" %} {% block content %} - <form action="{{ url_for("add_member") }}" method="post"> + <form action="{{ url_for(".add_member") }}" method="post"> {{ forms.add_member(form) }} </form> {% endblock %} diff --git a/budget/templates/authenticate.html b/budget/templates/authenticate.html index 0ad8815..9852d6a 100644 --- a/budget/templates/authenticate.html +++ b/budget/templates/authenticate.html @@ -8,7 +8,7 @@ {% if create_project %} <p class="info">The project you are trying to access do not exist, do you want -to <a href="{{ url_for("create_project", project_id=create_project) }}">create it</a>? +to <a href="{{ url_for(".create_project", project_id=create_project) }}">create it</a>? </p> {% endif %} <form action="" method="POST" accept-charset="utf-8"> diff --git a/budget/templates/edit_bill.html b/budget/templates/edit_bill.html index f069193..9c272ae 100644 --- a/budget/templates/edit_bill.html +++ b/budget/templates/edit_bill.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block top_menu %} -<a href="{{ url_for('list_bills') }}">Back to the list</a> +<a href="{{ url_for(".list_bills") }}">Back to the list</a> {% endblock %} {% block content %} diff --git a/budget/templates/forms.html b/budget/templates/forms.html index 7b512ff..b027763 100644 --- a/budget/templates/forms.html +++ b/budget/templates/forms.html @@ -80,7 +80,7 @@ {{ input(form.emails) }} <div class="actions"> <button class="btn">Send the invitations</button> - <a href="{{ url_for("list_bills") }}">No, thanks</a> + <a href="{{ url_for(".list_bills") }}">No, thanks</a> </div> {% endmacro %} diff --git a/budget/templates/home.html b/budget/templates/home.html index 1b77cbd..ceb3b57 100644 --- a/budget/templates/home.html +++ b/budget/templates/home.html @@ -7,7 +7,7 @@ <div id="header"> <div class="slide"> <h1><span>Manage your shared <br>expenses, easily</span></h1> - <a href="{{ url_for("demo") }}" class="about_link">Try out the demo</a> + <a href="{{ url_for(".demo") }}" class="about_link">Try out the demo</a> </div> <div class="additional-content"> <p>You're sharing a house?<br /> Going on holidays with friends?<br /> Simply sharing money with others? <br /><strong>We can help!</strong></p> @@ -22,14 +22,14 @@ <div class="row"> <div class="span8 columns"> - <form action="{{ url_for('authenticate') }}" method="post"> + <form action="{{ url_for(".authenticate") }}" method="post"> <h3>Log to an existing project...</h3> {{ forms.authenticate(auth_form, home=True) }} <button class="btn">log in</button> </form> </div> <div class="span8 columns"> - <form class="create" action="{{ url_for('create_project') }}" method="post"> + <form class="create" action="{{ url_for(".create_project") }}" method="post"> <h3>...or create a new one</h3> {{ forms.create_project(project_form, home=True) }} <button class="btn">let's get started</button> diff --git a/budget/templates/invitation_mail b/budget/templates/invitation_mail index 2844efd..f041db0 100644 --- a/budget/templates/invitation_mail +++ b/budget/templates/invitation_mail @@ -4,7 +4,7 @@ Someone using the email adress {{ g.project.contact_email }} invited you to shar It's as simple as saying what did you paid for, for who, and how much did it cost you, we are caring about the rest. -You can access it here: {{ config['SITE_URL'] }}{{ url_for("list_bills") }}, the password is "{{ g.project.password }}". +You can access it here: {{ config['SITE_URL'] }}{{ url_for(".list_bills") }}, the password is "{{ g.project.password }}". Enjoy, Some weird guys diff --git a/budget/templates/layout.html b/budget/templates/layout.html index cb7ab10..afdda85 100644 --- a/budget/templates/layout.html +++ b/budget/templates/layout.html @@ -4,7 +4,7 @@ <head> <title>Account manager</title> <meta http-equiv="content-type" content="text/html; charset=utf-8"> - <link rel=stylesheet type=text/css href="{{ url_for('static', filename='main.css') }}"> + <link rel=stylesheet type=text/css href="{{ url_for("static", filename='main.css') }}"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> <script type="text/javascript" charset="utf-8"> $(document).ready(function(){ @@ -42,7 +42,7 @@ <body> <div class="topbar"> - <h3><a class="logo" href="{% if g.project %}{{ url_for("list_bills") }}{% endif %}">#! money?</a></h3> + <h3><a class="logo" href="{% if g.project %}{{ url_for(".list_bills") }}{% endif %}">#! money?</a></h3> {% if g.project %} <ul> <li class="active"><a href="">Bills</a></li> @@ -56,12 +56,12 @@ <li class="divider"></li> {% for id, name in session['projects'] %} {% if id != g.project.id %} - <li><a href="{{ url_for("list_bills", project_id=id) }}">switch to {{ name }}</a></li> + <li><a href="{{ url_for(".list_bills", project_id=id) }}">switch to {{ name }}</a></li> {% endif %} {% endfor %} - <li><a href="{{ url_for("create_project") }}">Start a new project</a></li> + <li><a href="{{ url_for(".create_project") }}">Start a new project</a></li> <li class="divider"></li> - <li><a href="{{ url_for("exit") }}">Logout</a></li> + <li><a href="{{ url_for(".exit") }}">Logout</a></li> </ul> </li> </ul> diff --git a/budget/templates/list_bills.html b/budget/templates/list_bills.html index b485f81..545de6a 100644 --- a/budget/templates/list_bills.html +++ b/budget/templates/list_bills.html @@ -48,20 +48,20 @@ <td class="{% if balance[member] > 0 %}positive{% elif balance[member] < 0 %}negative{% endif %}"> {% if balance[member] > 0 %}+{% endif %}{{ balance[member] }} </td> - <td> {% if member.activated %}<a class="remove" href="{{ url_for("remove_member", member_id=member.id) }}">delete</a>{% else %}<a href="{{ url_for("reactivate", member_id=member.id) }}">reactivate</a>{% endif %}</td> + <td> {% if member.activated %}<a class="remove" href="{{ url_for(".remove_member", member_id=member.id) }}">delete</a>{% else %}<a href="{{ url_for(".reactivate", member_id=member.id) }}">reactivate</a>{% endif %}</td> </tr> {% endif %} {% endfor %} </table> - <form action="{{ url_for("add_member") }}" method="post"> + <form action="{{ url_for(".add_member") }}" method="post"> {{ forms.add_member(member_form) }} </form> {% endblock %} {% block content %} -<a id="new-bill" href="{{ url_for('add_bill') }}" class="primary">Add a new bill</a> - <form id="bill-form" action="{{ url_for('add_bill') }}" method="post" style="display: none"> +<a id="new-bill" href="{{ url_for(".add_bill") }}" class="primary">Add a new bill</a> + <form id="bill-form" action="{{ url_for(".add_bill") }}" method="post" style="display: none"> <a id="hide-bill-form" href="#">hide this form</a> {{ forms.add_bill(bill_form) }} </form> @@ -77,15 +77,15 @@ <td>{{ bill.what }}</td> <td>{% for ower in bill.owers %}{{ ower.name }} {% endfor %}</td> <td>{{ bill.amount }} ({{ bill.pay_each() }} each)</td> - <td><a href="{{ url_for("edit_bill", bill_id=bill.id) }}">edit</a> - <a class="delete" href="{{ url_for("delete_bill", bill_id=bill.id) }}">delete</a></td> + <td><a href="{{ url_for(".edit_bill", bill_id=bill.id) }}">edit</a> + <a class="delete" href="{{ url_for(".delete_bill", bill_id=bill.id) }}">delete</a></td> </tr> {% endfor %} </tbody> </table> {% else %} - <p>Nothing to list yet. You probably want to <a id="empty-new-bill" href="{{ url_for("add_bill") }}">add a bill</a> ?</p> + <p>Nothing to list yet. You probably want to <a id="empty-new-bill" href="{{ url_for(".add_bill") }}">add a bill</a> ?</p> {% endif %} </div> {% endblock %} diff --git a/budget/templates/send_invites.html b/budget/templates/send_invites.html index bf018e2..ec68333 100644 --- a/budget/templates/send_invites.html +++ b/budget/templates/send_invites.html @@ -4,14 +4,14 @@ <ol> <li>Create the project</li> <li><strong>Invite people</strong></li> - <li><a href="{{ url_for("list_bills") }}">Use it!</a></li> + <li><a href="{{ url_for(".list_bills") }}">Use it!</a></li> </ol> {% endblock %} {% block content %} <h2>Invite people to join this project</h2> <p>Specify a (coma separated) list of email adresses you want to notify about the creation of this budget management project and we will send them an email for you.</p> -<p>If you prefer, you can <a href="{{ url_for("list_bills") }}">skip this step</a> and notify them yourself</p> +<p>If you prefer, you can <a href="{{ url_for(".list_bills") }}">skip this step</a> and notify them yourself</p> {% include "display_errors.html" %} <form class="invites" method="post" accept-charset="utf-8"> diff --git a/budget/tests.py b/budget/tests.py index 9efb78a..db37e46 100644 --- a/budget/tests.py +++ b/budget/tests.py @@ -5,22 +5,22 @@ import unittest from flask import session -import web +import run import models class TestCase(unittest.TestCase): def setUp(self): - web.app.config['TESTING'] = True + run.app.config['TESTING'] = True - web.app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///memory" - web.app.config['CSRF_ENABLED'] = False # simplify the tests - self.app = web.app.test_client() + run.app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///memory" + run.app.config['CSRF_ENABLED'] = False # simplify the tests + self.app = run.app.test_client() - models.db.init_app(web.app) - web.mail.init_app(web.app) + models.db.init_app(run.app) + run.mail.init_app(run.app) - models.db.app = web.app + models.db.app = run.app models.db.create_all() def tearDown(self): @@ -57,7 +57,7 @@ class BudgetTestCase(TestCase): are checked properly. """ # sending a message to one person - with web.mail.record_messages() as outbox: + with run.mail.record_messages() as outbox: # create a project self.login("raclette") @@ -70,7 +70,7 @@ class BudgetTestCase(TestCase): self.assertEqual(outbox[0].recipients, ["alexis@notmyidea.org"]) # sending a message to multiple persons - with web.mail.record_messages() as outbox: + with run.mail.record_messages() as outbox: self.app.post("/raclette/invite", data= {"emails": 'alexis@notmyidea.org, toto@notmyidea.org'}) @@ -80,13 +80,13 @@ class BudgetTestCase(TestCase): ["alexis@notmyidea.org", "toto@notmyidea.org"]) # mail address checking - with web.mail.record_messages() as outbox: + with run.mail.record_messages() as outbox: response = self.app.post("/raclette/invite", data={"emails": "toto"}) self.assertEqual(len(outbox), 0) # no message sent self.assertIn("The email toto is not valid", response.data) # mixing good and wrong adresses shouldn't send any messages - with web.mail.record_messages() as outbox: + with run.mail.record_messages() as outbox: self.app.post("/raclette/invite", data= {"emails": 'alexis@notmyidea.org, alexis'}) # not valid @@ -95,7 +95,7 @@ class BudgetTestCase(TestCase): def test_project_creation(self): - with web.app.test_client() as c: + with run.app.test_client() as c: # add a valid project c.post("/create", data={ @@ -188,7 +188,7 @@ class BudgetTestCase(TestCase): def test_demo(self): # Test that it is possible to connect automatically by going onto /demo - with web.app.test_client() as c: + with run.app.test_client() as c: models.db.session.add(models.Project(id="demo", name=u"demonstration", password="demo", contact_email="demo@notmyidea.org")) models.db.session.commit() @@ -213,7 +213,7 @@ class BudgetTestCase(TestCase): self.assertIn("Authentication", resp.data) # try to connect with wrong credentials should not work - with web.app.test_client() as c: + with run.app.test_client() as c: resp = c.post("/authenticate", data={'id': 'raclette', 'password': 'nope'}) @@ -221,7 +221,7 @@ class BudgetTestCase(TestCase): self.assertNotIn('raclette', session) # try to connect with the right credentials should work - with web.app.test_client() as c: + with run.app.test_client() as c: resp = c.post("/authenticate", data={'id': 'raclette', 'password': 'raclette'}) diff --git a/budget/web.py b/budget/web.py index 1b1b61a..f72a686 100644 --- a/budget/web.py +++ b/budget/web.py @@ -9,21 +9,21 @@ from forms import (ProjectForm, AuthenticationForm, BillForm, MemberForm, InviteForm, CreateArchiveForm) from utils import get_billform_for, Redirect303 -# create the application, initialize stuff -app = Flask(__name__) -app.config.from_object("default_settings") -mail = Mail() +""" +The blueprint for the web interface. -# db -db.init_app(app) -db.app = app -db.create_all() +Contains all the interaction logic with the end user (except forms which +are directly handled in the forms module. -# mail -mail.init_app(app) +Basically, this blueprint takes care of the authentication and provides +some shortcuts to make your life better when coding (see `pull_project` +and `add_project_id` for a quick overview +""" +main = Blueprint("main", __name__) +mail = Mail() -@app.url_defaults +@main.url_defaults def add_project_id(endpoint, values): """Add the project id to the url calls if it is expected. @@ -31,10 +31,10 @@ def add_project_id(endpoint, values): """ if 'project_id' in values or not hasattr(g, 'project'): return - if app.url_map.is_endpoint_expecting(endpoint, 'project_id'): + if current_app.url_map.is_endpoint_expecting(endpoint, 'project_id'): values['project_id'] = g.project.id -@app.url_value_preprocessor +@main.url_value_preprocessor def pull_project(endpoint, values): """When a request contains a project_id value, transform it directly into a project by checking the credentials are stored in session. @@ -49,16 +49,16 @@ def pull_project(endpoint, values): if project_id: project = Project.query.get(project_id) if not project: - raise Redirect303(url_for("create_project", project_id=project_id)) + raise Redirect303(url_for(".create_project", project_id=project_id)) if project.id in session and session[project.id] == project.password: # add project into kwargs and call the original function g.project = project else: # redirect to authentication page raise Redirect303( - url_for("authenticate", project_id=project_id)) + url_for(".authenticate", project_id=project_id)) -@app.route("/authenticate", methods=["GET", "POST"]) +@main.route("/authenticate", methods=["GET", "POST"]) def authenticate(project_id=None): """Authentication form""" form = AuthenticationForm() @@ -76,7 +76,7 @@ def authenticate(project_id=None): # if credentials are already in session, redirect if project_id in session and project.password == session[project_id]: setattr(g, 'project', project) - return redirect(url_for("list_bills")) + return redirect(url_for(".list_bills")) # else process the form if request.method == "POST": @@ -92,19 +92,19 @@ def authenticate(project_id=None): session[project_id] = form.password.data session.update() setattr(g, 'project', project) - return redirect(url_for("list_bills")) + return redirect(url_for(".list_bills")) return render_template("authenticate.html", form=form, create_project=create_project) -@app.route("/") +@main.route("/") def home(): project_form = ProjectForm() auth_form = AuthenticationForm() return render_template("home.html", project_form=project_form, auth_form=auth_form, session=session) -@app.route("/create", methods=["GET", "POST"]) +@main.route("/create", methods=["GET", "POST"]) def create_project(): form = ProjectForm() if request.method == "GET" and 'project_id' in request.values: @@ -122,17 +122,17 @@ def create_project(): session.update() # redirect the user to the next step (invite) - return redirect(url_for("invite", project_id=project.id)) + return redirect(url_for(".invite", project_id=project.id)) return render_template("create_project.html", form=form) -@app.route("/exit") +@main.route("/exit") def exit(): # delete the session session.clear() - return redirect(url_for("home")) + return redirect(url_for(".home")) -@app.route("/demo") +@main.route("/demo") def demo(): """ Authenticate the user for the demonstration project and redirect him to @@ -147,9 +147,9 @@ def demo(): db.session.add(project) db.session.commit() session[project.id] = project.password - return redirect(url_for("list_bills", project_id=project.id)) + return redirect(url_for(".list_bills", project_id=project.id)) -@app.route("/<project_id>/invite", methods=["GET", "POST"]) +@main.route("/<project_id>/invite", methods=["GET", "POST"]) def invite(): """Send invitations for this particular project""" @@ -169,11 +169,11 @@ def invite(): for email in form.emails.data.split(",")]) mail.send(msg) flash("You invitations have been sent") - return redirect(url_for("list_bills")) + return redirect(url_for(".list_bills")) return render_template("send_invites.html", form=form) -@app.route("/<project_id>/") +@main.route("/<project_id>/") def list_bills(): bills = g.project.get_bills() return render_template("list_bills.html", @@ -181,7 +181,7 @@ def list_bills(): bill_form=get_billform_for(g.project) ) -@app.route("/<project_id>/members/add", methods=["GET", "POST"]) +@main.route("/<project_id>/members/add", methods=["GET", "POST"]) def add_member(): # FIXME manage form errors on the list_bills page form = MemberForm(g.project) @@ -194,14 +194,14 @@ def add_member(): person[0].activated = True db.session.commit() flash("%s is part of this project again" % person[0].name) - return redirect(url_for("list_bills")) + return redirect(url_for(".list_bills")) db.session.add(Person(name=form.name.data, project=g.project)) db.session.commit() - return redirect(url_for("list_bills")) + return redirect(url_for(".list_bills")) return render_template("add_member.html", form=form) -@app.route("/<project_id>/members/<member_id>/reactivate", methods=["GET",]) +@main.route("/<project_id>/members/<member_id>/reactivate", methods=["GET",]) def reactivate(member_id): person = Person.query.filter(Person.id == member_id)\ .filter(Project.id == g.project.id).all() @@ -209,10 +209,10 @@ def reactivate(member_id): person[0].activated = True db.session.commit() flash("%s is part of this project again" % person[0].name) - return redirect(url_for("list_bills")) + return redirect(url_for(".list_bills")) -@app.route("/<project_id>/members/<member_id>/delete", methods=["GET", "POST"]) +@main.route("/<project_id>/members/<member_id>/delete", methods=["GET", "POST"]) def remove_member(member_id): member = g.project.remove_member(member_id) if member.activated == False: @@ -220,9 +220,9 @@ def remove_member(member_id): else: flash("User '%s' has been removed" % member.name) - return redirect(url_for("list_bills")) + return redirect(url_for(".list_bills")) -@app.route("/<project_id>/add", methods=["GET", "POST"]) +@main.route("/<project_id>/add", methods=["GET", "POST"]) def add_bill(): form = get_billform_for(g.project) if request.method == 'POST': @@ -232,22 +232,22 @@ def add_bill(): db.session.commit() flash("The bill has been added") - return redirect(url_for('list_bills')) + return redirect(url_for('.list_bills')) return render_template("add_bill.html", form=form) -@app.route("/<project_id>/delete/<int:bill_id>") +@main.route("/<project_id>/delete/<int:bill_id>") def delete_bill(bill_id): bill = Bill.query.get_or_404(bill_id) db.session.delete(bill) db.session.commit() flash("The bill has been deleted") - return redirect(url_for('list_bills')) + return redirect(url_for('.list_bills')) -@app.route("/<project_id>/edit/<int:bill_id>", methods=["GET", "POST"]) +@main.route("/<project_id>/edit/<int:bill_id>", methods=["GET", "POST"]) def edit_bill(bill_id): bill = Bill.query.get_or_404(bill_id) form = get_billform_for(g.project, set_default=False) @@ -256,17 +256,17 @@ def edit_bill(bill_id): db.session.commit() flash("The bill has been modified") - return redirect(url_for('list_bills')) + return redirect(url_for('.list_bills')) form.fill(bill) return render_template("add_bill.html", form=form, edit=True) -@app.route("/<project_id>/compute") +@main.route("/<project_id>/compute") def compute_bills(): """Compute the sum each one have to pay to each other and display it""" return render_template("compute_bills.html") -@app.route("/<project_id>/archives/create") +@main.route("/<project_id>/archives/create") def create_archive(): form = CreateArchiveForm() if request.method == "POST": @@ -275,10 +275,3 @@ def create_archive(): flash("The data from XX to XX has been archived") return render_template("create_archive.html", form=form) - - -def main(): - app.run(host="0.0.0.0", debug=True) - -if __name__ == '__main__': - main() diff --git a/conf/supervisord.conf b/conf/supervisord.conf index 742759e..ec2d452 100644 --- a/conf/supervisord.conf +++ b/conf/supervisord.conf @@ -1,5 +1,5 @@ [program:budget] -command=/path/to/your/app/venv/bin/gunicorn -c /path/to/your/app/conf/gunicorn.conf.py budget:app +command=/path/to/your/app/venv/bin/gunicorn -c /path/to/your/app/conf/gunicorn.conf.py run:app directory=/path/to/your/app/budget/ user=www autostart=true |
