mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-05 20:51:49 +02:00
Merge branch 'master' into almet/fix-members-order
This commit is contained in:
commit
d6fbae2950
21 changed files with 221 additions and 102 deletions
|
@ -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
|
||||||
=====
|
=====
|
||||||
|
|
43
Makefile
43
Makefile
|
@ -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}'
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
BIN
ihatemoney/static/favicon.ico
Normal file
BIN
ihatemoney/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
13
ihatemoney/templates/404.html
Normal file
13
ihatemoney/templates/404.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Binary file not shown.
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue