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 validation of the hashed password (#310)
- Fix infinite loop that happened when accessing / (#358)
- Fix email validation when sending invites
Added
=====

View file

@ -7,52 +7,71 @@ DOC_STAMP = $(VENV)/.doc_env_installed.stamp
INSTALL_STAMP = $(VENV)/.install.stamp
TEMPDIR := $(shell mktemp -d)
all: install
install: virtualenv $(INSTALL_STAMP)
.PHONY: all
all: install ## Alias for install
.PHONY: install
install: virtualenv $(INSTALL_STAMP) ## Install dependencies
$(INSTALL_STAMP):
$(VENV)/bin/pip install -U pip
$(VENV)/bin/pip install -r requirements.txt
touch $(INSTALL_STAMP)
.PHONY: virtualenv
virtualenv: $(PYTHON)
$(PYTHON):
$(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
$(VENV)/bin/pip install -Ur dev-requirements.txt
touch $(DEV_STAMP)
.PHONY: remove-install-stamp
remove-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
test: $(DEV_STAMP)
.PHONY: test
test: $(DEV_STAMP) ## Run the tests
$(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
build-translations:
.PHONY: build-translations
build-translations: ## Build the 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 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; \
$(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)
$(TEMPDIR)/bin/pip install -U pip
$(TEMPDIR)/bin/pip install -Ue "."
$(TEMPDIR)/bin/pip freeze | grep -v -- '-e' > requirements.txt
clean:
.PHONY: clean
clean: ## Destroy the virtual environment
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.
* **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_DEFAULT_SENDER = "$MAIL_DEFAULT_SENDER"
ACTIVATE_DEMO_PROJECT = $ACTIVATE_DEMO_PROJECT
ADMIN_PASSWORD = "$ADMIN_PASSWORD"
ADMIN_PASSWORD = '$ADMIN_PASSWORD'
ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION
ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD
EOF

View file

@ -189,52 +189,98 @@ deployed instance, simply look at your *ihatemoney.cfg*.
Production values are recommended values for use in production.
`SQLALCHEMY_DATABASE_URI`
-------------------------
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| Setting name | Default | What does it do? |
+===============================+=================================+========================================================================================+
| SQLALCHEMY_DATABASE_URI | ``sqlite:///tmp/ihatemoney.db`` | Specifies the type of backend to use and its location. More information on the |
| | | 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 ``/tmp`` as this |
| | | folder is cleared at each boot. |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| SECRET_KEY | ``tralala`` | The secret key used to encrypt the cookies. |
| | | |
| | | **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. |
| | | |
| | | **Production value:** Any tuple you want. |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| ACTIVATE_DEMO_PROJECT | ``True`` | If set to `True`, a demo project will be available on the frontpage. |
| | | |
| | | **Production value:** Usually, you will want to set it to ``False`` for a private |
| | | instance. |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| | | Hashed password to access protected endpoints. If left empty, all administrative |
| ADMIN_PASSWORD | ``""`` | tasks are disabled. |
| | | |
| | | **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 | ``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 |
| | | settings). |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| ACTIVATE_ADMIN_DASHBOARD | ``False`` | 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 |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
| APPLICATION_ROOT | ``""`` | If empty, ihatemoney will be served at domain root (e.g: *http://domain.tld*), if set |
| | | to ``"foo"``, it will be served from a "folder" (e.g: *http://domain.tld/foo*) |
+-------------------------------+---------------------------------+----------------------------------------------------------------------------------------+
Specifies the type of backend to use and its location. More information on the
format used can be found on `the SQLAlchemy documentation`_.
- **default value:** ``sqlite:///tmp/ihatemoney.db``
- **Production value:** Set it to some path on your disk. Typically
``sqlite:///home/ihatemoney/ihatemoney.db``. Do *not* store it under
``/tmp`` as this folder is cleared at each boot.
`SECRET_KEY`
------------
The secret key used to encrypt the cookies.
- **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to
something random, which is good.
`MAIL_DEFAULT_SENDER`
---------------------
A python tuple describing the name and email address to use when sending emails.
- **Default value:** ``("Budget manager", "budget@notmyidea.org")``
- **Production value:** Any tuple you want.
`ACTIVATE_DEMO_PROJECT`
-----------------------
If set to `True`, a demo project will be available on the frontpage.
- **Default value:** ``True``
- **Production value:** Usually, you will want to set it to ``False`` for a
private instance.
`ADMIN_PASSWORD`
----------------
Hashed password to access protected endpoints. If left empty, all administrative
tasks are disabled.
- **Default value:** ``""`` (empty string)
- **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
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
--------------------------------

View file

@ -10,6 +10,8 @@ from werkzeug.security import generate_password_hash
from datetime import datetime
from jinja2 import Markup
import email_validator
from ihatemoney.models import Project, Person
from ihatemoney.utils import slugify
@ -184,9 +186,10 @@ class InviteForm(FlaskForm):
submit = SubmitField(_("Send invites"))
def validate_emails(form, field):
validator = Email()
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",
email=email))

View file

@ -10,7 +10,7 @@ from flask_migrate import Migrate, MigrateCommand
from werkzeug.security import generate_password_hash
from ihatemoney.run import create_app
from ihatemoney.models import db
from ihatemoney.models import db, Project
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():
QUIET_COMMANDS = ('generate_password_hash', 'generate-config')
@ -76,6 +83,7 @@ def main():
manager.add_command('db', MigrateCommand)
manager.add_command('generate_password_hash', GeneratePasswordHash)
manager.add_command('generate-config', GenerateConfig)
manager.add_command('delete-project', DeleteProject)
manager.run()

View file

@ -182,6 +182,12 @@ msgstr ""
msgid "The bill has been modified"
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"
msgstr ""

View file

@ -2,11 +2,10 @@ import os
import os.path
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_mail import Mail
from flask_migrate import Migrate, upgrade, stamp
from raven.contrib.flask import Sentry
from werkzeug.contrib.fixers import ProxyFix
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',
instance_relative_config=True):
app = Flask(
@ -122,17 +125,15 @@ def create_app(configuration=None, instance_path='/etc/ihatemoney',
validate_configuration(app)
app.register_blueprint(web_interface)
app.register_blueprint(api)
app.register_error_handler(404, page_not_found)
# Configure the application
# Configure the a, root="main"pplication
setup_database(app)
mail = Mail()
mail.init_app(app)
app.mail = mail
# Error reporting
Sentry(app)
# Jinja filters
app.jinja_env.filters['minimal_round'] = minimal_round

View file

@ -224,6 +224,10 @@ tr.payer_line .balance-name{
padding: 0 0 0 20px;
}
.action button:hover {
text-decoration: underline;
}
.delete button, .delete button:hover {
background: url('../images/deleter.png') left no-repeat;
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
function selectall()
{
var els = document.getElementsByName('payed_for');
for(var i =0;i<els.length;i++)
{
els[i].checked=true;
}
}
function selectnone()
{
var els = document.getElementsByName('payed_for');
for(var i =0;i<els.length;i++)
{
els[i].checked=false;
}
}
// Utility to select all or none of the checkboxes in the add_bill form.
function selectCheckboxes(value){
var els = document.getElementsByName('payed_for');
for(var i = 0; i < els.length; i++){
els[i].checked = value;
}
}

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 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 %}

View file

@ -100,7 +100,7 @@
<label class="col-3" for="payed_for">{{ _("For whom?") }}</label>
<div class="controls col-9">
<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') %}
<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 %}

View file

@ -32,13 +32,13 @@
<span class="navbar-toggler-icon"></span>
</button>
<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">
{% if g.project %}
{% 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 == 'settle_bill' %} active{% endif %}"><a class="nav-link" href="{{ url_for(".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 == '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("main.settle_bill") }}">{{ _("Settle") }}</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 %}
{% endif %}
</ul>
@ -47,23 +47,23 @@
<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>
<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>
{% for id, name in session['projects'] %}
{% 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 %}
{% 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><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>
</li>
{% 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 == "en" %} active{% endif %}"><a class="nav-link" href="{{ url_for(".change_lang", lang="en") }}">en</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("main.change_lang", lang="en") }}">en</a></li>
{% 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 %}
</ul>
</div>

View file

@ -20,7 +20,8 @@ from flask import session
from flask_testing import TestCase
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 utils
@ -1472,6 +1473,15 @@ class CommandTestCase(BaseTestCase):
print(stdout.getvalue())
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__":
unittest.main()

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\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"
"Last-Translator: Adrien CLERC <>\n"
"Language: fr\n"
@ -16,7 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.5.3\n"
"Generated-By: Babel 2.6.0\n"
msgid "Project name"
msgstr "Nom de projet"
@ -210,6 +210,12 @@ msgstr "La facture a été supprimée"
msgid "The bill has been modified"
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"
msgstr "Retourner à la liste"
@ -488,4 +494,3 @@ msgstr "Solde"
#~ msgid "Invite"
#~ 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)
"""
import os
from flask import (
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_babel import get_locale, gettext as _
@ -513,7 +514,7 @@ def delete_bill(bill_id):
# fixme: everyone is able to delete a bill
bill = Bill.query.get(g.project, bill_id)
if not bill:
raise NotFound()
return redirect(url_for('.list_bills'))
db.session.delete(bill)
db.session.commit()
@ -582,3 +583,9 @@ def dashboard():
projects=Project.query.all(),
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-restful>=0.3.6
jinja2>=2.6
raven
blinker
six>=1.10
itsdangerous>=0.24
email_validator>=1.0