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 validation of the hashed password (#310)
|
||||
- Fix infinite loop that happened when accessing / (#358)
|
||||
- Fix email validation when sending invites
|
||||
|
||||
Added
|
||||
=====
|
||||
|
|
43
Makefile
43
Makefile
|
@ -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}'
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
--------------------------------
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
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 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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
Binary file not shown.
|
@ -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"
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue