Merge branch 'master' into almet/fix-members-order

This commit is contained in:
Alexis Metaireau 2018-09-03 20:58:05 +02:00 committed by GitHub
commit d6fbae2950
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 221 additions and 102 deletions

View file

@ -14,6 +14,7 @@ Fixed
- Fix the generation of the supervisord template (#309) - Fix the generation of the supervisord template (#309)
- Fix the validation of the hashed password (#310) - Fix the validation of the hashed password (#310)
- Fix infinite loop that happened when accessing / (#358) - Fix infinite loop that happened when accessing / (#358)
- Fix email validation when sending invites
Added Added
===== =====

View file

@ -7,52 +7,71 @@ DOC_STAMP = $(VENV)/.doc_env_installed.stamp
INSTALL_STAMP = $(VENV)/.install.stamp INSTALL_STAMP = $(VENV)/.install.stamp
TEMPDIR := $(shell mktemp -d) TEMPDIR := $(shell mktemp -d)
all: install .PHONY: all
install: virtualenv $(INSTALL_STAMP) all: install ## Alias for install
.PHONY: install
install: virtualenv $(INSTALL_STAMP) ## Install dependencies
$(INSTALL_STAMP): $(INSTALL_STAMP):
$(VENV)/bin/pip install -U pip $(VENV)/bin/pip install -U pip
$(VENV)/bin/pip install -r requirements.txt $(VENV)/bin/pip install -r requirements.txt
touch $(INSTALL_STAMP) touch $(INSTALL_STAMP)
.PHONY: virtualenv
virtualenv: $(PYTHON) virtualenv: $(PYTHON)
$(PYTHON): $(PYTHON):
$(VIRTUALENV) $(VENV) $(VIRTUALENV) $(VENV)
install-dev: $(INSTALL_STAMP) $(DEV_STAMP) .PHONY: install-dev
install-dev: $(INSTALL_STAMP) $(DEV_STAMP) ## Install development dependencies
$(DEV_STAMP): $(PYTHON) dev-requirements.txt $(DEV_STAMP): $(PYTHON) dev-requirements.txt
$(VENV)/bin/pip install -Ur dev-requirements.txt $(VENV)/bin/pip install -Ur dev-requirements.txt
touch $(DEV_STAMP) touch $(DEV_STAMP)
.PHONY: remove-install-stamp
remove-install-stamp: remove-install-stamp:
rm $(INSTALL_STAMP) rm $(INSTALL_STAMP)
update: remove-install-stamp install .PHONY: update
update: remove-install-stamp install ## Update the dependencies
serve: install .PHONY: serve
serve: install ## Run the ihatemoney server
@echo 'Running ihatemoney on http://localhost:5000'
$(PYTHON) -m ihatemoney.manage runserver $(PYTHON) -m ihatemoney.manage runserver
test: $(DEV_STAMP) .PHONY: test
test: $(DEV_STAMP) ## Run the tests
$(VENV)/bin/tox $(VENV)/bin/tox
release: $(DEV_STAMP) .PHONY: release
release: $(DEV_STAMP) ## Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
$(VENV)/bin/fullrelease $(VENV)/bin/fullrelease
build-translations: .PHONY: build-translations
build-translations: ## Build the translations
$(VENV)/bin/pybabel compile -d ihatemoney/translations $(VENV)/bin/pybabel compile -d ihatemoney/translations
update-translations: .PHONY: update-translations
update-translations: ## Extract new translations from source code
$(VENV)/bin/pybabel extract --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney $(VENV)/bin/pybabel extract --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney
$(VENV)/bin/pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/ $(VENV)/bin/pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
create-database-revision: .PHONY: create-database-revision
create-database-revision: ## Create a new database revision
@read -p "Please enter a message describing this revision: " rev_message; \ @read -p "Please enter a message describing this revision: " rev_message; \
$(PYTHON) -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}" $(PYTHON) -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}"
build-requirements: .PHONY: build-requirements
build-requirements: ## Save currently installed packages to requirements.txt
$(VIRTUALENV) $(TEMPDIR) $(VIRTUALENV) $(TEMPDIR)
$(TEMPDIR)/bin/pip install -U pip $(TEMPDIR)/bin/pip install -U pip
$(TEMPDIR)/bin/pip install -Ue "." $(TEMPDIR)/bin/pip install -Ue "."
$(TEMPDIR)/bin/pip freeze | grep -v -- '-e' > requirements.txt $(TEMPDIR)/bin/pip freeze | grep -v -- '-e' > requirements.txt
clean: .PHONY: clean
clean: ## Destroy the virtual environment
rm -rf .venv rm -rf .venv
.PHONY: help
help: ## Show the help indications
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View file

@ -21,3 +21,10 @@ Requirements
* **Python**: 2.7, 3.4, 3.5, 3.6. * **Python**: 2.7, 3.4, 3.5, 3.6.
* **Backends**: MySQL, PostgreSQL, SQLite, Memory. * **Backends**: MySQL, PostgreSQL, SQLite, Memory.
Contributing
============
Do you wish to contribute to IHateMoney? Fantastic! There's a lot of very
useful help on the official `contributing
<https://ihatemoney.readthedocs.io/en/latest/contributing.html>`_ page.

View file

@ -13,7 +13,7 @@ MAIL_USERNAME = "$MAIL_USERNAME"
MAIL_PASSWORD = "$MAIL_PASSWORD" MAIL_PASSWORD = "$MAIL_PASSWORD"
MAIL_DEFAULT_SENDER = "$MAIL_DEFAULT_SENDER" MAIL_DEFAULT_SENDER = "$MAIL_DEFAULT_SENDER"
ACTIVATE_DEMO_PROJECT = $ACTIVATE_DEMO_PROJECT ACTIVATE_DEMO_PROJECT = $ACTIVATE_DEMO_PROJECT
ADMIN_PASSWORD = "$ADMIN_PASSWORD" ADMIN_PASSWORD = '$ADMIN_PASSWORD'
ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION
ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD
EOF EOF

View file

@ -189,52 +189,98 @@ deployed instance, simply look at your *ihatemoney.cfg*.
Production values are recommended values for use in production. Production values are recommended values for use in production.
`SQLALCHEMY_DATABASE_URI`
-------------------------
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ Specifies the type of backend to use and its location. More information on the
| Setting name | Default | What does it do? | format used can be found on `the SQLAlchemy documentation`_.
+===============================+=================================+========================================================================================+
| SQLALCHEMY_DATABASE_URI | ``sqlite:///tmp/ihatemoney.db`` | Specifies the type of backend to use and its location. More information on the | - **default value:** ``sqlite:///tmp/ihatemoney.db``
| | | format used can be found on `the SQLAlchemy documentation`_. | - **Production value:** Set it to some path on your disk. Typically
| | | | ``sqlite:///home/ihatemoney/ihatemoney.db``. Do *not* store it under
| | | **Production value:** Set it to some path on your disk. Typically | ``/tmp`` as this folder is cleared at each boot.
| | | ``sqlite:///home/ihatemoney/ihatemoney.db``. Do *not* store it under ``/tmp`` as this |
| | | folder is cleared at each boot. | `SECRET_KEY`
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ ------------
| SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. |
| | | | The secret key used to encrypt the cookies.
| | | **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to something |
| | | random, which is good. | - **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ something random, which is good.
| MAIL_DEFAULT_SENDER | ``("Budget manager", | A python tuple describing the name and email address to use when sending |
| | "budget@notmyidea.org")`` | emails. | `MAIL_DEFAULT_SENDER`
| | | | ---------------------
| | | **Production value:** Any tuple you want. |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ A python tuple describing the name and email address to use when sending emails.
| ACTIVATE_DEMO_PROJECT | ``True`` | If set to `True`, a demo project will be available on the frontpage. |
| | | | - **Default value:** ``("Budget manager", "budget@notmyidea.org")``
| | | **Production value:** Usually, you will want to set it to ``False`` for a private | - **Production value:** Any tuple you want.
| | | instance. |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ `ACTIVATE_DEMO_PROJECT`
| | | Hashed password to access protected endpoints. If left empty, all administrative | -----------------------
| ADMIN_PASSWORD | ``""`` | tasks are disabled. |
| | | | If set to `True`, a demo project will be available on the frontpage.
| | | **Production value:** To generate the proper password HASH, use |
| | | ``ihatemoney generate_password_hash`` and copy the output into the value of | - **Default value:** ``True``
| | | *ADMIN_PASSWORD*. | - **Production value:** Usually, you will want to set it to ``False`` for a
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ private instance.
| ALLOW_PUBLIC_PROJECT_CREATION | ``True`` | If set to ``True``, everyone can create a project without entering the admin password |
| | | If set to ``False``, the password needs to be entered (and as such, defined in the | `ADMIN_PASSWORD`
| | | settings). | ----------------
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| ACTIVATE_ADMIN_DASHBOARD | ``False`` | If set to `True`, the dashboard will become accessible entering the admin password | Hashed password to access protected endpoints. If left empty, all administrative
| | | If set to `True`, a non empty ADMIN_PASSWORD needs to be set | tasks are disabled.
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| APPLICATION_ROOT | ``""`` | If empty, ihatemoney will be served at domain root (e.g: *http://domain.tld*), if set | - **Default value:** ``""`` (empty string)
| | | to ``"foo"``, it will be served from a "folder" (e.g: *http://domain.tld/foo*) | - **Production value:** To generate the proper password HASH, use
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+ ``ihatemoney generate_password_hash`` and copy the output into the value of
*ADMIN_PASSWORD*.
`ALLOW_PUBLIC_PROJECT_CREATION`
-------------------------------
If set to ``True``, everyone can create a project without entering the admin
password. If set to ``False``, the password needs to be entered (and as such,
defined in the settings).
- **Default value:** : ``True``.
`ACTIVATE_ADMIN_DASHBOARD`
--------------------------
If set to `True`, the dashboard will become accessible entering the admin
password, if set to `True`, a non empty ADMIN_PASSWORD needs to be set.
- **Default value**: ``False``
`APPLICATION_ROOT`
------------------
If empty, ihatemoney will be served at domain root (e.g: *http://domain.tld*),
if set to ``"somestring"``, it will be served from a "folder"
(e.g: *http://domain.tld/somestring*).
- **Default value:** ``""`` (empty string)
.. _the SQLAlchemy documentation: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls .. _the SQLAlchemy documentation: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls
Configuring emails sending
--------------------------
By default, Ihatemoney sends emails using a local SMTP server, but it's
possible to configure it to act differently, thanks to the great
`Flask-Mail project <https://pythonhosted.org/flask-mail/#configuring-flask-mail>`_
* **MAIL_SERVER** : default **'localhost'**
* **MAIL_PORT** : default **25**
* **MAIL_USE_TLS** : default **False**
* **MAIL_USE_SSL** : default **False**
* **MAIL_DEBUG** : default **app.debug**
* **MAIL_USERNAME** : default **None**
* **MAIL_PASSWORD** : default **None**
* **DEFAULT_MAIL_SENDER** : default **None**
Using an alternate settings path Using an alternate settings path
-------------------------------- --------------------------------

View file

@ -10,6 +10,8 @@ from werkzeug.security import generate_password_hash
from datetime import datetime from datetime import datetime
from jinja2 import Markup from jinja2 import Markup
import email_validator
from ihatemoney.models import Project, Person from ihatemoney.models import Project, Person
from ihatemoney.utils import slugify from ihatemoney.utils import slugify
@ -184,9 +186,10 @@ class InviteForm(FlaskForm):
submit = SubmitField(_("Send invites")) submit = SubmitField(_("Send invites"))
def validate_emails(form, field): def validate_emails(form, field):
validator = Email()
for email in [email.strip() for email in form.emails.data.split(",")]: for email in [email.strip() for email in form.emails.data.split(",")]:
if not validator.regex.match(email): try:
email_validator.validate_email(email)
except email_validator.EmailNotValidError as e:
raise ValidationError(_("The email %(email)s is not valid", raise ValidationError(_("The email %(email)s is not valid",
email=email)) email=email))

View file

@ -10,7 +10,7 @@ from flask_migrate import Migrate, MigrateCommand
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from ihatemoney.run import create_app from ihatemoney.run import create_app
from ihatemoney.models import db from ihatemoney.models import db, Project
from ihatemoney.utils import create_jinja_env from ihatemoney.utils import create_jinja_env
@ -57,6 +57,13 @@ class GenerateConfig(Command):
)) ))
class DeleteProject(Command):
def run(self, project_name):
demo_project = Project.query.get(project_name)
db.session.delete(demo_project)
db.session.commit()
def main(): def main():
QUIET_COMMANDS = ('generate_password_hash', 'generate-config') QUIET_COMMANDS = ('generate_password_hash', 'generate-config')
@ -76,6 +83,7 @@ def main():
manager.add_command('db', MigrateCommand) manager.add_command('db', MigrateCommand)
manager.add_command('generate_password_hash', GeneratePasswordHash) manager.add_command('generate_password_hash', GeneratePasswordHash)
manager.add_command('generate-config', GenerateConfig) manager.add_command('generate-config', GenerateConfig)
manager.add_command('delete-project', DeleteProject)
manager.run() manager.run()

View file

@ -182,6 +182,12 @@ msgstr ""
msgid "The bill has been modified" msgid "The bill has been modified"
msgstr "" msgstr ""
msgid "Sorry, we were unable to find the page you've asked for."
msgstr ""
msgid "The best thing to do is probably to get back to the main page."
msgstr ""
msgid "Back to the list" msgid "Back to the list"
msgstr "" msgstr ""

View file

@ -2,11 +2,10 @@ import os
import os.path import os.path
import warnings import warnings
from flask import Flask, g, request, session from flask import Flask, g, request, session, render_template
from flask_babel import Babel from flask_babel import Babel
from flask_mail import Mail from flask_mail import Mail
from flask_migrate import Migrate, upgrade, stamp from flask_migrate import Migrate, upgrade, stamp
from raven.contrib.flask import Sentry
from werkzeug.contrib.fixers import ProxyFix from werkzeug.contrib.fixers import ProxyFix
from ihatemoney.api import api from ihatemoney.api import api
@ -103,6 +102,10 @@ def validate_configuration(app):
) )
def page_not_found(e):
return render_template('404.html', root="main"), 404
def create_app(configuration=None, instance_path='/etc/ihatemoney', def create_app(configuration=None, instance_path='/etc/ihatemoney',
instance_relative_config=True): instance_relative_config=True):
app = Flask( app = Flask(
@ -122,17 +125,15 @@ def create_app(configuration=None, instance_path='/etc/ihatemoney',
validate_configuration(app) validate_configuration(app)
app.register_blueprint(web_interface) app.register_blueprint(web_interface)
app.register_blueprint(api) app.register_blueprint(api)
app.register_error_handler(404, page_not_found)
# Configure the application # Configure the a, root="main"pplication
setup_database(app) setup_database(app)
mail = Mail() mail = Mail()
mail.init_app(app) mail.init_app(app)
app.mail = mail app.mail = mail
# Error reporting
Sentry(app)
# Jinja filters # Jinja filters
app.jinja_env.filters['minimal_round'] = minimal_round app.jinja_env.filters['minimal_round'] = minimal_round

View file

@ -224,6 +224,10 @@ tr.payer_line .balance-name{
padding: 0 0 0 20px; padding: 0 0 0 20px;
} }
.action button:hover {
text-decoration: underline;
}
.delete button, .delete button:hover { .delete button, .delete button:hover {
background: url('../images/deleter.png') left no-repeat; background: url('../images/deleter.png') left no-repeat;
color: red; color: red;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,18 +1,7 @@
// Add scripts to select all or non of the checkboxes in the add_bill form // Utility to select all or none of the checkboxes in the add_bill form.
function selectall() function selectCheckboxes(value){
{ var els = document.getElementsByName('payed_for');
var els = document.getElementsByName('payed_for'); for(var i = 0; i < els.length; i++){
for(var i =0;i<els.length;i++) els[i].checked = value;
{ }
els[i].checked=true; }
}
}
function selectnone()
{
var els = document.getElementsByName('payed_for');
for(var i =0;i<els.length;i++)
{
els[i].checked=false;
}
}

View file

@ -0,0 +1,13 @@
{% extends "layout.html" %}
{% block body %}
<header id="header" class="row">
<div class="col-xs-12 col-sm-5 offset-md-2">
<h2>{{ _("Sorry, we were unable to find the page you've asked for.") }}</h2>
</div>
</header>
<main class="row home">
<div class="col-xs-12 col-sm-5 col-md-4 offset-md-2">
<a href='{{ url_for("main.home") }}'>{{ _("The best thing to do is probably to get back to the main page.")}}</a>
</div>
{% endblock %}

View file

@ -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="alert alert-danger"><strong>{{ form[field_name].label.text }}:</strong> {{ error }}</p> <p class="alert alert-danger"><strong>{{ form[field_name].label.text }}:</strong> {{ error|escape }}</p>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View file

@ -100,7 +100,7 @@
<label class="col-3" for="payed_for">{{ _("For whom?") }}</label> <label class="col-3" for="payed_for">{{ _("For whom?") }}</label>
<div class="controls col-9"> <div class="controls col-9">
<ul id="payed_for" class="inputs-list"> <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> <p><a href="#" id="selectall" onclick="selectCheckboxes(true)">{{ _("Select all") }}</a> | <a href="#" id="selectnone" onclick="selectCheckboxes(false)">{{_("Select none")}}</a></p>
{% for key, value, checked in form.payed_for.iter_choices() | sort(attribute='1') %} {% for key, value, checked in form.payed_for.iter_choices() | sort(attribute='1') %}
<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> <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 %} {% endfor %}

View file

@ -32,13 +32,13 @@
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarToggler"> <div class="collapse navbar-collapse" id="navbarToggler">
<h1><a class="navbar-brand" href="{{ url_for(".home") }}">#! money?</a></h1> <h1><a class="navbar-brand" href="{{ url_for("main.home") }}">#! money?</a></h1>
<ul class="navbar-nav ml-auto mr-auto"> <ul class="navbar-nav ml-auto mr-auto">
{% if g.project %} {% if g.project %}
{% block navbar %} {% block navbar %}
<li class="nav-item{% if current_view == 'list_bills' %} active{% endif %}"><a class="nav-link" 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("main.list_bills") }}">{{ _("Bills") }}</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> <li class="nav-item{% if current_view == 'settle_bill' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.settle_bill") }}">{{ _("Settle") }}</a></li>
<li class="nav-item{% if current_view == 'statistics' %} active{% endif %}"><a class="nav-link" href="{{ url_for(".statistics") }}">{{ _("Statistics") }}</a></li> <li class="nav-item{% if current_view == 'statistics' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.statistics") }}">{{ _("Statistics") }}</a></li>
{% endblock %} {% endblock %}
{% endif %} {% endif %}
</ul> </ul>
@ -47,23 +47,23 @@
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<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> <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" aria-labelledby="navbarDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><a class="dropdown-item" href="{{ url_for(".edit_project") }}">{{ _("Project settings") }}</a></li> <li><a class="dropdown-item" href="{{ url_for("main.edit_project") }}">{{ _("Project settings") }}</a></li>
<li class="dropdown-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 class="dropdown-item" href="{{ url_for(".list_bills", project_id=id) }}">{{ _("switch to") }} {{ name }}</a></li> <li><a class="dropdown-item" href="{{ url_for("main.list_bills", project_id=id) }}">{{ _("switch to") }} {{ name }}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<li><a class="dropdown-item" href="{{ url_for(".create_project") }}">{{ _("Start a new project") }}</a></li> <li><a class="dropdown-item" href="{{ url_for("main.create_project") }}">{{ _("Start a new project") }}</a></li>
<li class="dropdown-divider"></li> <li class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for(".exit") }}">{{ _("Logout") }}</a></li> <li><a class="dropdown-item" href="{{ url_for("main.exit") }}">{{ _("Logout") }}</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
<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 class="nav-item{% if g.lang == "fr" %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.change_lang", lang="fr") }}">fr</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> <li class="nav-item{% if g.lang == "en" %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.change_lang", lang="en") }}">en</a></li>
{% if g.show_admin_dashboard_link %} {% if g.show_admin_dashboard_link %}
<li class="nav-item{% if request.url_rule.endpoint == "main.dashboard" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".dashboard") }}">{{ _("Dashboard") }}</a></li> <li class="nav-item{% if request.url_rule.endpoint == "main.dashboard" %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.dashboard") }}">{{ _("Dashboard") }}</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View file

