mirror of
https://github.com/YunoHost-Apps/ihatemoney_ynh.git
synced 2024-09-03 19:26:15 +02:00
Merge branch 'master' into auth-forms-usability
This commit is contained in:
commit
681f22f3e4
13 changed files with 534 additions and 47 deletions
27
README.rst
27
README.rst
|
@ -32,3 +32,30 @@ To deploy it, I'm using gunicorn and supervisord::
|
|||
3. reload both nginx and supervisord. It should be working ;)
|
||||
|
||||
Don't forget to set the right permission for your files !
|
||||
|
||||
How to contribute
|
||||
=================
|
||||
|
||||
There are different ways to help us, regarding if you are a designer,
|
||||
a developer or just an user.
|
||||
|
||||
As a developer
|
||||
--------------
|
||||
|
||||
The best way to contribute code is to write it and to make a pull request on
|
||||
github. Please, think about updating and running the tests before asking for
|
||||
a pull request as it will help us to maintain the code clean and running.
|
||||
|
||||
As a designer / Front-end developer
|
||||
-----------------------------------
|
||||
|
||||
Feel free to provide us mockups or to involve yourself into the discussions
|
||||
hapenning on the github issue tracker. All ideas are welcome. Of course, if you
|
||||
know how to implement them, feel free to fork and make a pull request.
|
||||
|
||||
End-user
|
||||
--------
|
||||
|
||||
You just wanted to have a look at the application and found a bug? Please tell
|
||||
us and go fill a new issue:
|
||||
https://github.com/ametaireau/budget-manager/issues/new
|
||||
|
|
153
budget/api.py
Normal file
153
budget/api.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from flask import *
|
||||
|
||||
from models import db, Project, Person, Bill
|
||||
from forms import ProjectForm
|
||||
from utils import for_all_methods
|
||||
|
||||
from rest import RESTResource, need_auth# FIXME make it an ext
|
||||
from werkzeug import Response
|
||||
|
||||
|
||||
api = Blueprint("api", __name__, url_prefix="/api")
|
||||
|
||||
def check_project(*args, **kwargs):
|
||||
"""Check the request for basic authentication for a given project.
|
||||
|
||||
Return the project if the authorization is good, False otherwise
|
||||
"""
|
||||
auth = request.authorization
|
||||
|
||||
# project_id should be contained in kwargs and equal to the username
|
||||
if auth and "project_id" in kwargs and \
|
||||
auth.username == kwargs["project_id"]:
|
||||
project = Project.query.get(auth.username)
|
||||
if project and project.password == auth.password:
|
||||
return project
|
||||
return False
|
||||
|
||||
|
||||
class ProjectHandler(object):
|
||||
|
||||
def add(self):
|
||||
form = ProjectForm(csrf_enabled=False)
|
||||
if form.validate():
|
||||
project = form.save(Project())
|
||||
db.session.add(project)
|
||||
db.session.commit()
|
||||
return 201, project.id
|
||||
return 400, form.errors
|
||||
|
||||
@need_auth(check_project, "project")
|
||||
def get(self, project):
|
||||
return project
|
||||
|
||||
@need_auth(check_project, "project")
|
||||
def delete(self, project):
|
||||
db.session.delete(project)
|
||||
db.session.commit()
|
||||
return 200, "DELETED"
|
||||
|
||||
@need_auth(check_project, "project")
|
||||
def update(self, project):
|
||||
form = ProjectForm(csrf_enabled=False)
|
||||
if form.validate():
|
||||
form.save(project)
|
||||
db.session.commit()
|
||||
return 200, "UPDATED"
|
||||
return 400, form.errors
|
||||
|
||||
|
||||
class MemberHandler(object):
|
||||
|
||||
def get(self, project, member_id):
|
||||
member = Person.query.get(member_id)
|
||||
if not member or member.project != project:
|
||||
return 404, "Not Found"
|
||||
return member
|
||||
|
||||
def list(self, project):
|
||||
return project.members
|
||||
|
||||
def add(self, project):
|
||||
form = MemberForm(csrf_enabled=False)
|
||||
if form.validate():
|
||||
member = Person()
|
||||
form.save(project, member)
|
||||
db.session.commit()
|
||||
return 200, member.id
|
||||
return 400, form.errors
|
||||
|
||||
def update(self, project, member_id):
|
||||
form = MemberForm(csrf_enabled=False)
|
||||
if form.validate():
|
||||
member = Person.query.get(member_id, project)
|
||||
form.save(project, member)
|
||||
db.session.commit()
|
||||
return 200, member
|
||||
return 400, form.errors
|
||||
|
||||
def delete(self, project, member_id):
|
||||
if project.remove_member(member_id):
|
||||
return 200, "OK"
|
||||
return 404, "Not Found"
|
||||
|
||||
|
||||
class BillHandler(object):
|
||||
|
||||
def get(self, project, bill_id):
|
||||
bill = Bill.query.get(project, bill_id)
|
||||
if not bill:
|
||||
return 404, "Not Found"
|
||||
return bill
|
||||
|
||||
def list(self, project):
|
||||
return project.get_bills().all()
|
||||
|
||||
def add(self, project):
|
||||
form = BillForm(csrf_enabled=False)
|
||||
if form.validate():
|
||||
bill = Bill()
|
||||
form.save(bill)
|
||||
db.session.add(bill)
|
||||
db.session.commit()
|
||||
return 200, bill.id
|
||||
return 400, form.errors
|
||||
|
||||
def update(self, project, bill_id):
|
||||
form = BillForm(csrf_enabled=False)
|
||||
if form.validate():
|
||||
form.save(bill)
|
||||
db.session.commit()
|
||||
return 200, bill.id
|
||||
return 400, form.errors
|
||||
|
||||
def delete(self, project, bill_id):
|
||||
bill = Bill.query.delete(project, bill_id)
|
||||
if not bill:
|
||||
return 404, "Not Found"
|
||||
return bill
|
||||
|
||||
|
||||
project_resource = RESTResource(
|
||||
name="project",
|
||||
route="/project",
|
||||
app=api,
|
||||
actions=["add", "update", "delete", "get"],
|
||||
handler=ProjectHandler())
|
||||
|
||||
member_resource = RESTResource(
|
||||
name="member",
|
||||
inject_name="project",
|
||||
route="/project/<project_id>/members",
|
||||
app=api,
|
||||
handler=MemberHandler(),
|
||||
authentifier=check_project)
|
||||
|
||||
bill_resource = RESTResource(
|
||||
name="bill",
|
||||
inject_name="project",
|
||||
route="/project/<project_id>/bills",
|
||||
app=api,
|
||||
handler=BillHandler(),
|
||||
authentifier=check_project)
|
|
@ -1,6 +1,6 @@
|
|||
from flaskext.wtf import *
|
||||
from wtforms.widgets import html_params
|
||||
from models import Project, Person, Bill
|
||||
from models import Project, Person, Bill, db
|
||||
from datetime import datetime
|
||||
from jinja2 import Markup
|
||||
from utils import slugify
|
||||
|
@ -36,11 +36,33 @@ def get_billform_for(request, project, set_default=True):
|
|||
return form
|
||||
|
||||
|
||||
class ProjectForm(Form):
|
||||
class EditProjectForm(Form):
|
||||
name = TextField("Project name", validators=[Required()])
|
||||
id = TextField("Project identifier", validators=[Required()])
|
||||
password = PasswordField("Private code", validators=[Required()])
|
||||
password = TextField("Private code", validators=[Required()])
|
||||
contact_email = TextField("Email", validators=[Required(), Email()])
|
||||
submit = SubmitField("Edit the project")
|
||||
|
||||
def save(self):
|
||||
"""Create a new project with the information given by this form.
|
||||
|
||||
Returns the created instance
|
||||
"""
|
||||
project = Project(name=self.name.data, id=self.id.data,
|
||||
password=self.password.data,
|
||||
contact_email=self.contact_email.data)
|
||||
return project
|
||||
|
||||
def update(self, project):
|
||||
"""Update the project with the information from the form"""
|
||||
project.name = self.name.data
|
||||
project.password = self.password.data
|
||||
project.contact_email = self.contact_email.data
|
||||
|
||||
return project
|
||||
|
||||
|
||||
class ProjectForm(EditProjectForm):
|
||||
id = TextField("Project identifier", validators=[Required()])
|
||||
submit = SubmitField("Create the project")
|
||||
|
||||
def validate_id(form, field):
|
||||
|
@ -56,16 +78,6 @@ class ProjectForm(Form):
|
|||
to remember.
|
||||
"""))
|
||||
|
||||
def save(self):
|
||||
"""Create a new project with the information given by this form.
|
||||
|
||||
Returns the created instance
|
||||
"""
|
||||
project = Project(name=self.name.data, id=self.id.data,
|
||||
password=self.password.data,
|
||||
contact_email=self.contact_email.data)
|
||||
return project
|
||||
|
||||
|
||||
class AuthenticationForm(Form):
|
||||
id = TextField("Project identifier", validators=[Required()])
|
||||
|
@ -103,19 +115,26 @@ class BillForm(Form):
|
|||
|
||||
|
||||
class MemberForm(Form):
|
||||
def __init__(self, project, *args, **kwargs):
|
||||
super(MemberForm, self).__init__(*args, **kwargs)
|
||||
self.project = project
|
||||
|
||||
name = TextField("Name", validators=[Required()])
|
||||
submit = SubmitField("Add a member")
|
||||
|
||||
def __init__(self, project, *args, **kwargs):
|
||||
super(MemberForm, self).__init__(*args, **kwargs)
|
||||
self.project = project
|
||||
|
||||
def validate_name(form, field):
|
||||
if Person.query.filter(Person.name == field.data)\
|
||||
.filter(Person.project == form.project)\
|
||||
.filter(Person.activated == True).all():
|
||||
raise ValidationError("This project already have this member")
|
||||
|
||||
def save(self, project, person):
|
||||
# if the user is already bound to the project, just reactivate him
|
||||
person.name = self.name.data
|
||||
person.project = project
|
||||
|
||||
return person
|
||||
|
||||
class InviteForm(Form):
|
||||
emails = TextAreaField("People to notify")
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from datetime import datetime
|
||||
from flaskext.sqlalchemy import SQLAlchemy
|
||||
from flaskext.sqlalchemy import SQLAlchemy, BaseQuery
|
||||
from flask import g
|
||||
|
||||
from sqlalchemy import orm
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
# define models
|
||||
class Project(db.Model):
|
||||
|
||||
_to_serialize = ("id", "name", "password", "contact_email",
|
||||
"members", "active_members")
|
||||
|
||||
id = db.Column(db.String, primary_key=True)
|
||||
|
||||
name = db.Column(db.UnicodeText)
|
||||
|
@ -68,6 +75,23 @@ class Project(db.Model):
|
|||
|
||||
|
||||
class Person(db.Model):
|
||||
|
||||
class PersonQuery(BaseQuery):
|
||||
def get_by_name(self, name, project):
|
||||
return Person.query.filter(Person.name == name)\
|
||||
.filter(Project.id == project.id).one()
|
||||
|
||||
def get(self, id, project=None):
|
||||
if not project:
|
||||
project = g.project
|
||||
return Person.query.filter(Person.id == id)\
|
||||
.filter(Project.id == project.id).one()
|
||||
|
||||
|
||||
query_class = PersonQuery
|
||||
|
||||
_to_serialize = ("id", "name", "activated")
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
project_id = db.Column(db.Integer, db.ForeignKey("project.id"))
|
||||
bills = db.relationship("Bill", backref="payer")
|
||||
|
@ -96,6 +120,29 @@ billowers = db.Table('billowers',
|
|||
)
|
||||
|
||||
class Bill(db.Model):
|
||||
|
||||
class BillQuery(BaseQuery):
|
||||
|
||||
def get(self, project, id):
|
||||
try:
|
||||
return self.join(Person, Project)\
|
||||
.filter(Bill.payer_id == Person.id)\
|
||||
.filter(Person.project_id == Project.id)\
|
||||
.filter(Project.id == project.id)\
|
||||
.filter(Bill.id == id).one()
|
||||
except orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
def delete(self, project, id):
|
||||
bill = self.get(project, id)
|
||||
if bill:
|
||||
db.session.delete(bill)
|
||||
return bill
|
||||
|
||||
query_class = BillQuery
|
||||
|
||||
_to_serialize = ("id", "payer_id", "owers", "amount", "date", "what")
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
payer_id = db.Column(db.Integer, db.ForeignKey("person.id"))
|
||||
|
@ -115,7 +162,6 @@ class Bill(db.Model):
|
|||
return "<Bill of %s from %s for %s>" % (self.amount,
|
||||
self.payer, ", ".join([o.name for o in self.owers]))
|
||||
|
||||
|
||||
class Archive(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
project_id = db.Column(db.Integer, db.ForeignKey("project.id"))
|
||||
|
@ -131,3 +177,4 @@ class Archive(db.Model):
|
|||
|
||||
def __repr__(self):
|
||||
return "<Archive>"
|
||||
|
||||
|
|
158
budget/rest.py
Normal file
158
budget/rest.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
import json
|
||||
from flask import request
|
||||
import werkzeug
|
||||
|
||||
class RESTResource(object):
|
||||
"""Represents a REST resource, with the different HTTP verbs"""
|
||||
_NEED_ID = ["get", "update", "delete"]
|
||||
_VERBS = {"get": "GET",
|
||||
"update": "PUT",
|
||||
"delete": "DELETE",
|
||||
"list": "GET",
|
||||
"add": "POST",}
|
||||
|
||||
def __init__(self, name, route, app, handler, authentifier=None,
|
||||
actions=None, inject_name=None):
|
||||
"""
|
||||
:name:
|
||||
name of the resource. This is being used when registering
|
||||
the route, for its name and for the name of the id parameter
|
||||
that will be passed to the views
|
||||
|
||||
:route:
|
||||
Default route for this resource
|
||||
|
||||
:app:
|
||||
Application to register the routes onto
|
||||
|
||||
:actions:
|
||||
Authorized actions. Optional. None means all.
|
||||
|
||||
:handler:
|
||||
The handler instance which will handle the requests
|
||||
|
||||
:authentifier:
|
||||
callable checking the authentication. If specified, all the
|
||||
methods will be checked against it.
|
||||
"""
|
||||
if not actions:
|
||||
actions = self._VERBS.keys()
|
||||
|
||||
self._route = route
|
||||
self._handler = handler
|
||||
self._name = name
|
||||
self._identifier = "%s_id" % name
|
||||
self._authentifier = authentifier
|
||||
self._inject_name = inject_name # FIXME
|
||||
|
||||
for action in actions:
|
||||
self.add_url_rule(app, action)
|
||||
|
||||
def _get_route_for(self, action):
|
||||
"""Return the complete URL for this action.
|
||||
|
||||
Basically:
|
||||
|
||||
- get, update and delete need an id
|
||||
- add and list does not
|
||||
"""
|
||||
route = self._route
|
||||
|
||||
if action in self._NEED_ID:
|
||||
route += "/<%s>" % self._identifier
|
||||
|
||||
return route
|
||||
|
||||
def add_url_rule(self, app, action):
|
||||
"""Registers a new url to the given application, regarding
|
||||
the action.
|
||||
"""
|
||||
method = getattr(self._handler, action)
|
||||
|
||||
# decorate the view
|
||||
if self._authentifier:
|
||||
method = need_auth(self._authentifier,
|
||||
self._inject_name or self._name)(method)
|
||||
|
||||
method = serialize(method)
|
||||
|
||||
app.add_url_rule(
|
||||
self._get_route_for(action),
|
||||
"%s_%s" % (self._name, action),
|
||||
method,
|
||||
methods=[self._VERBS.get(action, "GET")])
|
||||
|
||||
|
||||
def need_auth(authentifier, name=None, remove_attr=True):
|
||||
"""Decorator checking that the authentifier does not returns false in
|
||||
the current context.
|
||||
|
||||
If the request is authorized, the object returned by the authentifier
|
||||
is added to the kwargs of the method.
|
||||
|
||||
If not, issue a 403 Forbidden error
|
||||
|
||||
:authentifier:
|
||||
The callable to check the context onto.
|
||||
|
||||
:name:
|
||||
**Optional**, name of the argument to put the object into.
|
||||
If it is not provided, nothing will be added to the kwargs
|
||||
of the decorated function
|
||||
|
||||
:remove_attr:
|
||||
Remove or not the `*name*_id` from the kwargs before calling the
|
||||
function
|
||||
"""
|
||||
def wrapper(func):
|
||||
def wrapped(*args, **kwargs):
|
||||
result = authentifier(*args, **kwargs)
|
||||
if result:
|
||||
if name:
|
||||
kwargs[name] = result
|
||||
if remove_attr:
|
||||
del kwargs["%s_id" % name]
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
return 403, "Forbidden"
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
# serializers
|
||||
|
||||
def serialize(func):
|
||||
"""If the object returned by the view is not already a Response, serialize
|
||||
it using the ACCEPT header and return it.
|
||||
"""
|
||||
def wrapped(*args, **kwargs):
|
||||
# get the mimetype
|
||||
mime = request.accept_mimetypes.best_match(SERIALIZERS.keys())
|
||||
data = func(*args, **kwargs)
|
||||
serializer = SERIALIZERS[mime]
|
||||
|
||||
status = 200
|
||||
if len(data) == 2:
|
||||
status, data = data
|
||||
|
||||
# serialize it
|
||||
return werkzeug.Response(serializer.encode(data),
|
||||
status=status, mimetype=mime)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
"""Subclass of the default encoder to support custom objects"""
|
||||
def default(self, o):
|
||||
if hasattr(o, "_to_serialize"):
|
||||
# build up the object
|
||||
data = {}
|
||||
for attr in o._to_serialize:
|
||||
data[attr] = getattr(o, attr)
|
||||
return data
|
||||
elif hasattr(o, "isoformat"):
|
||||
return o.isoformat()
|
||||
else:
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
SERIALIZERS = {"text/json": JSONEncoder()}
|
|
@ -1,11 +1,13 @@
|
|||
from web import main, db, mail
|
||||
#import api
|
||||
from api import api
|
||||
|
||||
from flask import *
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object("default_settings")
|
||||
app.register_blueprint(main)
|
||||
app.register_blueprint(api)
|
||||
|
||||
|
||||
# db
|
||||
db.init_app(app)
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
@import "bootstrap-1.0.0.min.css";
|
||||
@import url(http://fonts.googleapis.com/css?family=Lobster|Comfortaa);
|
||||
|
||||
html body{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.topbar h3{ margin-left: 75px; }
|
||||
.topbar ul{ padding-left: 75px; }
|
||||
div.topbar ul.secondary-nav { padding-right: 75px; }
|
||||
|
@ -80,8 +84,9 @@ div.topbar ul.secondary-nav { padding-right: 75px; }
|
|||
height: 100px;
|
||||
}
|
||||
|
||||
.footer{
|
||||
position: absolute;
|
||||
#footer{
|
||||
margin-top: 30px;
|
||||
position: relative;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
|
||||
{% block content %}
|
||||
<h2>Create a new project</h2>
|
||||
<div class="uniForm">
|
||||
<form method="post" class="container span-24 add-bill">
|
||||
{{ forms.create_project(form) }}
|
||||
</form>
|
||||
</div>
|
||||
<form method="post">
|
||||
{{ forms.create_project(form) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -57,6 +57,17 @@
|
|||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro edit_project(form) %}
|
||||
|
||||
{% include "display_errors.html" %}
|
||||
{{ form.hidden_tag() }}
|
||||
{{ input(form.name) }}
|
||||
{{ input(form.password) }}
|
||||
{{ input(form.contact_email) }}
|
||||
{{ submit(form.submit) }}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro add_bill(form, edit=False) %}
|
||||
|
||||
<fieldset>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<li class="menu">
|
||||
<a href="#" class="menu"><strong>{{ g.project.name }}</strong> options</a>
|
||||
<ul class="menu-dropdown" style="display: none; ">
|
||||
<li><a href="">Project settings</a></li>
|
||||
<li><a href="{{ url_for(".edit_project") }}">Project settings</a></li>
|
||||
<li class="divider"></li>
|
||||
{% for id, name in session['projects'] %}
|
||||
{% if id != g.project.id %}
|
||||
|
@ -81,7 +81,7 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
<div class="footer">
|
||||
<div id="footer">
|
||||
<p><a href="https://github.com/ametaireau/budget-manager">This is a free software</a>, you can contribute and improve it!</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -30,9 +30,8 @@ class TestCase(unittest.TestCase):
|
|||
|
||||
def login(self, project, password=None, test_client=None):
|
||||
password = password or project
|
||||
test_client = test_client or self.app
|
||||
|
||||
return test_client.post('/authenticate', data=dict(
|
||||
return self.app.post('/authenticate', data=dict(
|
||||
id=project, password=password), follow_redirects=True)
|
||||
|
||||
def post_project(self, name):
|
||||
|
@ -187,6 +186,15 @@ class BudgetTestCase(TestCase):
|
|||
self.assertEqual(
|
||||
len(models.Project.query.get("raclette").active_members), 2)
|
||||
|
||||
# adding an user with the same name as another user from a different
|
||||
# project should not cause any troubles
|
||||
self.post_project("randomid")
|
||||
self.login("randomid")
|
||||
self.app.post("/randomid/members/add", data={'name': 'fred' })
|
||||
self.assertEqual(
|
||||
len(models.Project.query.get("randomid").active_members), 1)
|
||||
|
||||
|
||||
def test_demo(self):
|
||||
# Test that it is possible to connect automatically by going onto /demo
|
||||
with run.app.test_client() as c:
|
||||
|
@ -299,6 +307,31 @@ class BudgetTestCase(TestCase):
|
|||
balance = models.Project.query.get("raclette").get_balance()
|
||||
self.assertEqual(set(balance.values()), set([19.0, -19.0]))
|
||||
|
||||
def test_edit_project(self):
|
||||
# A project should be editable
|
||||
|
||||
self.post_project("raclette")
|
||||
new_data = {
|
||||
'name': 'Super raclette party!',
|
||||
'contact_email': 'alexis@notmyidea.org',
|
||||
'password': 'didoudida'
|
||||
}
|
||||
|
||||
resp = self.app.post("/raclette/edit", data=new_data,
|
||||
follow_redirects=True)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
project = models.Project.query.get("raclette")
|
||||
|
||||
for key, value in new_data.items():
|
||||
self.assertEqual(getattr(project, key), value, key)
|
||||
|
||||
# Editing a project with a wrong email address should fail
|
||||
new_data['contact_email'] = 'wrong_email'
|
||||
|
||||
resp = self.app.post("/raclette/edit", data=new_data,
|
||||
follow_redirects=True)
|
||||
self.assertIn("Invalid email address", resp.data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import re
|
||||
from functools import wraps
|
||||
import inspect
|
||||
|
||||
from flask import redirect, url_for, session, request
|
||||
from werkzeug.routing import HTTPException, RoutingException
|
||||
|
||||
|
@ -29,3 +31,12 @@ class Redirect303(HTTPException, RoutingException):
|
|||
|
||||
def get_response(self, environ):
|
||||
return redirect(self.new_url, 303)
|
||||
|
||||
def for_all_methods(decorator):
|
||||
"""Apply a decorator to all the methods of a class"""
|
||||
def decorate(cls):
|
||||
for name, method in inspect.getmembers(cls, inspect.ismethod):
|
||||
setattr(cls, name, decorator(method))
|
||||
return cls
|
||||
return decorate
|
||||
|
||||
|
|
|
@ -2,11 +2,12 @@ from collections import defaultdict
|
|||
|
||||
from flask import *
|
||||
from flaskext.mail import Mail, Message
|
||||
import werkzeug
|
||||
|
||||
# local modules
|
||||
from models import db, Project, Person, Bill
|
||||
from forms import (get_billform_for, ProjectForm, AuthenticationForm, BillForm,
|
||||
MemberForm, InviteForm, CreateArchiveForm)
|
||||
MemberForm, InviteForm, CreateArchiveForm, EditProjectForm)
|
||||
from utils import Redirect303
|
||||
|
||||
"""
|
||||
|
@ -62,7 +63,7 @@ def pull_project(endpoint, values):
|
|||
def authenticate(project_id=None):
|
||||
"""Authentication form"""
|
||||
form = AuthenticationForm()
|
||||
if not form.id.data and request.args['project_id']:
|
||||
if not form.id.data and request.args.get('project_id'):
|
||||
form.id.data = request.args['project_id']
|
||||
project_id = form.id.data
|
||||
project = Project.query.get(project_id)
|
||||
|
@ -70,7 +71,10 @@ def authenticate(project_id=None):
|
|||
if not project:
|
||||
# But if the user try to connect to an unexisting project, we will
|
||||
# propose him a link to the creation form.
|
||||
create_project = project_id
|
||||
if request.method == "POST":
|
||||
form.validate()
|
||||
else:
|
||||
create_project = project_id
|
||||
|
||||
else:
|
||||
# if credentials are already in session, redirect
|
||||
|
@ -145,6 +149,24 @@ def create_project():
|
|||
|
||||
return render_template("create_project.html", form=form)
|
||||
|
||||
@main.route("/<project_id>/edit", methods=["GET", "POST"])
|
||||
def edit_project():
|
||||
form = EditProjectForm()
|
||||
if request.method == "POST":
|
||||
if form.validate():
|
||||
project = form.update(g.project)
|
||||
db.session.commit()
|
||||
session[project.id] = project.password
|
||||
|
||||
return redirect(url_for(".list_bills"))
|
||||
else:
|
||||
form.name.data = g.project.name
|
||||
form.password.data = g.project.password
|
||||
form.contact_email.data = g.project.contact_email
|
||||
|
||||
return render_template("edit_project.html", form=form)
|
||||
|
||||
|
||||
@main.route("/exit")
|
||||
def exit():
|
||||
# delete the session
|
||||
|
@ -206,18 +228,11 @@ def add_member():
|
|||
form = MemberForm(g.project)
|
||||
if request.method == "POST":
|
||||
if form.validate():
|
||||
# if the user is already bound to the project, just reactivate him
|
||||
person = Person.query.filter(Person.name == form.name.data)\
|
||||
.filter(Project.id == g.project.id).all()
|
||||
if person:
|
||||
person[0].activated = True
|
||||
db.session.commit()
|
||||
flash("%s is part of this project again" % person[0].name)
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
db.session.add(Person(name=form.name.data, project=g.project))
|
||||
member = form.save(g.project, Person())
|
||||
db.session.commit()
|
||||
flash("%s is had been added" % member.name)
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
return render_template("add_member.html", form=form)
|
||||
|
||||
@main.route("/<project_id>/members/<member_id>/reactivate", methods=["GET",])
|
||||
|
@ -258,7 +273,11 @@ def add_bill():
|
|||
|
||||
@main.route("/<project_id>/delete/<int:bill_id>")
|
||||
def delete_bill(bill_id):
|
||||
bill = Bill.query.get_or_404(bill_id)
|
||||
# fixme: everyone is able to delete a bill
|
||||
bill = Bill.query.get(g.project, bill_id)
|
||||
if not bill:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
db.session.delete(bill)
|
||||
db.session.commit()
|
||||
flash("The bill has been deleted")
|
||||
|
@ -268,8 +287,13 @@ def delete_bill(bill_id):
|
|||
|
||||
@main.route("/<project_id>/edit/<int:bill_id>", methods=["GET", "POST"])
|
||||
def edit_bill(bill_id):
|
||||
bill = Bill.query.get_or_404(bill_id)
|
||||
# FIXME: Test this bill belongs to this project !
|
||||
bill = Bill.query.get(g.project, bill_id)
|
||||
if not bill:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
form = get_billform_for(request, g.project, set_default=False)
|
||||
|
||||
if request.method == 'POST' and form.validate():
|
||||
form.save(bill)
|
||||
db.session.commit()
|
||||
|
|
Loading…
Reference in a new issue