aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--budget/forms.py38
-rw-r--r--budget/static/main.css5
-rw-r--r--budget/templates/forms.html15
-rw-r--r--budget/templates/invitation_mail6
-rw-r--r--budget/templates/layout.html2
-rw-r--r--budget/templates/list_bills.html2
-rw-r--r--budget/templates/reminder_mail9
-rw-r--r--budget/tests.py5
-rw-r--r--budget/utils.py25
-rw-r--r--budget/web.py34
11 files changed, 104 insertions, 39 deletions
diff --git a/.gitignore b/.gitignore
index e7fc3ae..b034990 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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()