@ -20,7 +20,8 @@ from flask import session
from flask_testing import TestCase from flask_testing import TestCase
from ihatemoney.run import create_app, db, load_configuration from ihatemoney.run import create_app, db, load_configuration
from ihatemoney.manage import GenerateConfig, GeneratePasswordHash from ihatemoney.manage import (
GenerateConfig, GeneratePasswordHash, DeleteProject)
from ihatemoney import models from ihatemoney import models
from ihatemoney import utils from ihatemoney import utils
@ -1472,6 +1473,15 @@ class CommandTestCase(BaseTestCase):
print(stdout.getvalue()) print(stdout.getvalue())
self.assertEqual(len(stdout.getvalue().strip()), 187) self.assertEqual(len(stdout.getvalue().strip()), 187)
def test_demo_project_deletion(self):
self.create_project('demo')
self.assertEquals(models.Project.query.get('demo').name, 'demo')
cmd = DeleteProject()
cmd.run('demo')
self.assertEqual(len(models.Project.query.all()), 0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-07-16 23:26+0200\n" "POT-Creation-Date: 2018-08-05 23:41+0200\n"
"PO-Revision-Date: 2018-05-15 22:00+0200\n" "PO-Revision-Date: 2018-05-15 22:00+0200\n"
"Last-Translator: Adrien CLERC <>\n" "Last-Translator: Adrien CLERC <>\n"
"Language: fr\n" "Language: fr\n"
@ -16,7 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.5.3\n" "Generated-By: Babel 2.6.0\n"
msgid "Project name" msgid "Project name"
msgstr "Nom de projet" msgstr "Nom de projet"
@ -210,6 +210,12 @@ msgstr "La facture a été supprimée"
msgid "The bill has been modified" msgid "The bill has been modified"
msgstr "La facture a été modifiée" msgstr "La facture a été modifiée"
msgid "Sorry, we were unable to find the page you've asked for."
msgstr "Navré, nous ne trouvons pas la page que vous avez demandé."
msgid "The best thing to do is probably to get back to the main page."
msgstr "Votre meilleure piste est probablement la page d'accueil."
msgid "Back to the list" msgid "Back to the list"
msgstr "Retourner à la liste" msgstr "Retourner à la liste"
@ -488,4 +494,3 @@ msgstr "Solde"
#~ msgid "Invite" #~ msgid "Invite"
#~ msgstr "Invitez" #~ msgstr "Invitez"

View file

@ -9,9 +9,10 @@ some shortcuts to make your life better when coding (see `pull_project`
and `add_project_id` for a quick overview) and `add_project_id` for a quick overview)
""" """
import os
from flask import ( from flask import (
Blueprint, current_app, flash, g, redirect, render_template, request, Blueprint, current_app, flash, g, redirect, render_template, request,
session, url_for, send_file session, url_for, send_file, send_from_directory
) )
from flask_mail import Message from flask_mail import Message
from flask_babel import get_locale, gettext as _ from flask_babel import get_locale, gettext as _
@ -513,7 +514,7 @@ def delete_bill(bill_id):
# fixme: everyone is able to delete a bill # fixme: everyone is able to delete a bill
bill = Bill.query.get(g.project, bill_id) bill = Bill.query.get(g.project, bill_id)
if not bill: if not bill:
raise NotFound() return redirect(url_for('.list_bills'))
db.session.delete(bill) db.session.delete(bill)
db.session.commit() db.session.commit()
@ -582,3 +583,9 @@ def dashboard():
projects=Project.query.all(), projects=Project.query.all(),
is_admin_dashboard_activated=is_admin_dashboard_activated is_admin_dashboard_activated=is_admin_dashboard_activated
) )
@main.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(main.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')

View file

@ -7,7 +7,7 @@ Flask-script
flask-babel flask-babel
flask-restful>=0.3.6 flask-restful>=0.3.6
jinja2>=2.6 jinja2>=2.6
raven
blinker blinker
six>=1.10 six>=1.10
itsdangerous>=0.24 itsdangerous>=0.24
email_validator>=1.0