diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c42bf6d6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + time: "04:00" + open-pull-requests-limit: 10 + target-branch: master + allow: + - dependency-type: direct + - dependency-type: indirect + ignore: + - dependency-name: sphinx + versions: + - 3.5.0 diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml new file mode 100644 index 00000000..0efdffd9 --- /dev/null +++ b/.github/workflows/test-docs.yml @@ -0,0 +1,76 @@ +name: Test & Docs + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + # Use postgresql and MariaDB versions of Debian buster + services: + postgres: + image: postgres:11 + ports: + - 5432:5432 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ihatemoney + POSTGRES_DB: ihatemoney_ci + options: + --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + mariadb: + image: mariadb:10.3 + env: + MARIADB_ROOT_PASSWORD: ihatemoney + MARIADB_DATABASE: ihatemoney_ci + options: >- + --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + ports: + - 3306:3306 + + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + database: [sqlite] + # Test other databases only with one version of Python (Debian buster has 3.7) + include: + - python-version: 3.7 + database: postgresql + - python-version: 3.7 + database: mariadb + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + # Run tox using the version of Python in `PATH` + - name: Run Tox with sqlite + run: tox -e py + if: matrix.database == 'sqlite' + env: + TESTING_SQLALCHEMY_DATABASE_URI: 'sqlite:///budget.db' + - name: Run Tox with postgresql + run: tox -e py + if: matrix.database == 'postgresql' + env: + TESTING_SQLALCHEMY_DATABASE_URI: 'postgresql+psycopg2://postgres:ihatemoney@localhost:5432/ihatemoney_ci' + - name: Run Tox with mariadb + run: tox -e py + if: matrix.database == 'mariadb' + env: + TESTING_SQLALCHEMY_DATABASE_URI: 'mysql+pymysql://root:ihatemoney@localhost:3306/ihatemoney_ci' + - name: Run Lint & Docs + run: tox -e lint_docs + if: matrix.python-version == '3.8' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67f5fab4..5d07998a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,8 @@ Breaking changes - Drop support for Python 2 (#483) - Drop support for Python 3.5 (#571) +- Drop support for MySQL (#743) +- Require MariaDB version 10.3.2 or above (#632) The minimum supported version is now Python 3.6 diff --git a/Makefile b/Makefile index 30de0dd7..2e8c18ff 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ update: remove-install-stamp install ## Update the dependencies .PHONY: serve serve: install ## Run the ihatemoney server @echo 'Running ihatemoney on http://localhost:5000' - $(PYTHON) -m ihatemoney.manage runserver + $(PYTHON) -m ihatemoney.manage run .PHONY: test test: install-dev ## Run the tests diff --git a/README.rst b/README.rst index cab19292..e3e20e4e 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ I hate money ############ -.. image:: https://travis-ci.org/spiral-project/ihatemoney.svg?branch=master - :target: https://travis-ci.org/spiral-project/ihatemoney - :alt: Travis CI Build Status +.. image:: https://github.com/spiral-project/ihatemoney/actions/workflows/test-docs.yml/badge.svg + :target: https://github.com/spiral-project/ihatemoney/actions/workflows/test-docs.yml + :alt: GitHub Actions Status .. image:: https://hosted.weblate.org/widgets/i-hate-money/-/i-hate-money/svg-badge.svg :target: https://hosted.weblate.org/engage/i-hate-money/?utm_source=widget @@ -30,7 +30,7 @@ Requirements ============ * **Python**: version 3.6 to 3.9. -* **Backends**: MySQL, PostgreSQL, SQLite, Memory. +* **Backends**: SQLite, PostgreSQL, MariaDB (version 10.3.2 or above), Memory. Contributing ============ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..8ef34f23 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 5.0.x | :heavy_check_mark: | +| 4.1.x | :heavy_check_mark: | +| <= 4.0 | :x: | + +## Reporting a Vulnerability + +In order to report a vulnerability, you can either join the IRC channel `#ihatemoney` on libera.chat and ping active users available for a private message, +or write an email to bugs-ihatemoney “@” antipoul.fr This email address is an alias, so you can expect an answer from another address. diff --git a/docs/configuration.rst b/docs/configuration.rst index 7e29da1c..a1e34bed 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -24,7 +24,11 @@ format used can be found on `the SQLAlchemy documentation`_. ``sqlite:///home/ihatemoney/ihatemoney.db``. Do *not* store it under ``/tmp`` as this folder is cleared at each boot. -If you're using PostgreSQL, Your client must use utf8. Unfortunately, +For example, if you're using MariaDB, use a configuration similar to the following:: + + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://user:pass@localhost/dbname' + +If you're using PostgreSQL, your client must use utf8. Unfortunately, PostgreSQL default is to use ASCII. Either change your client settings, or specify the encoding by appending ``?client_encoding=utf8`` to the connection string. This will look like:: diff --git a/docs/contributing.rst b/docs/contributing.rst index 7b4f8a55..80174b76 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -195,7 +195,7 @@ its source is located inside the `docs folder Install doc dependencies (within the virtual environment, if any):: - pip install -r docs/requirements.txt + pip install -e .[doc] And to produce a HTML doc in the `docs/_output` folder:: diff --git a/docs/index.rst b/docs/index.rst index 3b55c35c..cfbb31af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ It keeps track of who bought what, when, and for whom; and helps to settle the bills. I hate money is written in python, using the `flask `_ -framework. It's developped with ease of use in mind, and is trying to +framework. It's developed with ease of use in mind, and is trying to keep things simple. Hope you (will) like it! Table of contents diff --git a/docs/installation.rst b/docs/installation.rst index 82ac12f2..74ca8335 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -20,7 +20,7 @@ Requirements «Ihatemoney» depends on: * **Python**: version 3.6 to 3.9 included will work. -* **A Backend**: to choose among MySQL, PostgreSQL, SQLite or Memory. +* **A Backend**: to choose among SQLite, PostgreSQL, MariaDB (>= 10.3.2) or Memory. * **Virtual environment** (recommended): `python3-venv` package under Debian/Ubuntu. We recommend to use `virtual environment `_ but @@ -61,14 +61,14 @@ Test it Once installed, you can start a test server:: - ihatemoney runserver + ihatemoney run And point your browser at `http://localhost:5000 `_. -Configure database with MySQL/MariaDB (optional) +Configure database with MariaDB (optional) ================================================ -.. note:: Only required if you use MySQL/MariaDB. +.. note:: Only required if you use MariaDB. Make sure to use MariaDB 10.3.2 or newer. 1. Install PyMySQL dependencies. On Debian or Ubuntu, that would be:: @@ -184,7 +184,7 @@ Install Gunicorn:: Obviously, adapt the ``ExecStart`` path for your installation folder. If you use SQLite as database: remove mentions of ``postgresql.service`` in ``ihatemoney.service``. - If you use MySQL or MariaDB as database: replace mentions of ``postgresql.service`` by ``mysql.service`` or ``mariadb.service`` in ``ihatemoney.service``. + If you use MariaDB as database: replace mentions of ``postgresql.service`` by ``mariadb.service`` in ``ihatemoney.service``. Then reload systemd, enable and start ``ihatemoney``:: diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index b207c098..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Sphinx==3.5.3 -docutils==0.17 diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 0db0428b..ec846324 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -57,7 +57,7 @@ If you were running IHateMoney using Python < 3.6, you must, **before** upgradin or several of the following deployment options : - Gunicorn (Nginx) - - MySQL + - MariaDB - PostgreSQL If so, pick the ``pip`` commands to use in the relevant section(s) of @@ -65,6 +65,30 @@ If so, pick the ``pip`` commands to use in the relevant section(s) of Then follow :ref:`general-procedure` from step 1. in order to complete the update. +Switch to MariaDB >= 10.3.2 instead of MySQL +++++++++++++++++++++++++++++++++++++++++++++ + +.. note:: If you are using SQLite or PostgreSQL, you can skip this section, no + special action is required. + +If you were running IHateMoney with MySQL, you must switch to MariaDB. +MySQL is no longer a supported database option. + +In addition, the minimum supported version of MariaDB is 10.3.2. +See `this MySQL / MariaDB issue `_ +for details. + +To upgrade: + +1. Ensure you have a MariaDB server installed and configured, and that its + version is at least 10.3.2. + +2. Copy your database from MySQL to MariaDB. + +3. Ensure that IHateMoney is correctly configured to use your MariaDB database, + see :ref:`configuration`. + +Then follow :ref:`general-procedure` from step 1. in order to complete the update. 2.x → 3.x --------- diff --git a/ihatemoney/api/common.py b/ihatemoney/api/common.py index cd247cdf..ede76e46 100644 --- a/ihatemoney/api/common.py +++ b/ihatemoney/api/common.py @@ -69,7 +69,7 @@ class ProjectHandler(Resource): return "DELETED" def put(self, project): - form = EditProjectForm(meta={"csrf": False}) + form = EditProjectForm(id=project.id, meta={"csrf": False}) if form.validate() and current_app.config.get("ALLOW_PUBLIC_PROJECT_CREATION"): form.update(project) db.session.commit() diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index e4a32d09..0029e151 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -1,12 +1,13 @@ from datetime import datetime from re import match +from types import SimpleNamespace import email_validator from flask import request from flask_babel import lazy_gettext as _ from flask_wtf.file import FileAllowed, FileField, FileRequired from flask_wtf.form import FlaskForm -from jinja2 import Markup +from markupsafe import Markup from werkzeug.security import check_password_hash, generate_password_hash from wtforms.fields.core import Label, SelectField, SelectMultipleField from wtforms.fields.html5 import DateField, DecimalField, URLField @@ -44,7 +45,7 @@ def get_billform_for(project, set_default=True, **kwargs): """ form = BillForm(**kwargs) - if form.original_currency.data == "None": + if form.original_currency.data is None: form.original_currency.data = project.default_currency show_no_currency = form.original_currency.data == CurrencyConverter.no_currency @@ -102,7 +103,11 @@ class CalculatorStringField(StringField): class EditProjectForm(FlaskForm): name = StringField(_("Project name"), validators=[DataRequired()]) - password = StringField(_("Private code"), validators=[DataRequired()]) + # If empty -> don't change the password + password = PasswordField( + _("New private code"), + description=_("Enter a new code if you want to change it"), + ) contact_email = StringField(_("Email"), validators=[DataRequired(), Email()]) project_history = BooleanField(_("Enable project history")) ip_recording = BooleanField(_("Use IP tracking for project history")) @@ -110,6 +115,14 @@ class EditProjectForm(FlaskForm): default_currency = SelectField(_("Default Currency"), validators=[DataRequired()]) def __init__(self, *args, **kwargs): + if not hasattr(self, "id"): + # We must access the project to validate the default currency, using its id. + # In ProjectForm, 'id' is provided, but not in this base class, so it *must* + # be provided by callers. + # Since id can be defined as a WTForms.StringField, we mimics it, + # using an object that can have a 'data' attribute. + # It defaults to empty string to ensure that query run smoothly. + self.id = SimpleNamespace(data=kwargs.pop("id", "")) super().__init__(*args, **kwargs) self.default_currency.choices = [ (currency_name, render_localized_currency(currency_name, detailed=True)) @@ -127,32 +140,36 @@ class EditProjectForm(FlaskForm): else: return LoggingMode.ENABLED - def save(self): - """Create a new project with the information given by this form. - - Returns the created instance - """ - project = Project( - name=self.name.data, - id=self.id.data, - password=generate_password_hash(self.password.data), - contact_email=self.contact_email.data, - logging_preference=self.logging_preference, - default_currency=self.default_currency.data, - ) - return project + def validate_default_currency(form, field): + project = Project.query.get(form.id.data) + if ( + project is not None + and field.data == CurrencyConverter.no_currency + and project.has_multiple_currencies() + ): + raise ValidationError( + _( + "This project cannot be set to 'no currency'" + " because it contains bills in multiple currencies." + ) + ) def update(self, project): """Update the project with the information from the form""" project.name = self.name.data - # Only update password if changed to prevent spurious log entries - if not check_password_hash(project.password, self.password.data): + if ( + # Only update password if a new one is provided + self.password.data + # Only update password if different from the previous one, + # to prevent spurious log entries + and not check_password_hash(project.password, self.password.data) + ): project.password = generate_password_hash(self.password.data) project.contact_email = self.contact_email.data project.logging_preference = self.logging_preference - project.default_currency = self.default_currency.data + project.switch_currency(self.default_currency.data) return project @@ -168,16 +185,30 @@ class UploadForm(FlaskForm): class ProjectForm(EditProjectForm): id = StringField(_("Project identifier"), validators=[DataRequired()]) + # This field overrides the one from EditProjectForm password = PasswordField(_("Private code"), validators=[DataRequired()]) submit = SubmitField(_("Create the project")) def save(self): + """Create a new project with the information given by this form. + + Returns the created instance + """ # WTForms Boolean Fields don't insert the default value when the # request doesn't include any value the way that other fields do, # so we'll manually do it here self.project_history.data = LoggingMode.default() != LoggingMode.DISABLED self.ip_recording.data = LoggingMode.default() == LoggingMode.RECORD_IP - return super().save() + # Create project + project = Project( + name=self.name.data, + id=self.id.data, + password=generate_password_hash(self.password.data), + contact_email=self.contact_email.data, + logging_preference=self.logging_preference, + default_currency=self.default_currency.data, + ) + return project def validate_id(form, field): form.id.data = slugify(field.data) diff --git a/ihatemoney/manage.py b/ihatemoney/manage.py index eb1e24c2..805a07f3 100755 --- a/ihatemoney/manage.py +++ b/ihatemoney/manage.py @@ -5,8 +5,8 @@ import os import random import sys -from flask_migrate import Migrate, MigrateCommand -from flask_script import Command, Manager, Option +import click +from flask.cli import FlaskGroup from werkzeug.security import generate_password_hash from ihatemoney.models import Project, db @@ -14,31 +14,48 @@ from ihatemoney.run import create_app from ihatemoney.utils import create_jinja_env -class GeneratePasswordHash(Command): +@click.group(cls=FlaskGroup, create_app=create_app) +def cli(): + """IHateMoney Management script""" + +@cli.command( + context_settings={"ignore_unknown_options": True, "allow_extra_args": True} +) +@click.pass_context +def runserver(ctx): + """Deprecated, use the "run" command instead""" + click.secho( + '"runserver" is deprecated, please use the standard "run" flask command', + fg="red", + ) + run = cli.get_command(ctx, "run") + ctx.forward(run) + + +@click.command(name="generate_password_hash") +def password_hash(): """Get password from user and hash it without printing it in clear text.""" - - def run(self): - password = getpass.getpass(prompt="Password: ") - print(generate_password_hash(password)) + password = getpass.getpass(prompt="Password: ") + print(generate_password_hash(password)) -class GenerateConfig(Command): - def get_options(self): - return [ - Option( - "config_file", - choices=[ - "ihatemoney.cfg", - "apache-vhost.conf", - "gunicorn.conf.py", - "supervisord.conf", - "nginx.conf", - ], - ) +@click.command() +@click.argument( + "config_file", + type=click.Choice( + [ + "ihatemoney.cfg", + "apache-vhost.conf", + "gunicorn.conf.py", + "supervisord.conf", + "nginx.conf", ] + ), +) +def generate_config(config_file): + """Generate front-end server configuration""" - @staticmethod def gen_secret_key(): return "".join( [ @@ -49,59 +66,33 @@ class GenerateConfig(Command): ] ) - def run(self, config_file): - env = create_jinja_env("conf-templates", strict_rendering=True) - template = env.get_template(f"{config_file}.j2") + env = create_jinja_env("conf-templates", strict_rendering=True) + template = env.get_template(f"{config_file}.j2") - bin_path = os.path.dirname(sys.executable) - pkg_path = os.path.abspath(os.path.dirname(__file__)) + bin_path = os.path.dirname(sys.executable) + pkg_path = os.path.abspath(os.path.dirname(__file__)) - print( - template.render( - pkg_path=pkg_path, - bin_path=bin_path, - sys_prefix=sys.prefix, - secret_key=self.gen_secret_key(), - ) + print( + template.render( + pkg_path=pkg_path, + bin_path=bin_path, + sys_prefix=sys.prefix, + secret_key=gen_secret_key(), ) + ) -class DeleteProject(Command): - def run(self, project_name): - demo_project = Project.query.get(project_name) - db.session.delete(demo_project) +@cli.command() +@click.argument("project_name") +def delete_project(project_name): + """Delete a project""" + project = Project.query.get(project_name) + if project is None: + click.secho(f'Project "{project_name}" not found', fg="red") + else: + db.session.delete(project) db.session.commit() -def main(): - QUIET_COMMANDS = ("generate_password_hash", "generate-config") - - exception = None - backup_stderr = sys.stderr - # Hack to divert stderr for commands generating content to stdout - # to avoid confusing the user - if len(sys.argv) > 1 and sys.argv[1] in QUIET_COMMANDS: - sys.stderr = open(os.devnull, "w") - - try: - app = create_app() - Migrate(app, db) - except Exception as e: - exception = e - - # Restore stderr - sys.stderr = backup_stderr - - if exception: - raise exception - - manager = Manager(app) - 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() - - if __name__ == "__main__": - main() + cli() diff --git a/ihatemoney/messages.pot b/ihatemoney/messages.pot index 27206f83..81c79a2d 100644 --- a/ihatemoney/messages.pot +++ b/ihatemoney/messages.pot @@ -6,7 +6,7 @@ msgstr "" msgid "Project name" msgstr "" -msgid "Private code" +msgid "New private code" msgstr "" msgid "Email" @@ -21,6 +21,11 @@ msgstr "" msgid "Default Currency" msgstr "" +msgid "" +"This project cannot be set to 'no currency' because it contains bills in " +"multiple currencies." +msgstr "" + msgid "Import previously exported JSON file" msgstr "" @@ -30,6 +35,9 @@ msgstr "" msgid "Project identifier" msgstr "" +msgid "Private code" +msgstr "" + msgid "Create the project" msgstr "" @@ -291,6 +299,12 @@ msgstr "" msgid "The Dashboard is currently deactivated." msgstr "" +msgid "Download Mobile Application" +msgstr "" + +msgid "Get it on" +msgstr "" + msgid "you sure?" msgstr "" @@ -573,12 +587,6 @@ msgstr "" msgid "Statistics" msgstr "" -msgid "History" -msgstr "" - -msgid "Settings" -msgstr "" - msgid "Languages" msgstr "" @@ -588,6 +596,12 @@ msgstr "" msgid "Start a new project" msgstr "" +msgid "History" +msgstr "" + +msgid "Settings" +msgstr "" + msgid "Other projects :" msgstr "" diff --git a/ihatemoney/models.py b/ihatemoney/models.py index 7984ab76..04415fa6 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -17,6 +17,7 @@ from sqlalchemy_continuum import make_versioned, version_class from sqlalchemy_continuum.plugins import FlaskPlugin from werkzeug.security import generate_password_hash +from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.patch_sqlalchemy_continuum import PatchedBuilder from ihatemoney.versioning import ( ConditionalVersioningManager, @@ -139,7 +140,7 @@ class Project(db.Model): "spent": sum( [ bill.pay_each() * member.weight - for bill in self.get_bills().all() + for bill in self.get_bills_unordered().all() if member in bill.owers ] ), @@ -156,7 +157,7 @@ class Project(db.Model): :rtype dict: """ monthly = defaultdict(lambda: defaultdict(float)) - for bill in self.get_bills().all(): + for bill in self.get_bills_unordered().all(): monthly[bill.date.year][bill.date.month] += bill.converted_amount return monthly @@ -215,15 +216,31 @@ class Project(db.Model): def has_bills(self): """return if the project do have bills or not""" - return self.get_bills().count() > 0 + return self.get_bills_unordered().count() > 0 - def get_bills(self): - """Return the list of bills related to this project""" + def has_multiple_currencies(self): + """Returns True if multiple currencies are used""" + # It would be more efficient to do the counting in the database, + # but this is called very rarely so we can tolerate if it's a bit + # slow. And doing this in Python is much more readable, see #784. + nb_currencies = len( + set(bill.original_currency for bill in self.get_bills_unordered()) + ) + return nb_currencies > 1 + + def get_bills_unordered(self): + """Base query for bill list""" return ( Bill.query.join(Person, Project) .filter(Bill.payer_id == Person.id) .filter(Person.project_id == Project.id) .filter(Project.id == self.id) + ) + + def get_bills(self): + """Return the list of bills related to this project""" + return ( + self.get_bills_unordered() .order_by(Bill.date.desc()) .order_by(Bill.creation_date.desc()) .order_by(Bill.id.desc()) @@ -232,11 +249,8 @@ class Project(db.Model): def get_member_bills(self, member_id): """Return the list of bills related to a specific member""" return ( - Bill.query.join(Person, Project) - .filter(Bill.payer_id == Person.id) - .filter(Person.project_id == Project.id) + self.get_bills_unordered() .filter(Person.id == member_id) - .filter(Project.id == self.id) .order_by(Bill.date.desc()) .order_by(Bill.id.desc()) ) @@ -263,6 +277,41 @@ class Project(db.Model): ) return pretty_bills + def switch_currency(self, new_currency): + if new_currency == self.default_currency: + return + # Update converted currency + if new_currency == CurrencyConverter.no_currency: + if self.has_multiple_currencies(): + raise ValueError(f"Can't unset currency of project {self.id}") + + for bill in self.get_bills_unordered(): + # We are removing the currency, and we already checked that all bills + # had the same currency: it means that we can simply strip the currency + # without converting the amounts. We basically ignore the current default_currency + + # Reset converted amount in case it was different from the original amount + bill.converted_amount = bill.amount + # Strip currency + bill.original_currency = CurrencyConverter.no_currency + db.session.add(bill) + else: + for bill in self.get_bills_unordered(): + if bill.original_currency == CurrencyConverter.no_currency: + # Bills that were created without currency will be set to the new currency + bill.original_currency = new_currency + bill.converted_amount = bill.amount + else: + # Convert amount for others, without touching original_currency + bill.converted_amount = CurrencyConverter().exchange_currency( + bill.amount, bill.original_currency, new_currency + ) + db.session.add(bill) + + self.default_currency = new_currency + db.session.add(self) + db.session.commit() + def remove_member(self, member_id): """Remove a member from the project. diff --git a/ihatemoney/run.py b/ihatemoney/run.py index 89020873..43ff52a5 100644 --- a/ihatemoney/run.py +++ b/ihatemoney/run.py @@ -7,7 +7,7 @@ from flask import Flask, g, render_template, request, session from flask_babel import Babel, format_currency from flask_mail import Mail from flask_migrate import Migrate, stamp, upgrade -from jinja2 import contextfilter +from jinja2 import pass_context from werkzeug.middleware.proxy_fix import ProxyFix from ihatemoney import default_settings @@ -160,7 +160,7 @@ def create_app( # Undocumented currencyformat filter from flask_babel is forwarding to Babel format_currency # We overwrite it to remove the currency sign ¤ when there is no currency - @contextfilter + @pass_context def currency(context, number, currency=None, *args, **kwargs): if currency is None: currency = context.get("g").project.default_currency diff --git a/ihatemoney/templates/dashboard.html b/ihatemoney/templates/dashboard.html index d9c150c4..3e26441a 100644 --- a/ihatemoney/templates/dashboard.html +++ b/ihatemoney/templates/dashboard.html @@ -5,7 +5,7 @@ {{ _("Project") }}{{ _("Number of members") }}{{ _("Number of bills") }}{{_("Newest bill")}}{{_("Oldest bill")}}{{_("Actions")}} {% for project in projects|sort(attribute='name') %} - {{ project.name }}{{ project.members | count }}{{ project.get_bills().count() }} + {{ project.name }}{{ project.members | count }}{{ project.get_bills_unordered().count() }} {% if project.has_bills() %} {{ project.get_bills().all()[0].date }} {{ project.get_bills().all()[-1].date }} diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html index 82b960e3..afc364f9 100644 --- a/ihatemoney/templates/forms.html +++ b/ihatemoney/templates/forms.html @@ -2,7 +2,7 @@
{% if field.type != "SubmitField" %} {% if inline %} - {{ field.label(class="col-3") }} + {{ field.label(class="col-3 mt-2") }} {% else %} {{ field.label() }} {% endif %} @@ -120,31 +120,38 @@ {% if title %}{% if edit %}{{ _("Edit this bill") }} {% else %}{{ _("Add a bill") }} {% endif %}{% endif %} {% include "display_errors.html" %} {{ form.hidden_tag() }} - {{ input(form.date, class="form-control", inline=True) }} + {{ input(form.date, inline=True) }} {{ input(form.what, inline=True) }} {{ input(form.payer, inline=True, class="form-control custom-select") }} {{ input(form.amount, inline=True) }} - {% if g.project.default_currency != "XXX" %} - {{ input(form.original_currency, inline=True) }} - {% endif %} - {{ input(form.external_link, inline=True) }}
-
- + {% endfor %} +
+ +
+ {{ _("More options") }} + {% if g.project.default_currency != "XXX" %} + {{ input(form.original_currency, inline=True, class="form-control custom-select") }} + {% endif %} + {{ input(form.external_link, inline=True) }} +
{{ form.submit(class="btn btn-primary") }} diff --git a/ihatemoney/templates/layout.html b/ihatemoney/templates/layout.html index f79270e4..cae707c2 100644 --- a/ihatemoney/templates/layout.html +++ b/ihatemoney/templates/layout.html @@ -42,14 +42,12 @@

#! money?