diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | budget/forms.py | 38 | ||||
| -rw-r--r-- | budget/static/main.css | 5 | ||||
| -rw-r--r-- | budget/templates/forms.html | 15 | ||||
| -rw-r--r-- | budget/templates/invitation_mail | 6 | ||||
| -rw-r--r-- | budget/templates/layout.html | 2 | ||||
| -rw-r--r-- | budget/templates/list_bills.html | 2 | ||||
| -rw-r--r-- | budget/templates/reminder_mail | 9 | ||||
| -rw-r--r-- | budget/tests.py | 5 | ||||
| -rw-r--r-- | budget/utils.py | 25 | ||||
| -rw-r--r-- | budget/web.py | 34 |
11 files changed, 104 insertions, 39 deletions
@@ -1 +1,3 @@ budget/budget.db +budget/memory +*.pyc diff --git a/budget/forms.py b/budget/forms.py index ceda0e7..33d7b38 100644 --- a/budget/forms.py +++ b/budget/forms.py @@ -2,6 +2,8 @@ from flaskext.wtf import * from wtforms.widgets import html_params from models import Project, Person, Bill, db from datetime import datetime +from jinja2 import Markup +from utils import slugify def select_multi_checkbox(field, ul_class='', **kwargs): @@ -18,9 +20,25 @@ def select_multi_checkbox(field, ul_class='', **kwargs): return u''.join(html) +def get_billform_for(request, project, set_default=True): + """Return an instance of BillForm configured for a particular project. + + :set_default: if set to True, on GET methods (usually when we want to + display the default form, it will call set_default on it. + + """ + form = BillForm() + form.payed_for.choices = form.payer.choices = [(str(m.id), m.name) for m in project.active_members] + form.payed_for.default = [str(m.id) for m in project.active_members] + + if set_default and request.method == "GET": + form.set_default() + return form + + class EditProjectForm(Form): name = TextField("Project name", validators=[Required()]) - password = TextField("Password", validators=[Required()]) + password = TextField("Private code", validators=[Required()]) contact_email = TextField("Email", validators=[Required(), Email()]) submit = SubmitField("Edit the project") @@ -42,20 +60,28 @@ class EditProjectForm(Form): return project + class ProjectForm(EditProjectForm): id = TextField("Project identifier", validators=[Required()]) - password = PasswordField("Password", validators=[Required()]) submit = SubmitField("Create the project") def validate_id(form, field): - if Project.query.get(field.data): - raise ValidationError("This project id is already used") - + form.id.data = slugify(field.data) + if Project.query.get(form.id.data): + raise ValidationError(Markup("""The project identifier is used + to log in and for the URL of the project. + <br /> + We tried to generate an identifier for you but + a project with this identifier already exists. + <br /> + Please create a new identifier you will be able + to remember. + """)) class AuthenticationForm(Form): id = TextField("Project identifier", validators=[Required()]) - password = PasswordField("Password", validators=[Required()]) + password = PasswordField("Private code", validators=[Required()]) submit = SubmitField("Get in") diff --git a/budget/static/main.css b/budget/static/main.css index d1bf24b..de3d675 100644 --- a/budget/static/main.css +++ b/budget/static/main.css @@ -93,3 +93,8 @@ div.topbar ul.secondary-nav { padding-right: 75px; } background-color: #fff; opacity: 0.8; } + +.identifier{ + text-align: right; + margin-top: -15px; +} diff --git a/budget/templates/forms.html b/budget/templates/forms.html index f32e9fd..80a0d17 100644 --- a/budget/templates/forms.html +++ b/budget/templates/forms.html @@ -17,9 +17,12 @@ </div> <!-- /clearfix --> {% endmacro %} -{% macro submit(field, cancel=False) -%} +{% macro submit(field, cancel=False, home=False) -%} <div class="actions"> <button type="submit" class="btn primary">{{ field.name }}</button> + {% if home %} + <a href="{{ url_for(".home") }}">Go back Home</a> + {% endif %} {% if cancel %} <button id="cancel-form" type="reset" class="btn">Cancel</button> {% endif %} @@ -33,7 +36,7 @@ {{ input(form.id) }} {{ input(form.password) }} {% if not home %} - {{ submit(form.submit) }} + {{ submit(form.submit, home=True) }} {% endif %} {% endmacro %} @@ -42,12 +45,14 @@ {% include "display_errors.html" %} {{ form.hidden_tag() }} - {{ input(form.name) }} + {% if not home %} {{ input(form.id) }} + {% endif %} + {{ input(form.name) }} {{ input(form.password) }} {{ input(form.contact_email) }} {% if not home %} - {{ submit(form.submit) }} + {{ submit(form.submit, home=True) }} {% endif %} {% endmacro %} @@ -90,7 +95,7 @@ {{ form.hidden_tag() }} {{ input(form.emails) }} <div class="actions"> - <button class="btn">Send the invitations</button> + <button class="btn primary">Send the invitations</button> <a href="{{ url_for(".list_bills") }}">No, thanks</a> </div> {% endmacro %} diff --git a/budget/templates/invitation_mail b/budget/templates/invitation_mail index f041db0..4f5bbf0 100644 --- a/budget/templates/invitation_mail +++ b/budget/templates/invitation_mail @@ -1,10 +1,10 @@ Hi, -Someone using the email adress {{ g.project.contact_email }} invited you to share your expenses for "{{ g.project.name }}". +Someone using the email address {{ g.project.contact_email }} invited you to share your expenses for "{{ g.project.name }}". 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 private code is "{{ g.project.password }}". Enjoy, -Some weird guys +Some weird guys (with beards) diff --git a/budget/templates/layout.html b/budget/templates/layout.html index 8858150..2b7c11e 100644 --- a/budget/templates/layout.html +++ b/budget/templates/layout.html @@ -12,7 +12,7 @@ $(".flash").fadeOut("slow", function () { $(".flash").remove(); }); - }, 2000); + }, 4000); $("body").bind("click", function(e) { $("ul.menu-dropdown").hide(); $('a.menu').parent("li").removeClass("open").children("ul.menu-dropdown").hide(); diff --git a/budget/templates/list_bills.html b/budget/templates/list_bills.html index 545de6a..63a8916 100644 --- a/budget/templates/list_bills.html +++ b/budget/templates/list_bills.html @@ -60,6 +60,8 @@ {% endblock %} {% block content %} +<div class="identifier">The project identifier is <a href="{{ url_for(".list_bills") }}">{{ g.project.id }}</a>, remember it or add this page to you bookmarks!</div> +<br /><br /> <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> diff --git a/budget/templates/reminder_mail b/budget/templates/reminder_mail new file mode 100644 index 0000000..b2e3a65 --- /dev/null +++ b/budget/templates/reminder_mail @@ -0,0 +1,9 @@ +Hi, + +You have just (or someone else using your email address) created the project "{{ g.project.name }}" to share your expenses. + +You can access it here: {{ config['SITE_URL'] }}{{ url_for(".list_bills") }} (the identifier is {{ g.project.id }}), +and the private code is "{{ g.project.password }}". + +Enjoy, +Some weird guys (with beards) diff --git a/budget/tests.py b/budget/tests.py index 7296803..4bb8e60 100644 --- a/budget/tests.py +++ b/budget/tests.py @@ -65,8 +65,9 @@ class BudgetTestCase(TestCase): self.app.post("/raclette/invite", data= {"emails": 'alexis@notmyidea.org'}) - self.assertEqual(len(outbox), 1) - self.assertEqual(outbox[0].recipients, ["alexis@notmyidea.org"]) + self.assertEqual(len(outbox), 2) + self.assertEqual(outbox[0].recipients, ["raclette@notmyidea.org"]) + self.assertEqual(outbox[1].recipients, ["alexis@notmyidea.org"]) # sending a message to multiple persons with run.mail.record_messages() as outbox: diff --git a/budget/utils.py b/budget/utils.py index 8d67410..88b8580 100644 --- a/budget/utils.py +++ b/budget/utils.py @@ -1,26 +1,21 @@ +import re from functools import wraps import inspect from flask import redirect, url_for, session, request from werkzeug.routing import HTTPException, RoutingException -from models import Bill, Project -from forms import BillForm +def slugify(value): + """Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. -def get_billform_for(project, set_default=True): - """Return an instance of BillForm configured for a particular project. - - :set_default: if set to True, on GET methods (usually when we want to - display the default form, it will call set_default on it. - + Copy/Pasted from ametaireau/pelican/utils itself took from django sources. """ - form = BillForm() - form.payed_for.choices = form.payer.choices = [(str(m.id), m.name) for m in project.active_members] - form.payed_for.default = [str(m.id) for m in project.active_members] - - if set_default and request.method == "GET": - form.set_default() - return form + if type(value) == unicode: + import unicodedata + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) + return re.sub('[-\s]+', '-', value) class Redirect303(HTTPException, RoutingException): """Raise if the map requests a redirect. This is for example the case if diff --git a/budget/web.py b/budget/web.py index 250359d..37c6415 100644 --- a/budget/web.py +++ b/budget/web.py @@ -6,9 +6,9 @@ import werkzeug # local modules from models import db, Project, Person, Bill -from forms import (ProjectForm, AuthenticationForm, BillForm, MemberForm, - InviteForm, CreateArchiveForm, EditProjectForm) -from utils import get_billform_for, Redirect303 +from forms import (get_billform_for, ProjectForm, AuthenticationForm, BillForm, + MemberForm, InviteForm, CreateArchiveForm, EditProjectForm) +from utils import Redirect303 """ The blueprint for the web interface. @@ -86,7 +86,7 @@ def authenticate(project_id=None): if request.method == "POST": if form.validate(): if not form.password.data == project.password: - form.errors['password'] = ["The password is not the right one"] + form.errors['password'] = ["This private code is not the right one"] else: # maintain a list of visited projects if "projects" not in session: @@ -115,6 +115,12 @@ def create_project(): form.name.data = request.values['project_id'] if request.method == "POST": + # At first, we don't want the user to bother with the identifier + # so it will automatically be missing because not displayed into the form + # Thus we fill it with the same value as the filled name, the validation will + # take care of the slug + if not form.id.data: + form.id.data = form.name.data if form.validate(): # save the object in the db project = form.save() @@ -125,7 +131,20 @@ def create_project(): session[project.id] = project.password session.update() + # send reminder email + g.project = project + + message_title = "You have just created '%s' to share your expenses" % g.project.name + + message_body = render_template("reminder_mail") + + msg = Message(message_title, + body=message_body, + recipients=[project.contact_email]) + mail.send(msg) + # redirect the user to the next step (invite) + flash("The project identifier is %s" % project.id) return redirect(url_for(".invite", project_id=project.id)) return render_template("create_project.html", form=form) @@ -200,7 +219,7 @@ def list_bills(): bills = g.project.get_bills() return render_template("list_bills.html", bills=bills, member_form=MemberForm(g.project), - bill_form=get_billform_for(g.project) + bill_form=get_billform_for(request, g.project) ) @main.route("/<project_id>/members/add", methods=["GET", "POST"]) @@ -239,7 +258,7 @@ def remove_member(member_id): @main.route("/<project_id>/add", methods=["GET", "POST"]) def add_bill(): - form = get_billform_for(g.project) + form = get_billform_for(request, g.project) if request.method == 'POST': if form.validate(): bill = Bill() @@ -273,7 +292,8 @@ def edit_bill(bill_id): if not bill: raise werkzeug.exceptions.NotFound() - form = get_billform_for(g.project, set_default=False) + form = get_billform_for(request, g.project, set_default=False) + if request.method == 'POST' and form.validate(): form.save(bill) db.session.commit() |
