mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-05 20:51:49 +02:00
Merge branch 'master' into Glandos-patch-1
This commit is contained in:
commit
cea50022c2
46 changed files with 3140 additions and 381 deletions
52
.github/workflows/dockerhub.yml
vendored
Normal file
52
.github/workflows/dockerhub.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
name: CI to Docker Hub
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPOSITORY }}
|
||||||
|
|
||||||
|
- uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./Dockerfile
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
push: true
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
@ -9,11 +9,12 @@ This document describes changes between each past release.
|
||||||
Breaking changes
|
Breaking changes
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
- Include project code into project authentication token. This invalidates all existing API tokens and invitation links from previous versions (#802)
|
||||||
- Drop support for Python 2 (#483)
|
- Drop support for Python 2 (#483)
|
||||||
- Drop support for Python 3.5 (#571)
|
- Drop support for Python 3.5 (#571)
|
||||||
- Drop support for MySQL (#743)
|
- Drop support for MySQL (#743)
|
||||||
- Require MariaDB version 10.3.2 or above (#632)
|
- Require MariaDB version 10.3.2 or above (#632)
|
||||||
- Authentication page URL needs a `project_id` parameter (#802)
|
- Enable session cookie security by default (#845)
|
||||||
|
|
||||||
The minimum supported version is now Python 3.6, and the project is tested
|
The minimum supported version is now Python 3.6, and the project is tested
|
||||||
with up to Python 3.9
|
with up to Python 3.9
|
||||||
|
@ -26,6 +27,7 @@ Security
|
||||||
|
|
||||||
- Add CSRF validation on destructive actions (#796)
|
- Add CSRF validation on destructive actions (#796)
|
||||||
- Ask for private code to delete project or project history (#796)
|
- Ask for private code to delete project or project history (#796)
|
||||||
|
- Add headers to mitigate Clickjacking, XSS, and other attacks: `X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`, `Content-Security-Policy`, `Referrer-Policy` (#845)
|
||||||
|
|
||||||
Added
|
Added
|
||||||
-----
|
-----
|
||||||
|
@ -38,10 +40,11 @@ Added
|
||||||
- Add sorting, pagination, and searching to the admin dashboard (#538)
|
- Add sorting, pagination, and searching to the admin dashboard (#538)
|
||||||
- Add Project History page that records all changes (#553)
|
- Add Project History page that records all changes (#553)
|
||||||
- Add token-based authentication to the API (#504)
|
- Add token-based authentication to the API (#504)
|
||||||
- Add translations for Hindi, Portuguese (Brazil), Tamil
|
|
||||||
- Add illustrations as a showcase, currently only for French (#544)
|
- Add illustrations as a showcase, currently only for French (#544)
|
||||||
- Add a page for downloading mobile application (#688)
|
- Add a page for downloading mobile application (#688)
|
||||||
|
- Add optional support for a simple CAPTCHA (#844)
|
||||||
- Add translations for Greek, Esperanto, Italian, Japanese, Portuguese and Swedish
|
- Add translations for Greek, Esperanto, Italian, Japanese, Portuguese and Swedish
|
||||||
|
- Publish an `official docker image <https://hub.docker.com/r/ihatemoney/ihatemoney>`_
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
-------
|
-------
|
||||||
|
@ -51,6 +54,7 @@ Changed
|
||||||
- Make language choice persistent (#547)
|
- Make language choice persistent (#547)
|
||||||
- Localize date strings in the current language (#590)
|
- Localize date strings in the current language (#590)
|
||||||
- Differenciate "flash alerts" notifications (#594)
|
- Differenciate "flash alerts" notifications (#594)
|
||||||
|
- Display "flash messages" persistently instead of making them disappear (#856)
|
||||||
- Improve menu bar spacing, put history and settings in a submenu (#739)
|
- Improve menu bar spacing, put history and settings in a submenu (#739)
|
||||||
- Change Dockerfile to install python dependencies at build time (#793)
|
- Change Dockerfile to install python dependencies at build time (#793)
|
||||||
- Updating project settings doesn't require to enter or update project code (#774)
|
- Updating project settings doesn't require to enter or update project code (#774)
|
||||||
|
|
|
@ -29,8 +29,8 @@ Glandos
|
||||||
Heimen Stoffels
|
Heimen Stoffels
|
||||||
James Leong
|
James Leong
|
||||||
Jocelyn Delalande
|
Jocelyn Delalande
|
||||||
Lucas Verney
|
|
||||||
Luc Didry
|
Luc Didry
|
||||||
|
Lucas Verney
|
||||||
Marien Fressinaud
|
Marien Fressinaud
|
||||||
Mathieu Leplatre
|
Mathieu Leplatre
|
||||||
mcnesium
|
mcnesium
|
||||||
|
@ -44,6 +44,7 @@ Richard Coates
|
||||||
THANOS SIOURDAKIS
|
THANOS SIOURDAKIS
|
||||||
Toover
|
Toover
|
||||||
Xavier Mehrenberger
|
Xavier Mehrenberger
|
||||||
|
Youe Graillot
|
||||||
zorun
|
zorun
|
||||||
|
|
||||||
The manual drawings are from Coline Billon, they are under CC BY 4.0.
|
The manual drawings are from Coline Billon, they are under CC BY 4.0.
|
||||||
|
|
|
@ -21,6 +21,7 @@ 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
|
||||||
BABEL_DEFAULT_TIMEZONE = "$BABEL_DEFAULT_TIMEZONE"
|
BABEL_DEFAULT_TIMEZONE = "$BABEL_DEFAULT_TIMEZONE"
|
||||||
|
SESSION_COOKIE_SECURE = $SESSION_COOKIE_SECURE
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start gunicorn without forking
|
# Start gunicorn without forking
|
||||||
|
|
92
docs/api.rst
92
docs/api.rst
|
@ -4,7 +4,9 @@ The REST API
|
||||||
All of what's possible to do with the website is also possible via a web API.
|
All of what's possible to do with the website is also possible via a web API.
|
||||||
This document explains how the API is organized and how you can query it.
|
This document explains how the API is organized and how you can query it.
|
||||||
|
|
||||||
The only supported data format is JSON.
|
The main supported data format is JSON. When using POST or PUT, you can
|
||||||
|
either pass data encoded in JSON or in ``application/x-www-form-urlencoded``
|
||||||
|
format.
|
||||||
|
|
||||||
Overall organisation
|
Overall organisation
|
||||||
====================
|
====================
|
||||||
|
@ -31,7 +33,7 @@ instead of basic auth.
|
||||||
For instance, start by generating the token (of course, you need to authenticate)::
|
For instance, start by generating the token (of course, you need to authenticate)::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/token
|
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/token
|
||||||
{"token": "eyJwcm9qZWN0X2lkIjoiZGVtbyJ9.M86C3AiZa_SFEyiddYXdTh2-OOI"}
|
{"token": "WyJ0ZXN0Il0.Rt04fNMmxp9YslCRq8hB6jE9s1Q"}
|
||||||
|
|
||||||
Make sure to store this token securely: it allows full access to the project.
|
Make sure to store this token securely: it allows full access to the project.
|
||||||
For instance, use it to obtain information about the project (replace PROJECT_TOKEN with
|
For instance, use it to obtain information about the project (replace PROJECT_TOKEN with
|
||||||
|
@ -47,7 +49,7 @@ looks like::
|
||||||
This token can also be used to authenticate for a project on the web interface, which can be useful
|
This token can also be used to authenticate for a project on the web interface, which can be useful
|
||||||
to generate invitation links. You would simply create an URL of the form::
|
to generate invitation links. You would simply create an URL of the form::
|
||||||
|
|
||||||
https://ihatemoney.org/authenticate?token=PROJECT_TOKEN
|
https://ihatemoney.org/demo/join/PROJECT_TOKEN
|
||||||
|
|
||||||
Such a link grants full access to the project associated with the token.
|
Such a link grants full access to the project associated with the token.
|
||||||
|
|
||||||
|
@ -64,10 +66,16 @@ Creating a project
|
||||||
|
|
||||||
A project needs the following arguments:
|
A project needs the following arguments:
|
||||||
|
|
||||||
* ``name``: The project name (string)
|
* ``name``: the project name (string)
|
||||||
* ``id``: the project identifier (string without special chars or spaces)
|
* ``id``: the project identifier (string without special chars or spaces)
|
||||||
* ``password``: the project password / secret code (string)
|
* ``password``: the project password / secret code (string)
|
||||||
* ``contact_email``: the contact email
|
* ``contact_email``: the contact email (string)
|
||||||
|
|
||||||
|
Optional arguments:
|
||||||
|
|
||||||
|
* ``default_currency``: the default currency to use for a multi-currency project,
|
||||||
|
in ISO 4217 format. Bills are converted to this currency for operations like balance
|
||||||
|
or statistics. Default value: ``XXX`` (no currency).
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
@ -75,7 +83,7 @@ A project needs the following arguments:
|
||||||
-d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org'
|
-d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org'
|
||||||
"yay"
|
"yay"
|
||||||
|
|
||||||
As you can see, the API returns the identifier of the project
|
As you can see, the API returns the identifier of the project.
|
||||||
|
|
||||||
Getting information about the project
|
Getting information about the project
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -88,6 +96,7 @@ Getting information about the project::
|
||||||
"id": "demo",
|
"id": "demo",
|
||||||
"name": "demonstration",
|
"name": "demonstration",
|
||||||
"contact_email": "demo@notmyidea.org",
|
"contact_email": "demo@notmyidea.org",
|
||||||
|
"default_currency": "XXX",
|
||||||
"members": [{"id": 11515, "name": "f", "weight": 1.0, "activated": true, "balance": 0},
|
"members": [{"id": 11515, "name": "f", "weight": 1.0, "activated": true, "balance": 0},
|
||||||
{"id": 11531, "name": "g", "weight": 1.0, "activated": true, "balance": 0},
|
{"id": 11531, "name": "g", "weight": 1.0, "activated": true, "balance": 0},
|
||||||
{"id": 11532, "name": "peter", "weight": 1.0, "activated": true, "balance": 5.0},
|
{"id": 11532, "name": "peter", "weight": 1.0, "activated": true, "balance": 5.0},
|
||||||
|
@ -151,22 +160,55 @@ You can get the list of bills by doing a ``GET`` on
|
||||||
|
|
||||||
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/bills
|
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/bills
|
||||||
|
|
||||||
Add a bill with a ``POST`` query on ``/api/projects/<id>/bills``. you need the
|
Or get a specific bill by ID::
|
||||||
following params:
|
|
||||||
|
|
||||||
* ``date``: the date of the bill; defaults to current date if not
|
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/bills/42
|
||||||
provided. (format is ``yyyy-mm-dd``)
|
{
|
||||||
* ``what``: what have been payed
|
"id": 42,
|
||||||
* ``payer``: by who ? (id)
|
"payer_id": 11,
|
||||||
* ``payed_for``: for who ? (id, to set multiple id use a list,
|
"owers": [
|
||||||
e.g. ``["id1", "id2"]``)
|
{
|
||||||
* ``amount``: amount payed
|
"id": 22,
|
||||||
|
"name": "Alexis",
|
||||||
|
"weight": 1,
|
||||||
|
"activated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"amount": 100,
|
||||||
|
"date": "2020-12-24",
|
||||||
|
"creation_date": "2021-01-13",
|
||||||
|
"what": "Raclette du nouvel an",
|
||||||
|
"external_link": "",
|
||||||
|
"original_currency": "XXX",
|
||||||
|
"converted_amount": 100
|
||||||
|
}
|
||||||
|
|
||||||
|
``amount`` is expressed in the ``original_currency`` of the bill, while
|
||||||
|
``converted_amount`` is expressed in the project ``default_currency``.
|
||||||
|
Here, they are the same.
|
||||||
|
|
||||||
|
Add a bill with a ``POST`` query on ``/api/projects/<id>/bills``. You need the
|
||||||
|
following required parameters:
|
||||||
|
|
||||||
|
* ``what``: what has been paid (string)
|
||||||
|
* ``payer``: paid by who? (id)
|
||||||
|
* ``payed_for``: for who ? (id). To set multiple id, simply pass
|
||||||
|
the parameter multiple times (x-www-form-urlencoded) or pass a list of id (JSON).
|
||||||
|
* ``amount``: amount payed (float)
|
||||||
|
|
||||||
|
And optional parameters:
|
||||||
|
|
||||||
|
* ``date``: the date of the bill (``yyyy-mm-dd`` format). Defaults to current date
|
||||||
|
if not provided.
|
||||||
|
* ``original_currency``: the currency in which ``amount`` has been paid (ISO 4217 code).
|
||||||
|
Only makes sense for a project with currencies. Defaults to the project ``default_currency``.
|
||||||
|
* ``external_link``: an optional URL associated with the bill.
|
||||||
|
|
||||||
Returns the id of the created bill ::
|
Returns the id of the created bill ::
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X POST\
|
$ curl --basic -u demo:demo -X POST\
|
||||||
https://ihatemoney.org/api/projects/demo/bills\
|
https://ihatemoney.org/api/projects/demo/bills\
|
||||||
-d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=200"
|
-d "date=2011-09-10&what=raclette&payer=1&payed_for=3&payed_for=5&amount=200"
|
||||||
80
|
80
|
||||||
|
|
||||||
You can also ``PUT`` a new version of the bill at
|
You can also ``PUT`` a new version of the bill at
|
||||||
|
@ -174,7 +216,7 @@ You can also ``PUT`` a new version of the bill at
|
||||||
|
|
||||||
$ curl --basic -u demo:demo -X PUT\
|
$ curl --basic -u demo:demo -X PUT\
|
||||||
https://ihatemoney.org/api/projects/demo/bills/80\
|
https://ihatemoney.org/api/projects/demo/bills/80\
|
||||||
-d "date=2011-09-10&what=raclette&payer=31&payed_for=31&amount=250"
|
-d "date=2011-09-10&what=raclette&payer=1&payed_for=3&payed_for=5&payed_for=1&amount=250"
|
||||||
80
|
80
|
||||||
|
|
||||||
And you can of course ``DELETE`` them at
|
And you can of course ``DELETE`` them at
|
||||||
|
@ -194,15 +236,15 @@ You can get some project stats with a ``GET`` on
|
||||||
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/statistics
|
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/statistics
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"balance": 12.5,
|
"member": {"activated": true, "id": 1, "name": "alexis", "weight": 1.0},
|
||||||
"member": {"activated": True, "id": 1, "name": "alexis", "weight": 1.0},
|
"paid": 25.5,
|
||||||
"paid": 25.0,
|
"spent": 15,
|
||||||
"spent": 12.5
|
"balance": 10.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"balance": -12.5,
|
"member": {"activated": true, "id": 2, "name": "fred", "weight": 1.0},
|
||||||
"member": {"activated": True, "id": 2, "name": "fred", "weight": 1.0},
|
"paid": 5,
|
||||||
"paid": 0,
|
"spent": 15.5,
|
||||||
"spent": 12.5
|
"balance": -10.5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,6 +13,21 @@ To know defaults on your deployed instance, simply look at your
|
||||||
|
|
||||||
"Production values" are the recommended values for use in production.
|
"Production values" are the recommended values for use in production.
|
||||||
|
|
||||||
|
Configuration files
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
By default, Ihatemoney loads its configuration from ``/etc/ihatemoney/ihatemoney.cfg``.
|
||||||
|
|
||||||
|
If you need to load the configuration from a custom path, you can define the
|
||||||
|
``IHATEMONEY_SETTINGS_FILE_PATH`` environment variable with the path to the configuration
|
||||||
|
file.
|
||||||
|
For instance ::
|
||||||
|
|
||||||
|
export IHATEMONEY_SETTINGS_FILE_PATH="/path/to/your/conf/file.cfg"
|
||||||
|
|
||||||
|
The path should be absolute. A relative path will be interpreted as being
|
||||||
|
inside ``/etc/ihatemoney/``.
|
||||||
|
|
||||||
`SQLALCHEMY_DATABASE_URI`
|
`SQLALCHEMY_DATABASE_URI`
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
@ -49,6 +64,21 @@ of the secret key could easily access any project and bypass the private code ve
|
||||||
- **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to
|
- **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to
|
||||||
something random, which is good.
|
something random, which is good.
|
||||||
|
|
||||||
|
`SESSION_COOKIE_SECURE`
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
A boolean that controls whether the session cookie will be marked "secure".
|
||||||
|
If this is the case, browsers will refuse to send the session cookie over plain HTTP.
|
||||||
|
|
||||||
|
- **Default value:** ``True``
|
||||||
|
- **Production value:** ``True`` if you run your service over HTTPS, ``False`` if you run
|
||||||
|
your service over plain HTTP.
|
||||||
|
|
||||||
|
Note: this setting is actually interpreted by Flask, see the
|
||||||
|
`Flask documentation`_ for details.
|
||||||
|
|
||||||
|
.. _Flask documentation: https://flask.palletsprojects.com/en/2.0.x/config/#SESSION_COOKIE_SECURE
|
||||||
|
|
||||||
`MAIL_DEFAULT_SENDER`
|
`MAIL_DEFAULT_SENDER`
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
@ -125,6 +155,11 @@ Note: this setting is actually interpreted by Flask-Babel, see the
|
||||||
|
|
||||||
.. _Flask-Babel guide for formatting dates: https://pythonhosted.org/Flask-Babel/#formatting-dates
|
.. _Flask-Babel guide for formatting dates: https://pythonhosted.org/Flask-Babel/#formatting-dates
|
||||||
|
|
||||||
|
`ENABLE_CAPTCHA`
|
||||||
|
---------------
|
||||||
|
|
||||||
|
It is possible to add a simple captcha in order to filter out spammer bots on the form creation.
|
||||||
|
In order to do so, you just have to set `ENABLE_CAPTCHA = True`.
|
||||||
|
|
||||||
Configuring emails sending
|
Configuring emails sending
|
||||||
--------------------------
|
--------------------------
|
||||||
|
@ -142,12 +177,3 @@ possible to configure it to act differently, thanks to the great
|
||||||
* **MAIL_PASSWORD** : default **None**
|
* **MAIL_PASSWORD** : default **None**
|
||||||
* **DEFAULT_MAIL_SENDER** : default **None**
|
* **DEFAULT_MAIL_SENDER** : default **None**
|
||||||
|
|
||||||
Using an alternate settings path
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
You can put your settings file where you want, and pass its path to the
|
|
||||||
application using the ``IHATEMONEY_SETTINGS_FILE_PATH`` environment variable.
|
|
||||||
|
|
||||||
For instance ::
|
|
||||||
|
|
||||||
export IHATEMONEY_SETTINGS_FILE_PATH="/path/to/your/conf/file.cfg"
|
|
||||||
|
|
|
@ -104,12 +104,20 @@ You can create a ``settings.cfg`` file, with the following content::
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
SQLACHEMY_ECHO = DEBUG
|
SQLACHEMY_ECHO = DEBUG
|
||||||
|
|
||||||
You can also set the `TESTING` flag to `True` so no mails are sent
|
|
||||||
(and no exception is raised) while you're on development mode.
|
|
||||||
Then before running the application, declare its path with ::
|
Then before running the application, declare its path with ::
|
||||||
|
|
||||||
export IHATEMONEY_SETTINGS_FILE_PATH="$(pwd)/settings.cfg"
|
export IHATEMONEY_SETTINGS_FILE_PATH="$(pwd)/settings.cfg"
|
||||||
|
|
||||||
|
You can also set the ``TESTING`` flag to ``True`` so no mails are sent
|
||||||
|
(and no exception is raised) while you're on development mode.
|
||||||
|
|
||||||
|
In some cases, you may need to disable secure cookies by setting
|
||||||
|
``SESSION_COOKIE_SECURE`` to ``False``. This is needed if you
|
||||||
|
access your dev server over the network: with the default value
|
||||||
|
of ``SESSION_COOKIE_SECURE``, the browser will refuse to send
|
||||||
|
the session cookie over insecure HTTP, so many features of Ihatemoney
|
||||||
|
won't work (project login, language change, etc).
|
||||||
|
|
||||||
.. _contributing-developer:
|
.. _contributing-developer:
|
||||||
|
|
||||||
Contributing as a developer
|
Contributing as a developer
|
||||||
|
|
|
@ -65,6 +65,17 @@ 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.
|
Then follow :ref:`general-procedure` from step 1. in order to complete the update.
|
||||||
|
|
||||||
|
Disable session cookie security if running over plain HTTP
|
||||||
|
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
.. note:: If you are running Ihatemoney over HTTPS, no special action is required.
|
||||||
|
|
||||||
|
Session cookies are now marked "secure" by default to increase security.
|
||||||
|
|
||||||
|
If you run Ihatemoney over plain HTTP, you need to explicitly disable this security
|
||||||
|
feature by setting ``SESSION_COOKIE_SECURE`` to ``False``, see :ref:`configuration`.
|
||||||
|
|
||||||
|
|
||||||
Switch to MariaDB >= 10.3.2 instead of MySQL
|
Switch to MariaDB >= 10.3.2 instead of MySQL
|
||||||
++++++++++++++++++++++++++++++++++++++++++++
|
++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,9 @@ def need_auth(f):
|
||||||
auth_token = auth_header.split(" ")[1]
|
auth_token = auth_header.split(" ")[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
abort(401)
|
abort(401)
|
||||||
project_id = Project.verify_token(auth_token, token_type="non_timed_token")
|
project_id = Project.verify_token(
|
||||||
|
auth_token, token_type="auth", project_id=project_id
|
||||||
|
)
|
||||||
if auth_token and project_id:
|
if auth_token and project_id:
|
||||||
project = Project.query.get(project_id)
|
project = Project.query.get(project_id)
|
||||||
if project:
|
if project:
|
||||||
|
|
|
@ -38,3 +38,11 @@ ACTIVATE_ADMIN_DASHBOARD = False
|
||||||
# You can change the timezone used to display time. By default it will be
|
# You can change the timezone used to display time. By default it will be
|
||||||
#derived from the server OS.
|
#derived from the server OS.
|
||||||
#BABEL_DEFAULT_TIMEZONE = "Europe/Paris"
|
#BABEL_DEFAULT_TIMEZONE = "Europe/Paris"
|
||||||
|
|
||||||
|
# Enable secure cookies. Requires HTTPS. Disable if you run your ihatemoney
|
||||||
|
# service over plain HTTP.
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
# You can activate an optional CAPTCHA if you want to. It can be helpful
|
||||||
|
# to filter spammer bots.
|
||||||
|
# ENABLE_CAPTCHA = True
|
||||||
|
|
|
@ -8,6 +8,7 @@ ACTIVATE_DEMO_PROJECT = True
|
||||||
ADMIN_PASSWORD = ""
|
ADMIN_PASSWORD = ""
|
||||||
ALLOW_PUBLIC_PROJECT_CREATION = True
|
ALLOW_PUBLIC_PROJECT_CREATION = True
|
||||||
ACTIVATE_ADMIN_DASHBOARD = False
|
ACTIVATE_ADMIN_DASHBOARD = False
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
SUPPORTED_LANGUAGES = [
|
SUPPORTED_LANGUAGES = [
|
||||||
"de",
|
"de",
|
||||||
"el",
|
"el",
|
||||||
|
@ -31,3 +32,4 @@ SUPPORTED_LANGUAGES = [
|
||||||
"uk",
|
"uk",
|
||||||
"zh_Hans",
|
"zh_Hans",
|
||||||
]
|
]
|
||||||
|
ENABLE_CAPTCHA = False
|
||||||
|
|
|
@ -13,6 +13,7 @@ from wtforms.fields.core import Label, SelectField, SelectMultipleField
|
||||||
from wtforms.fields.html5 import DateField, DecimalField, URLField
|
from wtforms.fields.html5 import DateField, DecimalField, URLField
|
||||||
from wtforms.fields.simple import BooleanField, PasswordField, StringField, SubmitField
|
from wtforms.fields.simple import BooleanField, PasswordField, StringField, SubmitField
|
||||||
from wtforms.validators import (
|
from wtforms.validators import (
|
||||||
|
URL,
|
||||||
DataRequired,
|
DataRequired,
|
||||||
Email,
|
Email,
|
||||||
EqualTo,
|
EqualTo,
|
||||||
|
@ -112,7 +113,11 @@ class EditProjectForm(FlaskForm):
|
||||||
project_history = BooleanField(_("Enable project history"))
|
project_history = BooleanField(_("Enable project history"))
|
||||||
ip_recording = BooleanField(_("Use IP tracking for project history"))
|
ip_recording = BooleanField(_("Use IP tracking for project history"))
|
||||||
currency_helper = CurrencyConverter()
|
currency_helper = CurrencyConverter()
|
||||||
default_currency = SelectField(_("Default Currency"), validators=[DataRequired()])
|
default_currency = SelectField(
|
||||||
|
_("Default Currency"),
|
||||||
|
validators=[DataRequired()],
|
||||||
|
default=CurrencyConverter.no_currency,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if not hasattr(self, "id"):
|
if not hasattr(self, "id"):
|
||||||
|
@ -220,6 +225,19 @@ class ProjectForm(EditProjectForm):
|
||||||
)
|
)
|
||||||
raise ValidationError(Markup(message))
|
raise ValidationError(Markup(message))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enable_captcha(cls):
|
||||||
|
captchaField = StringField(
|
||||||
|
_("Which is a real currency: Euro or Petro dollar?"),
|
||||||
|
validators=[DataRequired()],
|
||||||
|
)
|
||||||
|
setattr(cls, "captcha", captchaField)
|
||||||
|
|
||||||
|
def validate_captcha(form, field):
|
||||||
|
if not field.data.lower() == _("euro"):
|
||||||
|
message = _("Please, validate the captcha to proceed.")
|
||||||
|
raise ValidationError(Markup(message))
|
||||||
|
|
||||||
|
|
||||||
class DestructiveActionProjectForm(FlaskForm):
|
class DestructiveActionProjectForm(FlaskForm):
|
||||||
"""Used for any important "delete" action linked to a project:
|
"""Used for any important "delete" action linked to a project:
|
||||||
|
@ -292,7 +310,7 @@ class BillForm(FlaskForm):
|
||||||
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
|
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
|
||||||
external_link = URLField(
|
external_link = URLField(
|
||||||
_("External link"),
|
_("External link"),
|
||||||
validators=[Optional()],
|
validators=[Optional(), URL()],
|
||||||
description=_("A link to an external document, related to this bill"),
|
description=_("A link to an external document, related to this bill"),
|
||||||
)
|
)
|
||||||
payed_for = SelectMultipleField(
|
payed_for = SelectMultipleField(
|
||||||
|
@ -321,7 +339,7 @@ class BillForm(FlaskForm):
|
||||||
bill.external_link = ""
|
bill.external_link = ""
|
||||||
bill.date = self.date
|
bill.date = self.date
|
||||||
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for]
|
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for]
|
||||||
bill.original_currency = CurrencyConverter.no_currency
|
bill.original_currency = self.original_currency
|
||||||
bill.converted_amount = self.currency_helper.exchange_currency(
|
bill.converted_amount = self.currency_helper.exchange_currency(
|
||||||
bill.amount, bill.original_currency, project.default_currency
|
bill.amount, bill.original_currency, project.default_currency
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,8 +7,8 @@ from flask_sqlalchemy import BaseQuery, SQLAlchemy
|
||||||
from itsdangerous import (
|
from itsdangerous import (
|
||||||
BadSignature,
|
BadSignature,
|
||||||
SignatureExpired,
|
SignatureExpired,
|
||||||
TimedJSONWebSignatureSerializer,
|
|
||||||
URLSafeSerializer,
|
URLSafeSerializer,
|
||||||
|
URLSafeTimedSerializer,
|
||||||
)
|
)
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
@ -179,6 +179,7 @@ class Project(db.Model):
|
||||||
"ower": transaction["ower"].name,
|
"ower": transaction["ower"].name,
|
||||||
"receiver": transaction["receiver"].name,
|
"receiver": transaction["receiver"].name,
|
||||||
"amount": round(transaction["amount"], 2),
|
"amount": round(transaction["amount"], 2),
|
||||||
|
"currency": transaction["currency"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return pretty_transactions
|
return pretty_transactions
|
||||||
|
@ -192,6 +193,7 @@ class Project(db.Model):
|
||||||
"ower": members[ower_id],
|
"ower": members[ower_id],
|
||||||
"receiver": members[receiver_id],
|
"receiver": members[receiver_id],
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
|
"currency": self.default_currency,
|
||||||
}
|
}
|
||||||
for ower_id, amount, receiver_id in settle_plan
|
for ower_id, amount, receiver_id in settle_plan
|
||||||
]
|
]
|
||||||
|
@ -269,6 +271,7 @@ class Project(db.Model):
|
||||||
{
|
{
|
||||||
"what": bill.what,
|
"what": bill.what,
|
||||||
"amount": round(bill.amount, 2),
|
"amount": round(bill.amount, 2),
|
||||||
|
"currency": bill.original_currency,
|
||||||
"date": str(bill.date),
|
"date": str(bill.date),
|
||||||
"payer_name": Person.query.get(bill.payer_id).name,
|
"payer_name": Person.query.get(bill.payer_id).name,
|
||||||
"payer_weight": Person.query.get(bill.payer_id).weight,
|
"payer_weight": Person.query.get(bill.payer_id).weight,
|
||||||
|
@ -336,41 +339,61 @@ class Project(db.Model):
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def generate_token(self, expiration=0):
|
def generate_token(self, token_type="auth"):
|
||||||
"""Generate a timed and serialized JsonWebToken
|
"""Generate a timed and serialized JsonWebToken
|
||||||
|
|
||||||
:param expiration: Token expiration time (in seconds)
|
:param token_type: Either "auth" for authentication (invalidated when project code changed),
|
||||||
|
or "reset" for password reset (invalidated after expiration)
|
||||||
"""
|
"""
|
||||||
if expiration:
|
|
||||||
serializer = TimedJSONWebSignatureSerializer(
|
if token_type == "reset":
|
||||||
current_app.config["SECRET_KEY"], expiration
|
serializer = URLSafeTimedSerializer(
|
||||||
|
current_app.config["SECRET_KEY"], salt=token_type
|
||||||
)
|
)
|
||||||
token = serializer.dumps({"project_id": self.id}).decode("utf-8")
|
token = serializer.dumps([self.id])
|
||||||
else:
|
else:
|
||||||
serializer = URLSafeSerializer(current_app.config["SECRET_KEY"])
|
serializer = URLSafeSerializer(
|
||||||
token = serializer.dumps({"project_id": self.id})
|
current_app.config["SECRET_KEY"] + self.password, salt=token_type
|
||||||
|
)
|
||||||
|
token = serializer.dumps([self.id])
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def verify_token(token, token_type="timed_token"):
|
def verify_token(token, token_type="auth", project_id=None, max_age=3600):
|
||||||
"""Return the project id associated to the provided token,
|
"""Return the project id associated to the provided token,
|
||||||
None if the provided token is expired or not valid.
|
None if the provided token is expired or not valid.
|
||||||
|
|
||||||
:param token: Serialized TimedJsonWebToken
|
:param token: Serialized TimedJsonWebToken
|
||||||
|
:param token_type: Either "auth" for authentication (invalidated when project code changed),
|
||||||
|
or "reset" for password reset (invalidated after expiration)
|
||||||
|
:param project_id: Project ID. Used for token_type "auth" to use the password as serializer
|
||||||
|
secret key.
|
||||||
|
:param max_age: Token expiration time (in seconds). Only used with token_type "reset"
|
||||||
"""
|
"""
|
||||||
if token_type == "timed_token":
|
loads_kwargs = {}
|
||||||
serializer = TimedJSONWebSignatureSerializer(
|
if token_type == "reset":
|
||||||
current_app.config["SECRET_KEY"]
|
serializer = URLSafeTimedSerializer(
|
||||||
|
current_app.config["SECRET_KEY"], salt=token_type
|
||||||
)
|
)
|
||||||
|
loads_kwargs["max_age"] = max_age
|
||||||
else:
|
else:
|
||||||
serializer = URLSafeSerializer(current_app.config["SECRET_KEY"])
|
project = Project.query.get(project_id) if project_id is not None else None
|
||||||
|
password = project.password if project is not None else ""
|
||||||
|
serializer = URLSafeSerializer(
|
||||||
|
current_app.config["SECRET_KEY"] + password, salt=token_type
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
data = serializer.loads(token)
|
data = serializer.loads(token, **loads_kwargs)
|
||||||
except SignatureExpired:
|
except SignatureExpired:
|
||||||
return None
|
return None
|
||||||
except BadSignature:
|
except BadSignature:
|
||||||
return None
|
return None
|
||||||
return data["project_id"]
|
|
||||||
|
data_project = data[0] if isinstance(data, list) else None
|
||||||
|
return (
|
||||||
|
data_project if project_id is None or data_project == project_id else None
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -385,7 +408,7 @@ class Project(db.Model):
|
||||||
name="demonstration",
|
name="demonstration",
|
||||||
password=generate_password_hash("demo"),
|
password=generate_password_hash("demo"),
|
||||||
contact_email="demo@notmyidea.org",
|
contact_email="demo@notmyidea.org",
|
||||||
default_currency="EUR",
|
default_currency="XXX",
|
||||||
)
|
)
|
||||||
db.session.add(project)
|
db.session.add(project)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -413,7 +436,7 @@ class Project(db.Model):
|
||||||
bill.what = subject
|
bill.what = subject
|
||||||
bill.owers = [members[name] for name in owers]
|
bill.owers = [members[name] for name in owers]
|
||||||
bill.amount = amount
|
bill.amount = amount
|
||||||
bill.original_currency = "EUR"
|
bill.original_currency = "XXX"
|
||||||
bill.converted_amount = amount
|
bill.converted_amount = amount
|
||||||
|
|
||||||
db.session.add(bill)
|
db.session.add(bill)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from flask import Flask, g, render_template, request, session
|
||||||
from flask_babel import Babel, format_currency
|
from flask_babel import Babel, format_currency
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask_migrate import Migrate, stamp, upgrade
|
from flask_migrate import Migrate, stamp, upgrade
|
||||||
|
from flask_talisman import Talisman
|
||||||
from jinja2 import pass_context
|
from jinja2 import pass_context
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
import pytz
|
import pytz
|
||||||
|
@ -126,6 +127,24 @@ def create_app(
|
||||||
instance_relative_config=instance_relative_config,
|
instance_relative_config=instance_relative_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If we need to load external JS/CSS/image resources, it needs to be added here, see
|
||||||
|
# https://github.com/wntrblm/flask-talisman#content-security-policy
|
||||||
|
csp = {
|
||||||
|
"default-src": ["'self'"],
|
||||||
|
# We have several inline javascript scripts :(
|
||||||
|
"script-src": ["'self'", "'unsafe-inline'"],
|
||||||
|
"object-src": "'none'",
|
||||||
|
}
|
||||||
|
|
||||||
|
Talisman(
|
||||||
|
app,
|
||||||
|
# Forcing HTTPS is the job of a reverse proxy
|
||||||
|
force_https=False,
|
||||||
|
# This is handled separately through the SESSION_COOKIE_SECURE Flask setting
|
||||||
|
session_cookie_secure=False,
|
||||||
|
content_security_policy=csp,
|
||||||
|
)
|
||||||
|
|
||||||
# If a configuration object is passed, use it. Otherwise try to find one.
|
# If a configuration object is passed, use it. Otherwise try to find one.
|
||||||
load_configuration(app, configuration)
|
load_configuration(app, configuration)
|
||||||
app.wsgi_app = PrefixedWSGI(app)
|
app.wsgi_app = PrefixedWSGI(app)
|
||||||
|
|
|
@ -28,6 +28,7 @@ body {
|
||||||
}
|
}
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-family: "Lobster", arial, serif;
|
font-family: "Lobster", arial, serif;
|
||||||
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
|
@ -468,7 +469,6 @@ tr.payer_line .balance-name {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4.5rem;
|
top: 4.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.light {
|
.light {
|
||||||
|
|
|
@ -75,6 +75,9 @@
|
||||||
{{ input(form.name) }}
|
{{ input(form.name) }}
|
||||||
{{ input(form.password) }}
|
{{ input(form.password) }}
|
||||||
{{ input(form.contact_email) }}
|
{{ input(form.contact_email) }}
|
||||||
|
{% if config['ENABLE_CAPTCHA'] %}
|
||||||
|
{{ input(form.captcha) }}
|
||||||
|
{% endif %}
|
||||||
{{ input(form.default_currency) }}
|
{{ input(form.default_currency) }}
|
||||||
{% if not home %}
|
{% if not home %}
|
||||||
{{ submit(form.submit, home=True) }}
|
{{ submit(form.submit, home=True) }}
|
||||||
|
@ -171,7 +174,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="mb-3">
|
<details class="mb-3">
|
||||||
<summary class="mb-2">{{ _("More options") }}</summary>
|
<summary class="mb-2">{{ _("More options") }}</summary>
|
||||||
{% if g.project.default_currency != "XXX" %}
|
{% if g.project.default_currency != "XXX" %}
|
||||||
|
|
|
@ -4,7 +4,7 @@ Someone using the email address {{ g.project.contact_email }} invited you to sha
|
||||||
|
|
||||||
It's as simple as saying what did you pay for, for whom, and how much did it cost you, we are caring about the rest.
|
It's as simple as saying what did you pay for, for whom, and how much did it cost you, we are caring about the rest.
|
||||||
|
|
||||||
You can log in using this link: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}.
|
You can log in using this link: {{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}.
|
||||||
|
|
||||||
Once logged-in, you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }}
|
Once logged-in, you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }}
|
||||||
If your cookie gets deleted or if you log out, you will need to log back in using the first link.
|
If your cookie gets deleted or if you log out, you will need to log back in using the first link.
|
||||||
|
|
|
@ -4,7 +4,7 @@ Quelqu'un dont l'adresse email est {{ g.project.contact_email }} vous a invité
|
||||||
|
|
||||||
Il suffit de renseigner qui a payé pour quoi, pour qui, combien ça a coûté, et on s’occupe du reste.
|
Il suffit de renseigner qui a payé pour quoi, pour qui, combien ça a coûté, et on s’occupe du reste.
|
||||||
|
|
||||||
Vous pouvez vous connecter grâce à ce lien : {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}.
|
Vous pouvez vous connecter grâce à ce lien : {{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}.
|
||||||
|
|
||||||
Une fois connecté, vous pourrez utiliser le lien suivant qui est plus facile à mémoriser : {{ url_for(".list_bills", _external=True) }}
|
Une fois connecté, vous pourrez utiliser le lien suivant qui est plus facile à mémoriser : {{ url_for(".list_bills", _external=True) }}
|
||||||
Si vous êtes déconnecté volontairement ou non, vous devrez utiliser à nouveau le premier lien.
|
Si vous êtes déconnecté volontairement ou non, vous devrez utiliser à nouveau le premier lien.
|
||||||
|
|
|
@ -21,12 +21,6 @@
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
<script type="text/javascript" charset="utf-8">
|
<script type="text/javascript" charset="utf-8">
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
setTimeout(function(){
|
|
||||||
$(".flash").fadeOut("slow", function () {
|
|
||||||
$(".flash").remove();
|
|
||||||
});
|
|
||||||
}, 4000);
|
|
||||||
|
|
||||||
$('.dropdown-toggle').dropdown();
|
$('.dropdown-toggle').dropdown();
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
{% block js %}{% endblock %}
|
{% block js %}{% endblock %}
|
||||||
|
@ -128,11 +122,12 @@
|
||||||
|
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||||
{% if category == "message" %}{# Default category for flash(msg) #}
|
<div class="flash alert alert-{{ "success" if category == "message" else category }} alert-dismissible fade show">
|
||||||
<div class="flash alert alert-success">{{ message }}</div>
|
{{ message }}
|
||||||
{% else %}
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
<div class="flash alert alert-{{ category }}">{{ message }}</div>
|
<span aria-hidden="true">×</span>
|
||||||
{% endif %}
|
</button>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Hi,
|
Hi,
|
||||||
|
|
||||||
You requested to reset the password of the following project: "{{ project.name }}".
|
You requested to reset the password of the following project: "{{ project.name }}".
|
||||||
You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}.
|
You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}.
|
||||||
This link is only valid for one hour.
|
This link is only valid for one hour.
|
||||||
|
|
||||||
Hope this helps,
|
Hope this helps,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Salut,
|
Salut,
|
||||||
|
|
||||||
Vous avez demandé à réinitialiser le mot de passe du projet suivant : "{{ project.name }}".
|
Vous avez demandé à réinitialiser le mot de passe du projet suivant : "{{ project.name }}".
|
||||||
Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}.
|
Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}.
|
||||||
Ce lien est seulement valide pendant 1 heure.
|
Ce lien est seulement valide pendant 1 heure.
|
||||||
|
|
||||||
Faites-en bon usage !
|
Faites-en bon usage !
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ _("You can directly share the following link via your prefered medium") }}</br>
|
{{ _("You can directly share the following link via your prefered medium") }}</br>
|
||||||
<a href="{{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}">
|
<a href="{{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}">
|
||||||
{{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}
|
{{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<tr id="bal-member-{{ member.id }}" action="{% if member.activated %}delete{% else %}reactivate{% endif %}">
|
<tr id="bal-member-{{ member.id }}" action="{% if member.activated %}delete{% else %}reactivate{% endif %}">
|
||||||
<td class="balance-name">{{ member.name }}
|
<td class="balance-name">{{ member.name }}
|
||||||
{%- if show_weight -%}
|
{%- if show_weight -%}
|
||||||
<span class="light{% if not g.project.uses_weights %} extra-info{% endif %}">(x{{ member.weight|minimal_round(1) }})</span>
|
<span class="light{% if not g.project.uses_weights %} extra-info{% endif %}">(x{{ member.weight|minimal_round(2) }})</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
{%- if member_edit %}
|
{%- if member_edit %}
|
||||||
|
|
|
@ -11,20 +11,31 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
"""Tests the API"""
|
"""Tests the API"""
|
||||||
|
|
||||||
def api_create(self, name, id=None, password=None, contact=None):
|
def api_create(
|
||||||
|
self, name, id=None, password=None, contact=None, default_currency=None
|
||||||
|
):
|
||||||
id = id or name
|
id = id or name
|
||||||
password = password or name
|
password = password or name
|
||||||
contact = contact or f"{name}@notmyidea.org"
|
contact = contact or f"{name}@notmyidea.org"
|
||||||
|
|
||||||
return self.client.post(
|
if default_currency:
|
||||||
"/api/projects",
|
data = {
|
||||||
data={
|
|
||||||
"name": name,
|
"name": name,
|
||||||
"id": id,
|
"id": id,
|
||||||
"password": password,
|
"password": password,
|
||||||
"contact_email": contact,
|
"contact_email": contact,
|
||||||
"default_currency": "USD",
|
"default_currency": default_currency,
|
||||||
},
|
}
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"name": name,
|
||||||
|
"id": id,
|
||||||
|
"password": password,
|
||||||
|
"contact_email": contact,
|
||||||
|
}
|
||||||
|
return self.client.post(
|
||||||
|
"/api/projects",
|
||||||
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_add_member(self, project, name, weight=1):
|
def api_add_member(self, project, name, weight=1):
|
||||||
|
@ -85,7 +96,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"password": "raclette",
|
"password": "raclette",
|
||||||
"contact_email": "not-an-email",
|
"contact_email": "not-an-email",
|
||||||
"default_currency": "USD",
|
"default_currency": "XXX",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -114,7 +125,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"members": [],
|
"members": [],
|
||||||
"name": "raclette",
|
"name": "raclette",
|
||||||
"contact_email": "raclette@notmyidea.org",
|
"contact_email": "raclette@notmyidea.org",
|
||||||
"default_currency": "USD",
|
"default_currency": "XXX",
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"logging_preference": 1,
|
"logging_preference": 1,
|
||||||
}
|
}
|
||||||
|
@ -126,7 +137,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette",
|
"/api/projects/raclette",
|
||||||
data={
|
data={
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "USD",
|
"default_currency": "XXX",
|
||||||
"password": "raclette",
|
"password": "raclette",
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
"project_history": "y",
|
"project_history": "y",
|
||||||
|
@ -144,7 +155,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
expected = {
|
expected = {
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "USD",
|
"default_currency": "XXX",
|
||||||
"members": [],
|
"members": [],
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"logging_preference": 1,
|
"logging_preference": 1,
|
||||||
|
@ -157,7 +168,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette",
|
"/api/projects/raclette",
|
||||||
data={
|
data={
|
||||||
"contact_email": "yeah@notmyidea.org",
|
"contact_email": "yeah@notmyidea.org",
|
||||||
"default_currency": "USD",
|
"default_currency": "XXX",
|
||||||
"password": "tartiflette",
|
"password": "tartiflette",
|
||||||
"name": "The raclette party",
|
"name": "The raclette party",
|
||||||
},
|
},
|
||||||
|
@ -213,7 +224,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette/token", headers=self.get_auth("raclette")
|
"/api/projects/raclette/token", headers=self.get_auth("raclette")
|
||||||
)
|
)
|
||||||
decoded_resp = json.loads(resp.data.decode("utf-8"))
|
decoded_resp = json.loads(resp.data.decode("utf-8"))
|
||||||
resp = self.client.get("/authenticate?token={}".format(decoded_resp["token"]))
|
resp = self.client.get(f"/raclette/join/{decoded_resp['token']}")
|
||||||
# Test that we are redirected.
|
# Test that we are redirected.
|
||||||
self.assertEqual(302, resp.status_code)
|
self.assertEqual(302, resp.status_code)
|
||||||
|
|
||||||
|
@ -380,7 +391,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"date": "2011-08-10",
|
"date": "2011-08-10",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"converted_amount": 25.0,
|
"converted_amount": 25.0,
|
||||||
"original_currency": "USD",
|
"original_currency": "XXX",
|
||||||
"external_link": "https://raclette.fr",
|
"external_link": "https://raclette.fr",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,7 +462,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"date": "2011-09-10",
|
"date": "2011-09-10",
|
||||||
"external_link": "https://raclette.fr",
|
"external_link": "https://raclette.fr",
|
||||||
"converted_amount": 25.0,
|
"converted_amount": 25.0,
|
||||||
"original_currency": "USD",
|
"original_currency": "XXX",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -529,7 +540,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"date": "2011-08-10",
|
"date": "2011-08-10",
|
||||||
"id": id,
|
"id": id,
|
||||||
"external_link": "",
|
"external_link": "",
|
||||||
"original_currency": "USD",
|
"original_currency": "XXX",
|
||||||
"converted_amount": expected_amount,
|
"converted_amount": expected_amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,6 +575,157 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
self.assertStatus(400, req)
|
self.assertStatus(400, req)
|
||||||
|
|
||||||
|
def test_currencies(self):
|
||||||
|
# create project with a default currency
|
||||||
|
resp = self.api_create("raclette", default_currency="EUR")
|
||||||
|
self.assertTrue(201, resp.status_code)
|
||||||
|
|
||||||
|
# get information about it
|
||||||
|
resp = self.client.get(
|
||||||
|
"/api/projects/raclette", headers=self.get_auth("raclette")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(200, resp.status_code)
|
||||||
|
expected = {
|
||||||
|
"members": [],
|
||||||
|
"name": "raclette",
|
||||||
|
"contact_email": "raclette@notmyidea.org",
|
||||||
|
"default_currency": "EUR",
|
||||||
|
"id": "raclette",
|
||||||
|
"logging_preference": 1,
|
||||||
|
}
|
||||||
|
decoded_resp = json.loads(resp.data.decode("utf-8"))
|
||||||
|
self.assertDictEqual(decoded_resp, expected)
|
||||||
|
|
||||||
|
# Add members
|
||||||
|
self.api_add_member("raclette", "zorglub")
|
||||||
|
self.api_add_member("raclette", "fred")
|
||||||
|
self.api_add_member("raclette", "quentin")
|
||||||
|
|
||||||
|
# Add a bill without explicit currency
|
||||||
|
req = self.client.post(
|
||||||
|
"/api/projects/raclette/bills",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "fromage",
|
||||||
|
"payer": "1",
|
||||||
|
"payed_for": ["1", "2"],
|
||||||
|
"amount": "25",
|
||||||
|
"external_link": "https://raclette.fr",
|
||||||
|
},
|
||||||
|
headers=self.get_auth("raclette"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# should return the id
|
||||||
|
self.assertStatus(201, req)
|
||||||
|
self.assertEqual(req.data.decode("utf-8"), "1\n")
|
||||||
|
|
||||||
|
# get this bill details
|
||||||
|
req = self.client.get(
|
||||||
|
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
|
||||||
|
)
|
||||||
|
|
||||||
|
# compare with the added info
|
||||||
|
self.assertStatus(200, req)
|
||||||
|
expected = {
|
||||||
|
"what": "fromage",
|
||||||
|
"payer_id": 1,
|
||||||
|
"owers": [
|
||||||
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
||||||
|
{"activated": True, "id": 2, "name": "fred", "weight": 1},
|
||||||
|
],
|
||||||
|
"amount": 25.0,
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"id": 1,
|
||||||
|
"converted_amount": 25.0,
|
||||||
|
"original_currency": "EUR",
|
||||||
|
"external_link": "https://raclette.fr",
|
||||||
|
}
|
||||||
|
|
||||||
|
got = json.loads(req.data.decode("utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
datetime.date.today(),
|
||||||
|
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
||||||
|
)
|
||||||
|
del got["creation_date"]
|
||||||
|
self.assertDictEqual(expected, got)
|
||||||
|
|
||||||
|
# Change bill amount and currency
|
||||||
|
req = self.client.put(
|
||||||
|
"/api/projects/raclette/bills/1",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "fromage",
|
||||||
|
"payer": "1",
|
||||||
|
"payed_for": ["1", "2"],
|
||||||
|
"amount": "30",
|
||||||
|
"external_link": "https://raclette.fr",
|
||||||
|
"original_currency": "CAD",
|
||||||
|
},
|
||||||
|
headers=self.get_auth("raclette"),
|
||||||
|
)
|
||||||
|
self.assertStatus(200, req)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
req = self.client.get(
|
||||||
|
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
|
||||||
|
)
|
||||||
|
self.assertStatus(200, req)
|
||||||
|
expected_amount = self.converter.exchange_currency(30.0, "CAD", "EUR")
|
||||||
|
expected = {
|
||||||
|
"what": "fromage",
|
||||||
|
"payer_id": 1,
|
||||||
|
"owers": [
|
||||||
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
|
||||||
|
{"activated": True, "id": 2, "name": "fred", "weight": 1.0},
|
||||||
|
],
|
||||||
|
"amount": 30.0,
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"id": 1,
|
||||||
|
"converted_amount": expected_amount,
|
||||||
|
"original_currency": "CAD",
|
||||||
|
"external_link": "https://raclette.fr",
|
||||||
|
}
|
||||||
|
|
||||||
|
got = json.loads(req.data.decode("utf-8"))
|
||||||
|
del got["creation_date"]
|
||||||
|
self.assertDictEqual(expected, got)
|
||||||
|
|
||||||
|
# Add a bill with yet another currency
|
||||||
|
req = self.client.post(
|
||||||
|
"/api/projects/raclette/bills",
|
||||||
|
data={
|
||||||
|
"date": "2011-09-10",
|
||||||
|
"what": "Pierogi",
|
||||||
|
"payer": "1",
|
||||||
|
"payed_for": ["2", "3"],
|
||||||
|
"amount": "80",
|
||||||
|
"original_currency": "PLN",
|
||||||
|
},
|
||||||
|
headers=self.get_auth("raclette"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# should return the id
|
||||||
|
self.assertStatus(201, req)
|
||||||
|
self.assertEqual(req.data.decode("utf-8"), "2\n")
|
||||||
|
|
||||||
|
# Try to remove default project currency, it should fail
|
||||||
|
req = self.client.put(
|
||||||
|
"/api/projects/raclette",
|
||||||
|
data={
|
||||||
|
"contact_email": "yeah@notmyidea.org",
|
||||||
|
"default_currency": "XXX",
|
||||||
|
"password": "raclette",
|
||||||
|
"name": "The raclette party",
|
||||||
|
},
|
||||||
|
headers=self.get_auth("raclette"),
|
||||||
|
)
|
||||||
|
self.assertStatus(400, req)
|
||||||
|
self.assertIn("This project cannot be set", req.data.decode("utf-8"))
|
||||||
|
self.assertIn(
|
||||||
|
"because it contains bills in multiple currencies", req.data.decode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
def test_statistics(self):
|
def test_statistics(self):
|
||||||
# create a project
|
# create a project
|
||||||
self.api_create("raclette")
|
self.api_create("raclette")
|
||||||
|
@ -674,7 +836,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"external_link": "",
|
"external_link": "",
|
||||||
"converted_amount": 25.0,
|
"converted_amount": 25.0,
|
||||||
"original_currency": "USD",
|
"original_currency": "XXX",
|
||||||
}
|
}
|
||||||
got = json.loads(req.data.decode("utf-8"))
|
got = json.loads(req.data.decode("utf-8"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -717,7 +879,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"id": "raclette",
|
"id": "raclette",
|
||||||
"name": "raclette",
|
"name": "raclette",
|
||||||
"logging_preference": 1,
|
"logging_preference": 1,
|
||||||
"default_currency": "USD",
|
"default_currency": "XXX",
|
||||||
}
|
}
|
||||||
|
|
||||||
self.assertStatus(200, req)
|
self.assertStatus(200, req)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import json
|
||||||
import re
|
import re
|
||||||
from time import sleep
|
from time import sleep
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
from flask import session
|
from flask import session
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -12,6 +12,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from ihatemoney import models
|
from ihatemoney import models
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
from ihatemoney.currency_convertor import CurrencyConverter
|
||||||
|
from ihatemoney.tests.common.help_functions import extract_link
|
||||||
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||||
from ihatemoney.versioning import LoggingMode
|
from ihatemoney.versioning import LoggingMode
|
||||||
|
|
||||||
|
@ -88,10 +89,53 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
# Test empty and invalid tokens
|
# Test empty and invalid tokens
|
||||||
self.client.get("/exit")
|
self.client.get("/exit")
|
||||||
resp = self.client.get("/authenticate")
|
# Use another project_id
|
||||||
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
parsed_url = urlparse(url)
|
||||||
resp = self.client.get("/authenticate?token=token")
|
resp = self.client.get(
|
||||||
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
urlunparse(
|
||||||
|
parsed_url._replace(
|
||||||
|
path=parsed_url.path.replace("raclette/", "invalid_project/")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert "Create a new project" in resp.data.decode("utf-8")
|
||||||
|
|
||||||
|
# A token MUST have a point between payload and signature
|
||||||
|
resp = self.client.get("/raclette/join/token.invalid", follow_redirects=True)
|
||||||
|
self.assertIn("Provided token is invalid", resp.data.decode("utf-8"))
|
||||||
|
|
||||||
|
def test_invite_code_invalidation(self):
|
||||||
|
"""Test that invitation link expire after code change"""
|
||||||
|
self.login("raclette")
|
||||||
|
self.post_project("raclette")
|
||||||
|
response = self.client.get("/raclette/invite").data.decode("utf-8")
|
||||||
|
link = extract_link(response, "share the following link")
|
||||||
|
|
||||||
|
self.client.get("/exit")
|
||||||
|
response = self.client.get(link)
|
||||||
|
# Link is valid
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
# Change password to invalidate token
|
||||||
|
# Other data are required, but useless for the test
|
||||||
|
response = self.client.post(
|
||||||
|
"/raclette/edit",
|
||||||
|
data={
|
||||||
|
"name": "raclette",
|
||||||
|
"contact_email": "zorglub@notmyidea.org",
|
||||||
|
"password": "didoudida",
|
||||||
|
"default_currency": "XXX",
|
||||||
|
},
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "alert-danger" not in response.data.decode("utf-8")
|
||||||
|
|
||||||
|
self.client.get("/exit")
|
||||||
|
response = self.client.get(link, follow_redirects=True)
|
||||||
|
# Link is invalid
|
||||||
|
self.assertIn("Provided token is invalid", response.data.decode("utf-8"))
|
||||||
|
|
||||||
def test_password_reminder(self):
|
def test_password_reminder(self):
|
||||||
# test that it is possible to have an email containing the password of a
|
# test that it is possible to have an email containing the password of a
|
||||||
|
@ -633,6 +677,35 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
bill = models.Bill.query.filter(models.Bill.date == "2011-08-01")[0]
|
bill = models.Bill.query.filter(models.Bill.date == "2011-08-01")[0]
|
||||||
self.assertEqual(bill.amount, 25.02)
|
self.assertEqual(bill.amount, 25.02)
|
||||||
|
|
||||||
|
# add a bill with a valid external link
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2015-05-05",
|
||||||
|
"what": "fromage à raclette",
|
||||||
|
"payer": members_ids[0],
|
||||||
|
"payed_for": members_ids,
|
||||||
|
"amount": "42",
|
||||||
|
"external_link": "https://example.com/fromage",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bill = models.Bill.query.filter(models.Bill.date == "2015-05-05")[0]
|
||||||
|
self.assertEqual(bill.external_link, "https://example.com/fromage")
|
||||||
|
|
||||||
|
# add a bill with an invalid external link
|
||||||
|
resp = self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2015-05-06",
|
||||||
|
"what": "mauvais fromage à raclette",
|
||||||
|
"payer": members_ids[0],
|
||||||
|
"payed_for": members_ids,
|
||||||
|
"amount": "42000",
|
||||||
|
"external_link": "javascript:alert('Tu bluffes, Martoni.')",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertIn("Invalid URL", resp.data.decode("utf-8"))
|
||||||
|
|
||||||
def test_weighted_balance(self):
|
def test_weighted_balance(self):
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
|
|
||||||
|
@ -1009,6 +1082,8 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_export(self):
|
def test_export(self):
|
||||||
|
# Export a simple project without currencies
|
||||||
|
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
|
|
||||||
# add members
|
# add members
|
||||||
|
@ -1026,7 +1101,6 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2, 3, 4],
|
"payed_for": [1, 2, 3, 4],
|
||||||
"amount": "10.0",
|
"amount": "10.0",
|
||||||
"original_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1038,7 +1112,6 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 3],
|
"payed_for": [1, 3],
|
||||||
"amount": "200",
|
"amount": "200",
|
||||||
"original_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1050,13 +1123,464 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
"payer": 3,
|
"payer": 3,
|
||||||
"payed_for": [2],
|
"payed_for": [2],
|
||||||
"amount": "13.33",
|
"amount": "13.33",
|
||||||
"original_currency": "USD",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# generate json export of bills
|
# generate json export of bills
|
||||||
resp = self.client.get("/raclette/export/bills.json")
|
resp = self.client.get("/raclette/export/bills.json")
|
||||||
expected = [
|
expected = [
|
||||||
|
{
|
||||||
|
"date": "2017-01-01",
|
||||||
|
"what": "refund",
|
||||||
|
"amount": 13.33,
|
||||||
|
"currency": "XXX",
|
||||||
|
"payer_name": "tata",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["fred"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "red wine",
|
||||||
|
"amount": 200.0,
|
||||||
|
"currency": "XXX",
|
||||||
|
"payer_name": "fred",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["zorglub", "tata"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "fromage \xe0 raclette",
|
||||||
|
"amount": 10.0,
|
||||||
|
"currency": "XXX",
|
||||||
|
"payer_name": "zorglub",
|
||||||
|
"payer_weight": 2.0,
|
||||||
|
"owers": ["zorglub", "fred", "tata", "p\xe9p\xe9"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
|
||||||
|
|
||||||
|
# generate csv export of bills
|
||||||
|
resp = self.client.get("/raclette/export/bills.csv")
|
||||||
|
expected = [
|
||||||
|
"date,what,amount,currency,payer_name,payer_weight,owers",
|
||||||
|
"2017-01-01,refund,XXX,13.33,tata,1.0,fred",
|
||||||
|
'2016-12-31,red wine,XXX,200.0,fred,1.0,"zorglub, tata"',
|
||||||
|
'2016-12-31,fromage à raclette,10.0,XXX,zorglub,2.0,"zorglub, fred, tata, pépé"',
|
||||||
|
]
|
||||||
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
for i, line in enumerate(expected):
|
||||||
|
self.assertEqual(
|
||||||
|
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
|
||||||
|
)
|
||||||
|
|
||||||
|
# generate json export of transactions
|
||||||
|
resp = self.client.get("/raclette/export/transactions.json")
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
"amount": 2.00,
|
||||||
|
"currency": "XXX",
|
||||||
|
"receiver": "fred",
|
||||||
|
"ower": "p\xe9p\xe9",
|
||||||
|
},
|
||||||
|
{"amount": 55.34, "currency": "XXX", "receiver": "fred", "ower": "tata"},
|
||||||
|
{
|
||||||
|
"amount": 127.33,
|
||||||
|
"currency": "XXX",
|
||||||
|
"receiver": "fred",
|
||||||
|
"ower": "zorglub",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
|
||||||
|
|
||||||
|
# generate csv export of transactions
|
||||||
|
resp = self.client.get("/raclette/export/transactions.csv")
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
"amount,currency,receiver,ower",
|
||||||
|
"2.0,XXX,fred,pépé",
|
||||||
|
"55.34,XXX,fred,tata",
|
||||||
|
"127.33,XXX,fred,zorglub",
|
||||||
|
]
|
||||||
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
for i, line in enumerate(expected):
|
||||||
|
self.assertEqual(
|
||||||
|
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
|
||||||
|
)
|
||||||
|
|
||||||
|
# wrong export_format should return a 404
|
||||||
|
resp = self.client.get("/raclette/export/transactions.wrong")
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
def test_export_with_currencies(self):
|
||||||
|
self.post_project("raclette", default_currency="EUR")
|
||||||
|
|
||||||
|
# add members
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "fred"})
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "tata"})
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "pépé"})
|
||||||
|
|
||||||
|
# create bills
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "fromage à raclette",
|
||||||
|
"payer": 1,
|
||||||
|
"payed_for": [1, 2, 3, 4],
|
||||||
|
"amount": "10.0",
|
||||||
|
"original_currency": "EUR",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "poutine from Québec",
|
||||||
|
"payer": 2,
|
||||||
|
"payed_for": [1, 3],
|
||||||
|
"amount": "100",
|
||||||
|
"original_currency": "CAD",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2017-01-01",
|
||||||
|
"what": "refund",
|
||||||
|
"payer": 3,
|
||||||
|
"payed_for": [2],
|
||||||
|
"amount": "13.33",
|
||||||
|
"original_currency": "EUR",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# generate json export of bills
|
||||||
|
resp = self.client.get("/raclette/export/bills.json")
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
"date": "2017-01-01",
|
||||||
|
"what": "refund",
|
||||||
|
"amount": 13.33,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "tata",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["fred"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "poutine from Qu\xe9bec",
|
||||||
|
"amount": 100.0,
|
||||||
|
"currency": "CAD",
|
||||||
|
"payer_name": "fred",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["zorglub", "tata"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "fromage \xe0 raclette",
|
||||||
|
"amount": 10.0,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "zorglub",
|
||||||
|
"payer_weight": 2.0,
|
||||||
|
"owers": ["zorglub", "fred", "tata", "p\xe9p\xe9"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
|
||||||
|
|
||||||
|
# generate csv export of bills
|
||||||
|
resp = self.client.get("/raclette/export/bills.csv")
|
||||||
|
expected = [
|
||||||
|
"date,what,amount,currency,payer_name,payer_weight,owers",
|
||||||
|
"2017-01-01,refund,13.33,EUR,tata,1.0,fred",
|
||||||
|
'2016-12-31,poutine from Québec,100.0,CAD,fred,1.0,"zorglub, tata"',
|
||||||
|
'2016-12-31,fromage à raclette,10.0,EUR,zorglub,2.0,"zorglub, fred, tata, pépé"',
|
||||||
|
]
|
||||||
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
for i, line in enumerate(expected):
|
||||||
|
self.assertEqual(
|
||||||
|
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
|
||||||
|
)
|
||||||
|
|
||||||
|
# generate json export of transactions (in EUR!)
|
||||||
|
resp = self.client.get("/raclette/export/transactions.json")
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
"amount": 2.00,
|
||||||
|
"currency": "EUR",
|
||||||
|
"receiver": "fred",
|
||||||
|
"ower": "p\xe9p\xe9",
|
||||||
|
},
|
||||||
|
{"amount": 10.89, "currency": "EUR", "receiver": "fred", "ower": "tata"},
|
||||||
|
{"amount": 38.45, "currency": "EUR", "receiver": "fred", "ower": "zorglub"},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
|
||||||
|
|
||||||
|
# generate csv export of transactions
|
||||||
|
resp = self.client.get("/raclette/export/transactions.csv")
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
"amount,currency,receiver,ower",
|
||||||
|
"2.0,EUR,fred,pépé",
|
||||||
|
"10.89,EUR,fred,tata",
|
||||||
|
"38.45,EUR,fred,zorglub",
|
||||||
|
]
|
||||||
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
for i, line in enumerate(expected):
|
||||||
|
self.assertEqual(
|
||||||
|
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change project currency to CAD
|
||||||
|
project = models.Project.query.get("raclette")
|
||||||
|
project.switch_currency("CAD")
|
||||||
|
|
||||||
|
# generate json export of transactions (now in CAD!)
|
||||||
|
resp = self.client.get("/raclette/export/transactions.json")
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
"amount": 3.00,
|
||||||
|
"currency": "CAD",
|
||||||
|
"receiver": "fred",
|
||||||
|
"ower": "p\xe9p\xe9",
|
||||||
|
},
|
||||||
|
{"amount": 16.34, "currency": "CAD", "receiver": "fred", "ower": "tata"},
|
||||||
|
{"amount": 57.67, "currency": "CAD", "receiver": "fred", "ower": "zorglub"},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
|
||||||
|
|
||||||
|
# generate csv export of transactions
|
||||||
|
resp = self.client.get("/raclette/export/transactions.csv")
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
"amount,currency,receiver,ower",
|
||||||
|
"3.0,CAD,fred,pépé",
|
||||||
|
"16.34,CAD,fred,tata",
|
||||||
|
"57.67,CAD,fred,zorglub",
|
||||||
|
]
|
||||||
|
received_lines = resp.data.decode("utf-8").split("\n")
|
||||||
|
|
||||||
|
for i, line in enumerate(expected):
|
||||||
|
self.assertEqual(
|
||||||
|
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_import_currencies_in_empty_project_with_currency(self):
|
||||||
|
# Import JSON with currencies in an empty project with a default currency
|
||||||
|
|
||||||
|
self.post_project("raclette", default_currency="EUR")
|
||||||
|
self.login("raclette")
|
||||||
|
|
||||||
|
project = models.Project.query.get("raclette")
|
||||||
|
|
||||||
|
json_to_import = [
|
||||||
|
{
|
||||||
|
"date": "2017-01-01",
|
||||||
|
"what": "refund",
|
||||||
|
"amount": 13.33,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "tata",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["fred"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "poutine from québec",
|
||||||
|
"amount": 50.0,
|
||||||
|
"currency": "CAD",
|
||||||
|
"payer_name": "fred",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["zorglub", "tata"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "fromage a raclette",
|
||||||
|
"amount": 10.0,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "zorglub",
|
||||||
|
"payer_weight": 2.0,
|
||||||
|
"owers": ["zorglub", "fred", "tata", "pepe"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
from ihatemoney.web import import_project
|
||||||
|
|
||||||
|
file = io.StringIO()
|
||||||
|
json.dump(json_to_import, file)
|
||||||
|
file.seek(0)
|
||||||
|
import_project(file, project)
|
||||||
|
|
||||||
|
bills = project.get_pretty_bills()
|
||||||
|
|
||||||
|
# Check if all bills have been added
|
||||||
|
self.assertEqual(len(bills), len(json_to_import))
|
||||||
|
|
||||||
|
# Check if name of bills are ok
|
||||||
|
b = [e["what"] for e in bills]
|
||||||
|
b.sort()
|
||||||
|
ref = [e["what"] for e in json_to_import]
|
||||||
|
ref.sort()
|
||||||
|
|
||||||
|
self.assertEqual(b, ref)
|
||||||
|
|
||||||
|
# Check if other informations in bill are ok
|
||||||
|
for i in json_to_import:
|
||||||
|
for j in bills:
|
||||||
|
if j["what"] == i["what"]:
|
||||||
|
self.assertEqual(j["payer_name"], i["payer_name"])
|
||||||
|
self.assertEqual(j["amount"], i["amount"])
|
||||||
|
self.assertEqual(j["currency"], i["currency"])
|
||||||
|
self.assertEqual(j["payer_weight"], i["payer_weight"])
|
||||||
|
self.assertEqual(j["date"], i["date"])
|
||||||
|
|
||||||
|
list_project = [ower for ower in j["owers"]]
|
||||||
|
list_project.sort()
|
||||||
|
list_json = [ower for ower in i["owers"]]
|
||||||
|
list_json.sort()
|
||||||
|
|
||||||
|
self.assertEqual(list_project, list_json)
|
||||||
|
|
||||||
|
def test_import_single_currency_in_empty_project_without_currency(self):
|
||||||
|
# Import JSON with a single currency in an empty project with no
|
||||||
|
# default currency. It should work by stripping the currency from
|
||||||
|
# bills.
|
||||||
|
|
||||||
|
self.post_project("raclette")
|
||||||
|
self.login("raclette")
|
||||||
|
|
||||||
|
project = models.Project.query.get("raclette")
|
||||||
|
|
||||||
|
json_to_import = [
|
||||||
|
{
|
||||||
|
"date": "2017-01-01",
|
||||||
|
"what": "refund",
|
||||||
|
"amount": 13.33,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "tata",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["fred"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "fromage a raclette",
|
||||||
|
"amount": 10.0,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "zorglub",
|
||||||
|
"payer_weight": 2.0,
|
||||||
|
"owers": ["zorglub", "fred", "tata", "pepe"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
from ihatemoney.web import import_project
|
||||||
|
|
||||||
|
file = io.StringIO()
|
||||||
|
json.dump(json_to_import, file)
|
||||||
|
file.seek(0)
|
||||||
|
import_project(file, project)
|
||||||
|
|
||||||
|
bills = project.get_pretty_bills()
|
||||||
|
|
||||||
|
# Check if all bills have been added
|
||||||
|
self.assertEqual(len(bills), len(json_to_import))
|
||||||
|
|
||||||
|
# Check if name of bills are ok
|
||||||
|
b = [e["what"] for e in bills]
|
||||||
|
b.sort()
|
||||||
|
ref = [e["what"] for e in json_to_import]
|
||||||
|
ref.sort()
|
||||||
|
|
||||||
|
self.assertEqual(b, ref)
|
||||||
|
|
||||||
|
# Check if other informations in bill are ok
|
||||||
|
for i in json_to_import:
|
||||||
|
for j in bills:
|
||||||
|
if j["what"] == i["what"]:
|
||||||
|
self.assertEqual(j["payer_name"], i["payer_name"])
|
||||||
|
self.assertEqual(j["amount"], i["amount"])
|
||||||
|
# Currency should have been stripped
|
||||||
|
self.assertEqual(j["currency"], "XXX")
|
||||||
|
self.assertEqual(j["payer_weight"], i["payer_weight"])
|
||||||
|
self.assertEqual(j["date"], i["date"])
|
||||||
|
|
||||||
|
list_project = [ower for ower in j["owers"]]
|
||||||
|
list_project.sort()
|
||||||
|
list_json = [ower for ower in i["owers"]]
|
||||||
|
list_json.sort()
|
||||||
|
|
||||||
|
self.assertEqual(list_project, list_json)
|
||||||
|
|
||||||
|
def test_import_multiple_currencies_in_empty_project_without_currency(self):
|
||||||
|
# Import JSON with multiple currencies in an empty project with no
|
||||||
|
# default currency. It should fail.
|
||||||
|
|
||||||
|
self.post_project("raclette")
|
||||||
|
self.login("raclette")
|
||||||
|
|
||||||
|
project = models.Project.query.get("raclette")
|
||||||
|
|
||||||
|
json_to_import = [
|
||||||
|
{
|
||||||
|
"date": "2017-01-01",
|
||||||
|
"what": "refund",
|
||||||
|
"amount": 13.33,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "tata",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["fred"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "poutine from québec",
|
||||||
|
"amount": 50.0,
|
||||||
|
"currency": "CAD",
|
||||||
|
"payer_name": "fred",
|
||||||
|
"payer_weight": 1.0,
|
||||||
|
"owers": ["zorglub", "tata"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2016-12-31",
|
||||||
|
"what": "fromage a raclette",
|
||||||
|
"amount": 10.0,
|
||||||
|
"currency": "EUR",
|
||||||
|
"payer_name": "zorglub",
|
||||||
|
"payer_weight": 2.0,
|
||||||
|
"owers": ["zorglub", "fred", "tata", "pepe"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
from ihatemoney.web import import_project
|
||||||
|
|
||||||
|
file = io.StringIO()
|
||||||
|
json.dump(json_to_import, file)
|
||||||
|
file.seek(0)
|
||||||
|
# Import should fail
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
import_project(file, project)
|
||||||
|
|
||||||
|
bills = project.get_pretty_bills()
|
||||||
|
|
||||||
|
# Check that there are no bills
|
||||||
|
self.assertEqual(len(bills), 0)
|
||||||
|
|
||||||
|
def test_import_no_currency_in_empty_project_with_currency(self):
|
||||||
|
# Import JSON without currencies (from ihatemoney < 5) in an empty
|
||||||
|
# project with a default currency.
|
||||||
|
|
||||||
|
self.post_project("raclette", default_currency="EUR")
|
||||||
|
self.login("raclette")
|
||||||
|
|
||||||
|
project = models.Project.query.get("raclette")
|
||||||
|
|
||||||
|
json_to_import = [
|
||||||
{
|
{
|
||||||
"date": "2017-01-01",
|
"date": "2017-01-01",
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
|
@ -1075,62 +1599,55 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "fromage \xe0 raclette",
|
"what": "fromage a raclette",
|
||||||
"amount": 10.0,
|
"amount": 10.0,
|
||||||
"payer_name": "zorglub",
|
"payer_name": "zorglub",
|
||||||
"payer_weight": 2.0,
|
"payer_weight": 2.0,
|
||||||
"owers": ["zorglub", "fred", "tata", "p\xe9p\xe9"],
|
"owers": ["zorglub", "fred", "tata", "pepe"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
|
|
||||||
|
|
||||||
# generate csv export of bills
|
from ihatemoney.web import import_project
|
||||||
resp = self.client.get("/raclette/export/bills.csv")
|
|
||||||
expected = [
|
|
||||||
"date,what,amount,payer_name,payer_weight,owers",
|
|
||||||
"2017-01-01,refund,13.33,tata,1.0,fred",
|
|
||||||
'2016-12-31,red wine,200.0,fred,1.0,"zorglub, tata"',
|
|
||||||
'2016-12-31,fromage à raclette,10.0,zorglub,2.0,"zorglub, fred, tata, pépé"',
|
|
||||||
]
|
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
|
||||||
|
|
||||||
for i, line in enumerate(expected):
|
file = io.StringIO()
|
||||||
self.assertEqual(
|
json.dump(json_to_import, file)
|
||||||
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
|
file.seek(0)
|
||||||
)
|
import_project(file, project)
|
||||||
|
|
||||||
# generate json export of transactions
|
bills = project.get_pretty_bills()
|
||||||
resp = self.client.get("/raclette/export/transactions.json")
|
|
||||||
expected = [
|
|
||||||
{"amount": 2.00, "receiver": "fred", "ower": "p\xe9p\xe9"},
|
|
||||||
{"amount": 55.34, "receiver": "fred", "ower": "tata"},
|
|
||||||
{"amount": 127.33, "receiver": "fred", "ower": "zorglub"},
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
|
# Check if all bills have been added
|
||||||
|
self.assertEqual(len(bills), len(json_to_import))
|
||||||
|
|
||||||
# generate csv export of transactions
|
# Check if name of bills are ok
|
||||||
resp = self.client.get("/raclette/export/transactions.csv")
|
b = [e["what"] for e in bills]
|
||||||
|
b.sort()
|
||||||
|
ref = [e["what"] for e in json_to_import]
|
||||||
|
ref.sort()
|
||||||
|
|
||||||
expected = [
|
self.assertEqual(b, ref)
|
||||||
"amount,receiver,ower",
|
|
||||||
"2.0,fred,pépé",
|
|
||||||
"55.34,fred,tata",
|
|
||||||
"127.33,fred,zorglub",
|
|
||||||
]
|
|
||||||
received_lines = resp.data.decode("utf-8").split("\n")
|
|
||||||
|
|
||||||
for i, line in enumerate(expected):
|
# Check if other informations in bill are ok
|
||||||
self.assertEqual(
|
for i in json_to_import:
|
||||||
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
|
for j in bills:
|
||||||
)
|
if j["what"] == i["what"]:
|
||||||
|
self.assertEqual(j["payer_name"], i["payer_name"])
|
||||||
|
self.assertEqual(j["amount"], i["amount"])
|
||||||
|
# All bills are converted to default project currency
|
||||||
|
self.assertEqual(j["currency"], "EUR")
|
||||||
|
self.assertEqual(j["payer_weight"], i["payer_weight"])
|
||||||
|
self.assertEqual(j["date"], i["date"])
|
||||||
|
|
||||||
# wrong export_format should return a 404
|
list_project = [ower for ower in j["owers"]]
|
||||||
resp = self.client.get("/raclette/export/transactions.wrong")
|
list_project.sort()
|
||||||
self.assertEqual(resp.status_code, 404)
|
list_json = [ower for ower in i["owers"]]
|
||||||
|
list_json.sort()
|
||||||
|
|
||||||
def test_import_new_project(self):
|
self.assertEqual(list_project, list_json)
|
||||||
# Import JSON in an empty project
|
|
||||||
|
def test_import_no_currency_in_empty_project_without_currency(self):
|
||||||
|
# Import JSON without currencies (from ihatemoney < 5) in an empty
|
||||||
|
# project with no default currency.
|
||||||
|
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
self.login("raclette")
|
self.login("raclette")
|
||||||
|
@ -1173,7 +1690,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
bills = project.get_pretty_bills()
|
bills = project.get_pretty_bills()
|
||||||
|
|
||||||
# Check if all bills has been add
|
# Check if all bills have been added
|
||||||
self.assertEqual(len(bills), len(json_to_import))
|
self.assertEqual(len(bills), len(json_to_import))
|
||||||
|
|
||||||
# Check if name of bills are ok
|
# Check if name of bills are ok
|
||||||
|
@ -1190,6 +1707,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
if j["what"] == i["what"]:
|
if j["what"] == i["what"]:
|
||||||
self.assertEqual(j["payer_name"], i["payer_name"])
|
self.assertEqual(j["payer_name"], i["payer_name"])
|
||||||
self.assertEqual(j["amount"], i["amount"])
|
self.assertEqual(j["amount"], i["amount"])
|
||||||
|
self.assertEqual(j["currency"], "XXX")
|
||||||
self.assertEqual(j["payer_weight"], i["payer_weight"])
|
self.assertEqual(j["payer_weight"], i["payer_weight"])
|
||||||
self.assertEqual(j["date"], i["date"])
|
self.assertEqual(j["date"], i["date"])
|
||||||
|
|
||||||
|
@ -1227,6 +1745,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
"date": "2017-01-01",
|
"date": "2017-01-01",
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
"amount": 13.33,
|
"amount": 13.33,
|
||||||
|
"currency": "XXX",
|
||||||
"payer_name": "tata",
|
"payer_name": "tata",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
"owers": ["fred"],
|
"owers": ["fred"],
|
||||||
|
@ -1235,6 +1754,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"amount": 200.0,
|
"amount": 200.0,
|
||||||
|
"currency": "XXX",
|
||||||
"payer_name": "fred",
|
"payer_name": "fred",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
"owers": ["zorglub", "tata"],
|
"owers": ["zorglub", "tata"],
|
||||||
|
@ -1243,6 +1763,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "fromage a raclette",
|
"what": "fromage a raclette",
|
||||||
"amount": 10.0,
|
"amount": 10.0,
|
||||||
|
"currency": "XXX",
|
||||||
"payer_name": "zorglub",
|
"payer_name": "zorglub",
|
||||||
"payer_weight": 2.0,
|
"payer_weight": 2.0,
|
||||||
"owers": ["zorglub", "fred", "tata", "pepe"],
|
"owers": ["zorglub", "fred", "tata", "pepe"],
|
||||||
|
@ -1258,7 +1779,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
bills = project.get_pretty_bills()
|
bills = project.get_pretty_bills()
|
||||||
|
|
||||||
# Check if all bills has been add
|
# Check if all bills have been added
|
||||||
self.assertEqual(len(bills), len(json_to_import))
|
self.assertEqual(len(bills), len(json_to_import))
|
||||||
|
|
||||||
# Check if name of bills are ok
|
# Check if name of bills are ok
|
||||||
|
@ -1275,6 +1796,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
if j["what"] == i["what"]:
|
if j["what"] == i["what"]:
|
||||||
self.assertEqual(j["payer_name"], i["payer_name"])
|
self.assertEqual(j["payer_name"], i["payer_name"])
|
||||||
self.assertEqual(j["amount"], i["amount"])
|
self.assertEqual(j["amount"], i["amount"])
|
||||||
|
self.assertEqual(j["currency"], i["currency"])
|
||||||
self.assertEqual(j["payer_weight"], i["payer_weight"])
|
self.assertEqual(j["payer_weight"], i["payer_weight"])
|
||||||
self.assertEqual(j["date"], i["date"])
|
self.assertEqual(j["date"], i["date"])
|
||||||
|
|
||||||
|
@ -1314,29 +1836,12 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
from ihatemoney.web import import_project
|
from ihatemoney.web import import_project
|
||||||
|
|
||||||
try:
|
for data in [json_1, json_2]:
|
||||||
file = io.StringIO()
|
file = io.StringIO()
|
||||||
json.dump(json_1, file)
|
json.dump(data, file)
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
import_project(file, project)
|
with pytest.raises(ValueError):
|
||||||
except ValueError:
|
import_project(file, project)
|
||||||
self.assertTrue(True)
|
|
||||||
except Exception:
|
|
||||||
self.fail("unexpected exception raised")
|
|
||||||
else:
|
|
||||||
self.fail("ExpectedException not raised")
|
|
||||||
|
|
||||||
try:
|
|
||||||
file = io.StringIO()
|
|
||||||
json.dump(json_2, file)
|
|
||||||
file.seek(0)
|
|
||||||
import_project(file, project)
|
|
||||||
except ValueError:
|
|
||||||
self.assertTrue(True)
|
|
||||||
except Exception:
|
|
||||||
self.fail("unexpected exception raised")
|
|
||||||
else:
|
|
||||||
self.fail("ExpectedException not raised")
|
|
||||||
|
|
||||||
def test_access_other_projects(self):
|
def test_access_other_projects(self):
|
||||||
"""Test that accessing or editing bills and members from another project fails"""
|
"""Test that accessing or editing bills and members from another project fails"""
|
||||||
|
@ -1464,10 +1969,6 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
def test_currency_switch(self):
|
def test_currency_switch(self):
|
||||||
|
|
||||||
mock_data = {"USD": 1, "EUR": 0.8, "CAD": 1.2, CurrencyConverter.no_currency: 1}
|
|
||||||
converter = CurrencyConverter()
|
|
||||||
converter.get_rates = MagicMock(return_value=mock_data)
|
|
||||||
|
|
||||||
# A project should be editable
|
# A project should be editable
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
|
|
||||||
|
@ -1559,14 +2060,16 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
last_bill = project.get_bills().first()
|
last_bill = project.get_bills().first()
|
||||||
expected_amount = converter.exchange_currency(last_bill.amount, "CAD", "EUR")
|
expected_amount = self.converter.exchange_currency(
|
||||||
|
last_bill.amount, "CAD", "EUR"
|
||||||
|
)
|
||||||
assert last_bill.converted_amount == expected_amount
|
assert last_bill.converted_amount == expected_amount
|
||||||
|
|
||||||
# Switch to USD. Now, NO bill should be in USD, since they already had a currency
|
# Switch to USD. Now, NO bill should be in USD, since they already had a currency
|
||||||
project.switch_currency("USD")
|
project.switch_currency("USD")
|
||||||
for bill in project.get_bills():
|
for bill in project.get_bills():
|
||||||
assert bill.original_currency != "USD"
|
assert bill.original_currency != "USD"
|
||||||
expected_amount = converter.exchange_currency(
|
expected_amount = self.converter.exchange_currency(
|
||||||
bill.amount, bill.original_currency, "USD"
|
bill.amount, bill.original_currency, "USD"
|
||||||
)
|
)
|
||||||
assert bill.converted_amount == expected_amount
|
assert bill.converted_amount == expected_amount
|
||||||
|
@ -1583,7 +2086,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
"password": "demo",
|
"password": "demo",
|
||||||
"contact_email": "demo@notmyidea.org",
|
"contact_email": "demo@notmyidea.org",
|
||||||
"project_history": "y",
|
"project_history": "y",
|
||||||
"default_currency": converter.no_currency,
|
"default_currency": CurrencyConverter.no_currency,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# A user displayed error should be generated, and its currency should be the same.
|
# A user displayed error should be generated, and its currency should be the same.
|
||||||
|
@ -1593,10 +2096,6 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
def test_currency_switch_to_bill_currency(self):
|
def test_currency_switch_to_bill_currency(self):
|
||||||
|
|
||||||
mock_data = {"USD": 1, "EUR": 0.8, "CAD": 1.2, CurrencyConverter.no_currency: 1}
|
|
||||||
converter = CurrencyConverter()
|
|
||||||
converter.get_rates = MagicMock(return_value=mock_data)
|
|
||||||
|
|
||||||
# Default currency is 'XXX', but we should start from a project with a currency
|
# Default currency is 'XXX', but we should start from a project with a currency
|
||||||
self.post_project("raclette", default_currency="USD")
|
self.post_project("raclette", default_currency="USD")
|
||||||
|
|
||||||
|
@ -1620,7 +2119,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
project = models.Project.query.get("raclette")
|
project = models.Project.query.get("raclette")
|
||||||
|
|
||||||
bill = project.get_bills().first()
|
bill = project.get_bills().first()
|
||||||
assert bill.converted_amount == converter.exchange_currency(
|
assert bill.converted_amount == self.converter.exchange_currency(
|
||||||
bill.amount, "EUR", "USD"
|
bill.amount, "EUR", "USD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1631,10 +2130,6 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
|
|
||||||
def test_currency_switch_to_no_currency(self):
|
def test_currency_switch_to_no_currency(self):
|
||||||
|
|
||||||
mock_data = {"USD": 1, "EUR": 0.8, "CAD": 1.2, CurrencyConverter.no_currency: 1}
|
|
||||||
converter = CurrencyConverter()
|
|
||||||
converter.get_rates = MagicMock(return_value=mock_data)
|
|
||||||
|
|
||||||
# Default currency is 'XXX', but we should start from a project with a currency
|
# Default currency is 'XXX', but we should start from a project with a currency
|
||||||
self.post_project("raclette", default_currency="USD")
|
self.post_project("raclette", default_currency="USD")
|
||||||
|
|
||||||
|
@ -1670,17 +2165,40 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
project = models.Project.query.get("raclette")
|
project = models.Project.query.get("raclette")
|
||||||
|
|
||||||
for bill in project.get_bills_unordered():
|
for bill in project.get_bills_unordered():
|
||||||
assert bill.converted_amount == converter.exchange_currency(
|
assert bill.converted_amount == self.converter.exchange_currency(
|
||||||
bill.amount, "EUR", "USD"
|
bill.amount, "EUR", "USD"
|
||||||
)
|
)
|
||||||
|
|
||||||
# And switch project to no currency: amount should be equal to what was submitted
|
# And switch project to no currency: amount should be equal to what was submitted
|
||||||
project.switch_currency(converter.no_currency)
|
project.switch_currency(CurrencyConverter.no_currency)
|
||||||
no_currency_bills = [
|
no_currency_bills = [
|
||||||
(bill.amount, bill.converted_amount) for bill in project.get_bills()
|
(bill.amount, bill.converted_amount) for bill in project.get_bills()
|
||||||
]
|
]
|
||||||
assert no_currency_bills == [(5.0, 5.0), (10.0, 10.0)]
|
assert no_currency_bills == [(5.0, 5.0), (10.0, 10.0)]
|
||||||
|
|
||||||
|
def test_decimals_on_weighted_members_list(self):
|
||||||
|
|
||||||
|
self.post_project("raclette")
|
||||||
|
|
||||||
|
# add three users with different weights
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/members/add", data={"name": "zorglub", "weight": 1.0}
|
||||||
|
)
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "tata", "weight": 1.10})
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "fred", "weight": 1.15})
|
||||||
|
|
||||||
|
# check if weights of the users are 1, 1.1, 1.15 respectively
|
||||||
|
resp = self.client.get("/raclette/")
|
||||||
|
self.assertIn(
|
||||||
|
'zorglub<span class="light">(x1)</span>', resp.data.decode("utf-8")
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
'tata<span class="light">(x1.1)</span>', resp.data.decode("utf-8")
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
'fred<span class="light">(x1.15)</span>', resp.data.decode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
|
||||||
def em_surround(string, regex_escape=False):
|
def em_surround(string, regex_escape=False):
|
||||||
if regex_escape:
|
if regex_escape:
|
||||||
return r'<em class="font-italic">%s<\/em>' % string
|
return r'<em class="font-italic">%s<\/em>' % string
|
||||||
else:
|
else:
|
||||||
return '<em class="font-italic">%s</em>' % string
|
return '<em class="font-italic">%s</em>' % string
|
||||||
|
|
||||||
|
|
||||||
|
def extract_link(data, start_prefix):
|
||||||
|
base_index = data.find(start_prefix)
|
||||||
|
start = data.find('href="', base_index) + 6
|
||||||
|
end = data.find('">', base_index)
|
||||||
|
link = Markup(data[start:end]).unescape()
|
||||||
|
return link
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import os
|
import os
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from flask_testing import TestCase
|
from flask_testing import TestCase
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from ihatemoney import models
|
from ihatemoney import models
|
||||||
|
from ihatemoney.currency_convertor import CurrencyConverter
|
||||||
from ihatemoney.run import create_app, db
|
from ihatemoney.run import create_app, db
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +15,7 @@ class BaseTestCase(TestCase):
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
"TESTING_SQLALCHEMY_DATABASE_URI", "sqlite://"
|
"TESTING_SQLALCHEMY_DATABASE_URI", "sqlite://"
|
||||||
)
|
)
|
||||||
|
ENABLE_CAPTCHA = False
|
||||||
|
|
||||||
def create_app(self):
|
def create_app(self):
|
||||||
# Pass the test object as a configuration.
|
# Pass the test object as a configuration.
|
||||||
|
@ -20,6 +23,18 @@ class BaseTestCase(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
# Add dummy data to CurrencyConverter for all tests (since it's a singleton)
|
||||||
|
mock_data = {
|
||||||
|
"USD": 1,
|
||||||
|
"EUR": 0.8,
|
||||||
|
"CAD": 1.2,
|
||||||
|
"PLN": 4,
|
||||||
|
CurrencyConverter.no_currency: 1,
|
||||||
|
}
|
||||||
|
converter = CurrencyConverter()
|
||||||
|
converter.get_rates = MagicMock(return_value=mock_data)
|
||||||
|
# Also add it to an attribute to make tests clearer
|
||||||
|
self.converter = converter
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# clean after testing
|
# clean after testing
|
||||||
|
|
|
@ -31,6 +31,7 @@ class ConfigurationTestCase(BaseTestCase):
|
||||||
self.assertTrue(self.app.config["ACTIVATE_DEMO_PROJECT"])
|
self.assertTrue(self.app.config["ACTIVATE_DEMO_PROJECT"])
|
||||||
self.assertTrue(self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"])
|
self.assertTrue(self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"])
|
||||||
self.assertFalse(self.app.config["ACTIVATE_ADMIN_DASHBOARD"])
|
self.assertFalse(self.app.config["ACTIVATE_ADMIN_DASHBOARD"])
|
||||||
|
self.assertFalse(self.app.config["ENABLE_CAPTCHA"])
|
||||||
|
|
||||||
def test_env_var_configuration_file(self):
|
def test_env_var_configuration_file(self):
|
||||||
"""Test that settings are loaded from a configuration file specified
|
"""Test that settings are loaded from a configuration file specified
|
||||||
|
@ -241,9 +242,59 @@ class EmailFailureTestCase(IhatemoneyTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CaptchaTestCase(IhatemoneyTestCase):
|
||||||
|
ENABLE_CAPTCHA = True
|
||||||
|
|
||||||
|
def test_project_creation_with_captcha(self):
|
||||||
|
with self.app.test_client() as c:
|
||||||
|
c.post(
|
||||||
|
"/create",
|
||||||
|
data={
|
||||||
|
"name": "raclette party",
|
||||||
|
"id": "raclette",
|
||||||
|
"password": "party",
|
||||||
|
"contact_email": "raclette@notmyidea.org",
|
||||||
|
"default_currency": "USD",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(models.Project.query.all()) == 0
|
||||||
|
|
||||||
|
c.post(
|
||||||
|
"/create",
|
||||||
|
data={
|
||||||
|
"name": "raclette party",
|
||||||
|
"id": "raclette",
|
||||||
|
"password": "party",
|
||||||
|
"contact_email": "raclette@notmyidea.org",
|
||||||
|
"default_currency": "USD",
|
||||||
|
"captcha": "nope",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(models.Project.query.all()) == 0
|
||||||
|
|
||||||
|
c.post(
|
||||||
|
"/create",
|
||||||
|
data={
|
||||||
|
"name": "raclette party",
|
||||||
|
"id": "raclette",
|
||||||
|
"password": "party",
|
||||||
|
"contact_email": "raclette@notmyidea.org",
|
||||||
|
"default_currency": "USD",
|
||||||
|
"captcha": "euro",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(models.Project.query.all()) == 1
|
||||||
|
|
||||||
|
|
||||||
class TestCurrencyConverter(unittest.TestCase):
|
class TestCurrencyConverter(unittest.TestCase):
|
||||||
converter = CurrencyConverter()
|
converter = CurrencyConverter()
|
||||||
mock_data = {"USD": 1, "EUR": 0.8, "CAD": 1.2, CurrencyConverter.no_currency: 1}
|
mock_data = {
|
||||||
|
"USD": 1,
|
||||||
|
"EUR": 0.8,
|
||||||
|
"CAD": 1.2,
|
||||||
|
"PLN": 4,
|
||||||
|
CurrencyConverter.no_currency: 1,
|
||||||
|
}
|
||||||
converter.get_rates = MagicMock(return_value=mock_data)
|
converter.get_rates = MagicMock(return_value=mock_data)
|
||||||
|
|
||||||
def test_only_one_instance(self):
|
def test_only_one_instance(self):
|
||||||
|
@ -254,7 +305,7 @@ class TestCurrencyConverter(unittest.TestCase):
|
||||||
def test_get_currencies(self):
|
def test_get_currencies(self):
|
||||||
self.assertCountEqual(
|
self.assertCountEqual(
|
||||||
self.converter.get_currencies(),
|
self.converter.get_currencies(),
|
||||||
["USD", "EUR", "CAD", CurrencyConverter.no_currency],
|
["USD", "EUR", "CAD", "PLN", CurrencyConverter.no_currency],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_exchange_currency(self):
|
def test_exchange_currency(self):
|
||||||
|
|
|
@ -30,7 +30,7 @@ msgid "New private code"
|
||||||
msgstr "ব্যক্তিগত কোড"
|
msgstr "ব্যক্তিগত কোড"
|
||||||
|
|
||||||
msgid "Enter a new code if you want to change it"
|
msgid "Enter a new code if you want to change it"
|
||||||
msgstr ""
|
msgstr "আপনি যদি এটি পরিবর্তন করতে চান তবে একটি নতুন কোড লিখুন"
|
||||||
|
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "ইমেইল"
|
msgstr "ইমেইল"
|
||||||
|
@ -39,10 +39,10 @@ msgid "Enable project history"
|
||||||
msgstr "প্রকল্পের ইতিহাস সক্রিয় করো"
|
msgstr "প্রকল্পের ইতিহাস সক্রিয় করো"
|
||||||
|
|
||||||
msgid "Use IP tracking for project history"
|
msgid "Use IP tracking for project history"
|
||||||
msgstr ""
|
msgstr "প্রকল্পের ইতিহাসের জন্য আইপি ট্র্যাকিং ব্যবহার করুন"
|
||||||
|
|
||||||
msgid "Default Currency"
|
msgid "Default Currency"
|
||||||
msgstr ""
|
msgstr "ডিফল্ট মুদ্রা"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"This project cannot be set to 'no currency' because it contains bills in "
|
"This project cannot be set to 'no currency' because it contains bills in "
|
||||||
|
@ -50,19 +50,19 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Import previously exported JSON file"
|
msgid "Import previously exported JSON file"
|
||||||
msgstr ""
|
msgstr "পূর্বে রপ্তানি করা JSON ফাইল আমদানি করুন"
|
||||||
|
|
||||||
msgid "Import"
|
msgid "Import"
|
||||||
msgstr ""
|
msgstr "আমদানি"
|
||||||
|
|
||||||
msgid "Project identifier"
|
msgid "Project identifier"
|
||||||
msgstr ""
|
msgstr "প্রকল্প শনাক্তকারী"
|
||||||
|
|
||||||
msgid "Private code"
|
msgid "Private code"
|
||||||
msgstr "ব্যক্তিগত কোড"
|
msgstr "ব্যক্তিগত কোড"
|
||||||
|
|
||||||
msgid "Create the project"
|
msgid "Create the project"
|
||||||
msgstr ""
|
msgstr "প্রকল্প তৈরি করুন"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -71,20 +71,21 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enter private code to confirm deletion"
|
msgid "Enter private code to confirm deletion"
|
||||||
msgstr ""
|
msgstr "মুছে ফেলার জন্য ব্যক্তিগত কোড লিখুন"
|
||||||
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr ""
|
msgstr "অজানা ত্রুটি
|
||||||
|
"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Invalid private code."
|
msgid "Invalid private code."
|
||||||
msgstr "ব্যক্তিগত কোড"
|
msgstr "ব্যক্তিগত কোড"
|
||||||
|
|
||||||
msgid "Get in"
|
msgid "Get in"
|
||||||
msgstr ""
|
msgstr "ভিতরে আস"
|
||||||
|
|
||||||
msgid "Admin password"
|
msgid "Admin password"
|
||||||
msgstr ""
|
msgstr "অ্যাডমিন পাসওয়ার্ড"
|
||||||
|
|
||||||
msgid "Send me the code by email"
|
msgid "Send me the code by email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
Binary file not shown.
|
@ -1,18 +1,18 @@
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||||
"PO-Revision-Date: 2021-01-08 14:32+0000\n"
|
"PO-Revision-Date: 2021-09-05 13:34+0000\n"
|
||||||
"Last-Translator: Oliver Klimt <klimt.oliver@gmail.com>\n"
|
"Last-Translator: Clonewayx <fillip1@seznam.cz>\n"
|
||||||
|
"Language-Team: Czech <https://hosted.weblate.org/projects/i-hate-money/"
|
||||||
|
"i-hate-money/cs/>\n"
|
||||||
"Language: cs\n"
|
"Language: cs\n"
|
||||||
"Language-Team: Czech <https://hosted.weblate.org/projects/i-hate-money/i"
|
|
||||||
"-hate-money/cs/>\n"
|
|
||||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
|
|
||||||
"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"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
||||||
|
"X-Generator: Weblate 4.8.1-dev\n"
|
||||||
"Generated-By: Babel 2.9.0\n"
|
"Generated-By: Babel 2.9.0\n"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -23,12 +23,11 @@ msgstr "Neplatná částka nebo výraz. Pouze čísla a operátory + - * / jsou
|
||||||
msgid "Project name"
|
msgid "Project name"
|
||||||
msgstr "Název projektu"
|
msgstr "Název projektu"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "New private code"
|
msgid "New private code"
|
||||||
msgstr "Přístupový kód"
|
msgstr "Nový soukromý kód"
|
||||||
|
|
||||||
msgid "Enter a new code if you want to change it"
|
msgid "Enter a new code if you want to change it"
|
||||||
msgstr ""
|
msgstr "Pokud chcete provést změnu vložte nový kód"
|
||||||
|
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Email"
|
msgstr "Email"
|
||||||
|
@ -46,6 +45,8 @@ msgid ""
|
||||||
"This project cannot be set to 'no currency' because it contains bills in "
|
"This project cannot be set to 'no currency' because it contains bills in "
|
||||||
"multiple currencies."
|
"multiple currencies."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Tento projekt nemůže být nastaven na 'bez měny' protože obsahuje účty v "
|
||||||
|
"různých měnáh."
|
||||||
|
|
||||||
msgid "Import previously exported JSON file"
|
msgid "Import previously exported JSON file"
|
||||||
msgstr "Import exportovaného JSON souboru"
|
msgstr "Import exportovaného JSON souboru"
|
||||||
|
@ -71,14 +72,13 @@ msgstr ""
|
||||||
"nový identifikátor"
|
"nový identifikátor"
|
||||||
|
|
||||||
msgid "Enter private code to confirm deletion"
|
msgid "Enter private code to confirm deletion"
|
||||||
msgstr ""
|
msgstr "Potvrďte smazání vložením soukromého kódu"
|
||||||
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr ""
|
msgstr "Neznámá chyba"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Invalid private code."
|
msgid "Invalid private code."
|
||||||
msgstr "Přístupový kód"
|
msgstr "Neplatný soukromý přístupový kód."
|
||||||
|
|
||||||
msgid "Get in"
|
msgid "Get in"
|
||||||
msgstr "Vstoupit"
|
msgstr "Vstoupit"
|
||||||
|
@ -145,7 +145,7 @@ msgid "Name"
|
||||||
msgstr "Jméno"
|
msgstr "Jméno"
|
||||||
|
|
||||||
msgid "Weights should be positive"
|
msgid "Weights should be positive"
|
||||||
msgstr ""
|
msgstr "Váhy musí být kladné"
|
||||||
|
|
||||||
msgid "Weight"
|
msgid "Weight"
|
||||||
msgstr "Váha"
|
msgstr "Váha"
|
||||||
|
@ -171,11 +171,11 @@ msgstr "Toto (%(email)s) není validní e-mail"
|
||||||
|
|
||||||
#. List with two items only
|
#. List with two items only
|
||||||
msgid "{dual_object_0} and {dual_object_1}"
|
msgid "{dual_object_0} and {dual_object_1}"
|
||||||
msgstr ""
|
msgstr "{dual_object_0} a {dual_object_1}"
|
||||||
|
|
||||||
#. Last two items of a list with more than 3 items
|
#. Last two items of a list with more than 3 items
|
||||||
msgid "{previous_object}, and {end_object}"
|
msgid "{previous_object}, and {end_object}"
|
||||||
msgstr ""
|
msgstr "{previous_object}, a {end_object}"
|
||||||
|
|
||||||
#. Two items in a middle of a list with more than 5 objects
|
#. Two items in a middle of a list with more than 5 objects
|
||||||
msgid "{previous_object}, {next_object}"
|
msgid "{previous_object}, {next_object}"
|
||||||
|
@ -230,37 +230,42 @@ msgid ""
|
||||||
"instructions. Please check the email configuration of the server or "
|
"instructions. Please check the email configuration of the server or "
|
||||||
"contact the administrator."
|
"contact the administrator."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Omlouváme se, během odesílání emailu s instrukcemi pro obnovení hesla se "
|
||||||
|
"vyskytla chyba. Zkontrolujte si prosím nastavení vašeho emailového serveru "
|
||||||
|
"nebo kontaktujte administrátora."
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
msgid "No token provided"
|
msgid "No token provided"
|
||||||
msgstr ""
|
msgstr "Nebyl vložen klíč"
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
msgid "Invalid token"
|
msgid "Invalid token"
|
||||||
msgstr ""
|
msgstr "Neplatná klíč"
|
||||||
|
|
||||||
msgid "Unknown project"
|
msgid "Unknown project"
|
||||||
msgstr ""
|
msgstr "Neznámý projekt"
|
||||||
|
|
||||||
msgid "Password successfully reset."
|
msgid "Password successfully reset."
|
||||||
msgstr ""
|
msgstr "Heslo bylo úspěšné obnoveno."
|
||||||
|
|
||||||
msgid "Project successfully uploaded"
|
msgid "Project successfully uploaded"
|
||||||
msgstr ""
|
msgstr "Projekt byl úspěšně nahrán."
|
||||||
|
|
||||||
msgid "Invalid JSON"
|
msgid "Invalid JSON"
|
||||||
msgstr ""
|
msgstr "Neplatný JSON"
|
||||||
|
|
||||||
msgid "Project successfully deleted"
|
msgid "Project successfully deleted"
|
||||||
msgstr ""
|
msgstr "Projekt byl úspěšně smazán"
|
||||||
|
|
||||||
msgid "Error deleting project"
|
msgid "Error deleting project"
|
||||||
msgstr ""
|
msgstr "Nastala chyba při mazání projektu"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "You have been invited to share your expenses for %(project)s"
|
msgid "You have been invited to share your expenses for %(project)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Your invitations have been sent"
|
msgid "Your invitations have been sent"
|
||||||
msgstr ""
|
msgstr "Vaše pozvánka byla odeslána"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Sorry, there was an error while trying to send the invitation emails. "
|
"Sorry, there was an error while trying to send the invitation emails. "
|
||||||
|
|
Binary file not shown.
|
@ -3,8 +3,8 @@ msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||||
"PO-Revision-Date: 2021-08-19 20:34+0000\n"
|
"PO-Revision-Date: 2021-09-23 19:36+0000\n"
|
||||||
"Last-Translator: corny <nico.eckstein+weblate@gmail.com>\n"
|
"Last-Translator: Christian H. <sunrisechain@gmail.com>\n"
|
||||||
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/"
|
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/"
|
||||||
"i-hate-money/de/>\n"
|
"i-hate-money/de/>\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
|
@ -12,7 +12,7 @@ msgstr ""
|
||||||
"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"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 4.8-dev\n"
|
"X-Generator: Weblate 4.9-dev\n"
|
||||||
"Generated-By: Babel 2.9.0\n"
|
"Generated-By: Babel 2.9.0\n"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -74,7 +74,7 @@ msgstr ""
|
||||||
"wähle eine andere Kennung"
|
"wähle eine andere Kennung"
|
||||||
|
|
||||||
msgid "Enter private code to confirm deletion"
|
msgid "Enter private code to confirm deletion"
|
||||||
msgstr ""
|
msgstr "Geben Sie Ihren privaten Code ein, um die Löschung zu bestätigen"
|
||||||
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "Unbekannter Fehler"
|
msgstr "Unbekannter Fehler"
|
||||||
|
|
Binary file not shown.
|
@ -1,18 +1,18 @@
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||||
"PO-Revision-Date: 2021-07-10 15:34+0000\n"
|
"PO-Revision-Date: 2021-10-01 20:35+0000\n"
|
||||||
"Last-Translator: phlostically <phlostically@mailinator.com>\n"
|
"Last-Translator: phlostically <phlostically@mailinator.com>\n"
|
||||||
|
"Language-Team: Esperanto <https://hosted.weblate.org/projects/i-hate-money/"
|
||||||
|
"i-hate-money/eo/>\n"
|
||||||
"Language: eo\n"
|
"Language: eo\n"
|
||||||
"Language-Team: Esperanto <https://hosted.weblate.org/projects/i-hate-"
|
|
||||||
"money/i-hate-money/eo/>\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1\n"
|
|
||||||
"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"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
|
"X-Generator: Weblate 4.9-dev\n"
|
||||||
"Generated-By: Babel 2.9.0\n"
|
"Generated-By: Babel 2.9.0\n"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -76,13 +76,11 @@ msgstr ""
|
||||||
msgid "Enter private code to confirm deletion"
|
msgid "Enter private code to confirm deletion"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "Nekonata projekto"
|
msgstr "Nekonata eraro"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Invalid private code."
|
msgid "Invalid private code."
|
||||||
msgstr "Nova privata kodo"
|
msgstr "Nevalida privata kodo."
|
||||||
|
|
||||||
msgid "Get in"
|
msgid "Get in"
|
||||||
msgstr "Eniri"
|
msgstr "Eniri"
|
||||||
|
@ -175,30 +173,30 @@ msgstr "La retpoŝta adreso %(email)s ne validas"
|
||||||
|
|
||||||
#. List with two items only
|
#. List with two items only
|
||||||
msgid "{dual_object_0} and {dual_object_1}"
|
msgid "{dual_object_0} and {dual_object_1}"
|
||||||
msgstr ""
|
msgstr "{dual_object_0} kaj {dual_object_1}"
|
||||||
|
|
||||||
#. Last two items of a list with more than 3 items
|
#. Last two items of a list with more than 3 items
|
||||||
msgid "{previous_object}, and {end_object}"
|
msgid "{previous_object}, and {end_object}"
|
||||||
msgstr ""
|
msgstr "{previous_object} kaj {end_object}"
|
||||||
|
|
||||||
#. Two items in a middle of a list with more than 5 objects
|
#. Two items in a middle of a list with more than 5 objects
|
||||||
msgid "{previous_object}, {next_object}"
|
msgid "{previous_object}, {next_object}"
|
||||||
msgstr ""
|
msgstr "{previous_object}, {next_object}"
|
||||||
|
|
||||||
#. First two items of a list with more than 3 items
|
#. First two items of a list with more than 3 items
|
||||||
msgid "{start_object}, {next_object}"
|
msgid "{start_object}, {next_object}"
|
||||||
msgstr ""
|
msgstr "{start_object}, {next_object}"
|
||||||
|
|
||||||
msgid "No Currency"
|
msgid "No Currency"
|
||||||
msgstr "Neniu valuto"
|
msgstr "Neniu valuto"
|
||||||
|
|
||||||
#. Form error with only one error
|
#. Form error with only one error
|
||||||
msgid "{prefix}: {error}"
|
msgid "{prefix}: {error}"
|
||||||
msgstr ""
|
msgstr "{prefix}: {error}"
|
||||||
|
|
||||||
#. Form error with a list of errors
|
#. Form error with a list of errors
|
||||||
msgid "{prefix}:<br />{errors}"
|
msgid "{prefix}:<br />{errors}"
|
||||||
msgstr ""
|
msgstr "{prefix}:<br />{errors}"
|
||||||
|
|
||||||
msgid "Too many failed login attempts, please retry later."
|
msgid "Too many failed login attempts, please retry later."
|
||||||
msgstr "Tro da malsukcesaj provoj de salutado; bonvolu reprovi poste."
|
msgstr "Tro da malsukcesaj provoj de salutado; bonvolu reprovi poste."
|
||||||
|
@ -396,16 +394,14 @@ msgstr "Elŝuti programon por poŝaparato"
|
||||||
msgid "Get it on"
|
msgid "Get it on"
|
||||||
msgstr "Elŝuti ĝin ĉe"
|
msgstr "Elŝuti ĝin ĉe"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr "ĉu vi certas?"
|
msgstr "Ĉu vi certas?"
|
||||||
|
|
||||||
msgid "Edit project"
|
msgid "Edit project"
|
||||||
msgstr "Redakti projekton"
|
msgstr "Redakti projekton"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Delete project"
|
msgid "Delete project"
|
||||||
msgstr "Redakti projekton"
|
msgstr "Forviŝi projekton"
|
||||||
|
|
||||||
msgid "Import JSON"
|
msgid "Import JSON"
|
||||||
msgstr "Enporti JSON-dosieron"
|
msgstr "Enporti JSON-dosieron"
|
||||||
|
@ -453,7 +449,7 @@ msgid "Everyone"
|
||||||
msgstr "Ĉiuj"
|
msgstr "Ĉiuj"
|
||||||
|
|
||||||
msgid "No one"
|
msgid "No one"
|
||||||
msgstr ""
|
msgstr "Neniu"
|
||||||
|
|
||||||
msgid "More options"
|
msgid "More options"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
BIN
ihatemoney/translations/kn/LC_MESSAGES/messages.mo
Normal file
BIN
ihatemoney/translations/kn/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
881
ihatemoney/translations/kn/LC_MESSAGES/messages.po
Normal file
881
ihatemoney/translations/kn/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,881 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2021-10-13 09:18+0200\n"
|
||||||
|
"PO-Revision-Date: 2021-10-13 20:00+0000\n"
|
||||||
|
"Last-Translator: a-g-rao <athrigrao@gmail.com>\n"
|
||||||
|
"Language-Team: Kannada <https://hosted.weblate.org/projects/i-hate-money/"
|
||||||
|
"i-hate-money/kn/>\n"
|
||||||
|
"Language: kn\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||||
|
"X-Generator: Weblate 4.9-dev\n"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Not a valid amount or expression. Only numbers and + - * / operators are "
|
||||||
|
"accepted."
|
||||||
|
msgstr ""
|
||||||
|
"ಮೊತ್ತ ಅಥವಾ ಪದಕಂತೆ ಸರಿಯಿಲ್ಲ. ಸಂಖ್ಯೆ ಮತ್ತು +-*/ ಎಣಿಕೆಬಳಕಗಳನ್ನು ಮಾತ್ರ "
|
||||||
|
"ಸ್ವೀಕರಿಸಲಾಗುವುದು."
|
||||||
|
|
||||||
|
msgid "Project name"
|
||||||
|
msgstr "ಯೋಜನೆಯ ಹೆಸರು"
|
||||||
|
|
||||||
|
msgid "New private code"
|
||||||
|
msgstr "ಹೊಸ ಸಂಕೇತಪದ"
|
||||||
|
|
||||||
|
msgid "Enter a new code if you want to change it"
|
||||||
|
msgstr "ಸಂಕೇತಪದ ಬದಲಾಯಿಸಬೇಕೆಂದರೆ ನಮೂದಿಸಿ."
|
||||||
|
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "ಮಿನ್ನಂಚೆ"
|
||||||
|
|
||||||
|
msgid "Enable project history"
|
||||||
|
msgstr "ಯೋಜನೆ ಇತಿಹಾಸ ಸಕ್ರಿಯಗೊಳಿಸಿ"
|
||||||
|
|
||||||
|
msgid "Use IP tracking for project history"
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
msgid "Import"
|
||||||
|
msgstr "ಆಮದು"
|
||||||
|
|
||||||
|
msgid "Project identifier"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Private code"
|
||||||
|
msgstr "ಸಂಕೇತಪದ"
|
||||||
|
|
||||||
|
msgid "Create the project"
|
||||||
|
msgstr "ಯೋಜನೆಯನ್ನು ರಚಿಸಿ/ಸೃಷ್ಟಿಸಿ"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"A project with this identifier (\"%(project)s\") already exists. Please "
|
||||||
|
"choose a new identifier"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enter private code to confirm deletion"
|
||||||
|
msgstr "ತೆಗೆದುಹಾಕಲು ನಿಮ್ಮ ಸಂಕೇತಪದವನ್ನು ನಮೂದಿಸಿ"
|
||||||
|
|
||||||
|
msgid "Unknown error"
|
||||||
|
msgstr "ಅಜ್ಞಾತ ದೋಷ"
|
||||||
|
|
||||||
|
msgid "Invalid private code."
|
||||||
|
msgstr "ನಮೂದಿಸಿರುವ ಸಂಕೇತಪದ ಅಮಾನ್ಯವಾಗಿದೆ."
|
||||||
|
|
||||||
|
msgid "Get in"
|
||||||
|
msgstr "ಒಳಹೊಗು"
|
||||||
|
|
||||||
|
msgid "Admin password"
|
||||||
|
msgstr "ಆಡಳಿತ ನಿರ್ವಾಹಕ ಪ್ರವೇಶ ಪದ"
|
||||||
|
|
||||||
|
msgid "Send me the code by email"
|
||||||
|
msgstr "ಸಂಕೇತಪದವನ್ನು ಮಿನ್ನಂಚೆ ಮೂಲಕ ಕಳುಹಿಸಿ"
|
||||||
|
|
||||||
|
msgid "This project does not exists"
|
||||||
|
msgstr "ಈ ಯೋಜನೆ ಅಸ್ತಿತ್ವದಲ್ಲಿಲ್ಲ"
|
||||||
|
|
||||||
|
msgid "Password mismatch"
|
||||||
|
msgstr "ಪ್ರವೇಶಪದ ಹೊಂದಾಣಿಕೆಯಾಗುತ್ತಿಲ್ಲ"
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "ಪ್ರವೇಶಪದ"
|
||||||
|
|
||||||
|
msgid "Password confirmation"
|
||||||
|
msgstr "ಪ್ರವೇಶಪದ ದೃಢೀಕರಿಸಿ"
|
||||||
|
|
||||||
|
msgid "Reset password"
|
||||||
|
msgstr "ಪ್ರವೇಶಪದ ಮರುಹೊಂದಿಸಿ"
|
||||||
|
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "ದಿನಾಂಕ"
|
||||||
|
|
||||||
|
msgid "What?"
|
||||||
|
msgstr "ಏನು?"
|
||||||
|
|
||||||
|
msgid "Payer"
|
||||||
|
msgstr "ಪಾವತಿಸಿದವರು"
|
||||||
|
|
||||||
|
msgid "Amount paid"
|
||||||
|
msgstr "ಪಾವತಿಸಿದ ಮೊತ್ತ"
|
||||||
|
|
||||||
|
msgid "Currency"
|
||||||
|
msgstr "ನಾಣ್ಯಪದ್ಧತಿ"
|
||||||
|
|
||||||
|
msgid "External link"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "A link to an external document, related to this bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "For whom?"
|
||||||
|
msgstr "ಯಾರಿಂದ?"
|
||||||
|
|
||||||
|
msgid "Submit"
|
||||||
|
msgstr "ಕೋರಿಕೆ ಸಲ್ಲಿಸು"
|
||||||
|
|
||||||
|
msgid "Submit and add a new one"
|
||||||
|
msgstr "ಕೋರಿಕೆ ಸಲ್ಲಿಸಿ ಹೊಸದನ್ನುಸೇರಿಸು"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project default: %(currency)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Bills can't be null"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "ಹೆಸರು"
|
||||||
|
|
||||||
|
msgid "Weights should be positive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Weight"
|
||||||
|
msgstr "ತೂಕ"
|
||||||
|
|
||||||
|
msgid "Add"
|
||||||
|
msgstr "ಕೂಡು"
|
||||||
|
|
||||||
|
msgid "User name incorrect"
|
||||||
|
msgstr "ತಪ್ಪು ಬಳಕೆದಾರನ ಹೆಸರು"
|
||||||
|
|
||||||
|
msgid "This project already have this member"
|
||||||
|
msgstr "ಈ ಸದಸ್ಯರು ಈಗಾಗಲೆ ಈ ಯೋಜನೆಯ ಸದಸ್ಯರಾಗಿದ್ದಾರೆ"
|
||||||
|
|
||||||
|
msgid "People to notify"
|
||||||
|
msgstr "ಸೂಚಿಸಬೇಕಾದ ಜನರು"
|
||||||
|
|
||||||
|
msgid "Send invites"
|
||||||
|
msgstr "ಆಮಂತ್ರಣ ಕಳುಹಿಸು"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "The email %(email)s is not valid"
|
||||||
|
msgstr "ಮಿನ್ನಂಚೆ %(email)s ಸಮಂಜಸವಾಗಿಲ್ಲ"
|
||||||
|
|
||||||
|
#. List with two items only
|
||||||
|
msgid "{dual_object_0} and {dual_object_1}"
|
||||||
|
msgstr "{dual_object_0} ಮತ್ತು {dual_object_1}"
|
||||||
|
|
||||||
|
#. Last two items of a list with more than 3 items
|
||||||
|
msgid "{previous_object}, and {end_object}"
|
||||||
|
msgstr "{previous_object}, ಮತ್ತು{end_object}"
|
||||||
|
|
||||||
|
#. Two items in a middle of a list with more than 5 objects
|
||||||
|
msgid "{previous_object}, {next_object}"
|
||||||
|
msgstr "{previous_object}, {next_object}"
|
||||||
|
|
||||||
|
#. First two items of a list with more than 3 items
|
||||||
|
msgid "{start_object}, {next_object}"
|
||||||
|
msgstr "{start_object}, {next_object}"
|
||||||
|
|
||||||
|
msgid "No Currency"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Form error with only one error
|
||||||
|
msgid "{prefix}: {error}"
|
||||||
|
msgstr "{prefix}: {error}"
|
||||||
|
|
||||||
|
#. Form error with a list of errors
|
||||||
|
msgid "{prefix}:<br />{errors}"
|
||||||
|
msgstr "{prefix}:<br />{errors}"
|
||||||
|
|
||||||
|
msgid "Too many failed login attempts, please retry later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "This admin password is not the right one. Only %(num)d attempts left."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You either provided a bad token or no project identifier."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This private code is not the right one"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "You have just created '%(project)s' to share your expenses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "A reminder email has just been sent to you"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"We tried to send you an reminder email, but there was an error. You can "
|
||||||
|
"still use the project normally."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "The project identifier is %(project)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Sorry, there was an error while sending you an email with password reset "
|
||||||
|
"instructions. Please check the email configuration of the server or "
|
||||||
|
"contact the administrator."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No token provided"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unknown project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password successfully reset."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project successfully uploaded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid JSON"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project successfully deleted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "You have been invited to share your expenses for %(project)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Your invitations have been sent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Sorry, there was an error while trying to send the invitation emails. "
|
||||||
|
"Please check the email configuration of the server or contact the "
|
||||||
|
"administrator."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "%(member)s has been added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error activating member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "%(name)s is part of this project again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error removing member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"User '%(name)s' has been deactivated. It will still appear in the users "
|
||||||
|
"list until its balance becomes zero."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "User '%(name)s' has been removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "User '%(name)s' has been edited"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The bill has been added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The bill has been deleted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The bill has been modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting project history"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Deleted project history."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting recorded IP addresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Deleted recorded IP addresses in project history."
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
msgid "Administration tasks are currently disabled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The project you are trying to access do not exist, do you want to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "create it"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create a new project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Number of members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Number of bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Newest bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Oldest bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "show"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The Dashboard is currently deactivated."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download Mobile Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Get it on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Are you sure?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Import JSON"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Choose file"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download project's data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Bill items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download the list of bills with owner, amount, reason,... "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settle plans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download the list of transactions needed to settle the current bills."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Can't remember the password?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Privacy Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit the project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This will remove all bills and participants in this project!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit this bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add a bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Everyone"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No one"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "More options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add participant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit this member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "john.doe@example.com, mary.moe@site.com"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send the invitations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disabled Project History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disabled Project History & IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled Project History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disabled IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled Project History & IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "History Settings Changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: %(property_name)s changed to %(after)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Confirm Remove IP Adresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Are you sure you want to delete all recorded IP addresses from this "
|
||||||
|
"project?\n"
|
||||||
|
" The rest of the project history will be unaffected. This "
|
||||||
|
"action cannot be undone."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Confirm deletion"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete Confirmation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Are you sure you want to erase all history for this project? This action "
|
||||||
|
"cannot be undone."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: added %(owers_list_str)s to owers list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" <i>This project has history disabled. New actions won't "
|
||||||
|
"appear below. You can enable history on the</i>\n"
|
||||||
|
" <a href=\"%(url)s\">settings page</a>\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" <i>The table below reflects actions recorded prior to "
|
||||||
|
"disabling project history. You can\n"
|
||||||
|
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
|
||||||
|
"data-target=\"#confirm-erase\">clear project history</a> to remove "
|
||||||
|
"them.</i></p>\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Some entries below contain IP addresses, even though this project has IP "
|
||||||
|
"recording disabled. "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete stored IP addresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No history to erase"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Clear Project History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No IP Addresses to erase"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete Stored IP Addresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Event"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IP address recording can be enabled on the settings page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IP address recording can be disabled on the settings page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "From IP"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project %(name)s added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project private code changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project renamed to %(new_project_name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project contact email changed to %(new_email)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project settings modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s deactivated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s reactivated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s renamed to %(new_name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s renamed to %(new_description)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Amount"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Amount in %(currency)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project %(name)s changed in an unknown way"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s changed in an unknown way"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s changed in an unknown way"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Nothing to list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Someone probably cleared the project history."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Manage your shared <br />expenses, easily"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Try out the demo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You're sharing a house?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Going on holidays with friends?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Simply sharing money with others?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "We can help!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log in to an existing project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "can't remember your password?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Don\\'t reuse a personal password. Choose a private code and send it to "
|
||||||
|
"your friends"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Account manager"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Statistics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Languages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Projects"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Start a new project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Other projects :"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "switch to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Logout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Mobile Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Documentation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Administation Dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "\"I hate money\" is free software"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "you can contribute and improve it!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "%(amount)s each"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "you sure?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invite people"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You should start by adding participants"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add a new bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Newer bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Older bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "When?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Who paid?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "For what?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "How much?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Added on %(date)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Everyone but %(excluded)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Nothing to list yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You probably want to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "add a bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "add participants"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password reminder"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"A link to reset your password has been sent to you, please check your "
|
||||||
|
"emails."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Return to home page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Your projects"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reset your password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invite people to join this project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Share Identifier & code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"You can share the project identifier and the private code by any "
|
||||||
|
"communication means."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Identifier:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Share the Link"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You can directly share the following link via your prefered medium"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send via Emails"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Specify a (comma separated) list of email adresses you want to notify "
|
||||||
|
"about the\n"
|
||||||
|
" creation of this budget management project and we will "
|
||||||
|
"send them an email for you."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Who pays?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "To whom?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Who?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "deactivate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "reactivate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Spent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Expenses by Month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Period"
|
||||||
|
msgstr ""
|
Binary file not shown.
|
@ -1,19 +1,19 @@
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||||
"PO-Revision-Date: 2021-05-10 11:33+0000\n"
|
"PO-Revision-Date: 2021-09-20 12:38+0000\n"
|
||||||
"Last-Translator: Vsevolod <sevauserg.com@gmail.com>\n"
|
"Last-Translator: Роман Прокопов <pochta.romana.iz.vyborga@gmail.com>\n"
|
||||||
|
"Language-Team: Russian <https://hosted.weblate.org/projects/i-hate-money/"
|
||||||
|
"i-hate-money/ru/>\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
"Language-Team: Russian <https://hosted.weblate.org/projects/i-hate-"
|
|
||||||
"money/i-hate-money/ru/>\n"
|
|
||||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
|
||||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2\n"
|
|
||||||
"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"
|
||||||
|
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||||
|
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||||
|
"X-Generator: Weblate 4.9-dev\n"
|
||||||
"Generated-By: Babel 2.9.0\n"
|
"Generated-By: Babel 2.9.0\n"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -26,9 +26,8 @@ msgstr ""
|
||||||
msgid "Project name"
|
msgid "Project name"
|
||||||
msgstr "Имя проекта"
|
msgstr "Имя проекта"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "New private code"
|
msgid "New private code"
|
||||||
msgstr "Приватный код"
|
msgstr "Новый приватный код"
|
||||||
|
|
||||||
msgid "Enter a new code if you want to change it"
|
msgid "Enter a new code if you want to change it"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
BIN
ihatemoney/translations/th/LC_MESSAGES/messages.mo
Normal file
BIN
ihatemoney/translations/th/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
880
ihatemoney/translations/th/LC_MESSAGES/messages.po
Normal file
880
ihatemoney/translations/th/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,880 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2021-09-15 15:41+0200\n"
|
||||||
|
"PO-Revision-Date: 2021-09-16 14:36+0000\n"
|
||||||
|
"Last-Translator: PPNplus <ppnplus@protonmail.com>\n"
|
||||||
|
"Language-Team: Thai <https://hosted.weblate.org/projects/i-hate-money/"
|
||||||
|
"i-hate-money/th/>\n"
|
||||||
|
"Language: th\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
"X-Generator: Weblate 4.9-dev\n"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Not a valid amount or expression. Only numbers and + - * / operators are "
|
||||||
|
"accepted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project name"
|
||||||
|
msgstr "ชื่อโครงการ"
|
||||||
|
|
||||||
|
msgid "New private code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enter a new code if you want to change it"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "อีเมล"
|
||||||
|
|
||||||
|
msgid "Enable project history"
|
||||||
|
msgstr "เปิดใช้ประวัติโครงการ"
|
||||||
|
|
||||||
|
msgid "Use IP tracking for project history"
|
||||||
|
msgstr "ใช้การติดตามที่อยู่ IP สำหรับประวัติโครงการ"
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
msgid "Import"
|
||||||
|
msgstr "นำเข้า"
|
||||||
|
|
||||||
|
msgid "Project identifier"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Private code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create the project"
|
||||||
|
msgstr "สร้างโครงการ"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"A project with this identifier (\"%(project)s\") already exists. Please "
|
||||||
|
"choose a new identifier"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enter private code to confirm deletion"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unknown error"
|
||||||
|
msgstr "ข้อผิดพลาดที่ไม่รู้จัก"
|
||||||
|
|
||||||
|
msgid "Invalid private code."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Get in"
|
||||||
|
msgstr "เข้าด้านใน"
|
||||||
|
|
||||||
|
msgid "Admin password"
|
||||||
|
msgstr "รหัสผ่านผู้ดูแล"
|
||||||
|
|
||||||
|
msgid "Send me the code by email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This project does not exists"
|
||||||
|
msgstr "โครงการนี้ไม่มีอยู่"
|
||||||
|
|
||||||
|
msgid "Password mismatch"
|
||||||
|
msgstr "รหัสผ่านไม่ตรงกัน"
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "รหัสผ่าน"
|
||||||
|
|
||||||
|
msgid "Password confirmation"
|
||||||
|
msgstr "ยืนยันรหัสผ่าน"
|
||||||
|
|
||||||
|
msgid "Reset password"
|
||||||
|
msgstr "รีเซ็ตรหัสผ่าน"
|
||||||
|
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "วันที่"
|
||||||
|
|
||||||
|
msgid "What?"
|
||||||
|
msgstr "อะไร?"
|
||||||
|
|
||||||
|
msgid "Payer"
|
||||||
|
msgstr "ผู้จ่าย"
|
||||||
|
|
||||||
|
msgid "Amount paid"
|
||||||
|
msgstr "จำนวนเงินที่จ่าย"
|
||||||
|
|
||||||
|
msgid "Currency"
|
||||||
|
msgstr "สกุลเงิน"
|
||||||
|
|
||||||
|
msgid "External link"
|
||||||
|
msgstr "ลิงก์ภายนอก"
|
||||||
|
|
||||||
|
msgid "A link to an external document, related to this bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "For whom?"
|
||||||
|
msgstr "ให้ใคร?"
|
||||||
|
|
||||||
|
msgid "Submit"
|
||||||
|
msgstr "ส่ง"
|
||||||
|
|
||||||
|
msgid "Submit and add a new one"
|
||||||
|
msgstr "ส่งและเพิ่มอันใหม่"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project default: %(currency)s"
|
||||||
|
msgstr "ค่าเริ่มต้นของโครงการ: %(currency)s"
|
||||||
|
|
||||||
|
msgid "Bills can't be null"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "ชื่อ"
|
||||||
|
|
||||||
|
msgid "Weights should be positive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Weight"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add"
|
||||||
|
msgstr "เพิ่ม"
|
||||||
|
|
||||||
|
msgid "User name incorrect"
|
||||||
|
msgstr "ชื่อผู้ใช้ไม่ถูกต้อง"
|
||||||
|
|
||||||
|
msgid "This project already have this member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "People to notify"
|
||||||
|
msgstr "ผู้คนที่จะแจ้งเตือน"
|
||||||
|
|
||||||
|
msgid "Send invites"
|
||||||
|
msgstr "ส่งคำเชิญ"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "The email %(email)s is not valid"
|
||||||
|
msgstr "อีเมล %(email)s ไม่ถูกต้อง"
|
||||||
|
|
||||||
|
#. List with two items only
|
||||||
|
msgid "{dual_object_0} and {dual_object_1}"
|
||||||
|
msgstr "{dual_object_0} และ {dual_object_1}"
|
||||||
|
|
||||||
|
#. Last two items of a list with more than 3 items
|
||||||
|
msgid "{previous_object}, and {end_object}"
|
||||||
|
msgstr "{previous_object}, และ {end_object}"
|
||||||
|
|
||||||
|
#. Two items in a middle of a list with more than 5 objects
|
||||||
|
msgid "{previous_object}, {next_object}"
|
||||||
|
msgstr "{previous_object}, {next_object}"
|
||||||
|
|
||||||
|
#. First two items of a list with more than 3 items
|
||||||
|
msgid "{start_object}, {next_object}"
|
||||||
|
msgstr "{start_object}, {next_object}"
|
||||||
|
|
||||||
|
msgid "No Currency"
|
||||||
|
msgstr "ไม่มีสกุลเงิน"
|
||||||
|
|
||||||
|
#. Form error with only one error
|
||||||
|
msgid "{prefix}: {error}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Form error with a list of errors
|
||||||
|
msgid "{prefix}:<br />{errors}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Too many failed login attempts, please retry later."
|
||||||
|
msgstr "เข้าสู่ระบบล้มเหลวหลายครั้ง โปรดลองอีกครั้งในภายหลัง"
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "This admin password is not the right one. Only %(num)d attempts left."
|
||||||
|
msgstr ""
|
||||||
|
"รหัสผ่านผู้ดูแลนี้ไม่ถูกต้อง สามารถพยายามเข้าสู่ระบบได้อีก %(num)d ครั้ง"
|
||||||
|
|
||||||
|
msgid "You either provided a bad token or no project identifier."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This private code is not the right one"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "You have just created '%(project)s' to share your expenses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "A reminder email has just been sent to you"
|
||||||
|
msgstr "ส่งอีเมลเตือนความจำให้คุณแล้ว"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"We tried to send you an reminder email, but there was an error. You can "
|
||||||
|
"still use the project normally."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "The project identifier is %(project)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Sorry, there was an error while sending you an email with password reset "
|
||||||
|
"instructions. Please check the email configuration of the server or "
|
||||||
|
"contact the administrator."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No token provided"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unknown project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password successfully reset."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project successfully uploaded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invalid JSON"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project successfully deleted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "You have been invited to share your expenses for %(project)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Your invitations have been sent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Sorry, there was an error while trying to send the invitation emails. "
|
||||||
|
"Please check the email configuration of the server or contact the "
|
||||||
|
"administrator."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "%(member)s has been added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error activating member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "%(name)s is part of this project again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error removing member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"User '%(name)s' has been deactivated. It will still appear in the users "
|
||||||
|
"list until its balance becomes zero."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "User '%(name)s' has been removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "User '%(name)s' has been edited"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The bill has been added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The bill has been deleted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The bill has been modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting project history"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Deleted project history."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error deleting recorded IP addresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Deleted recorded IP addresses in project history."
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
msgid "Administration tasks are currently disabled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The project you are trying to access do not exist, do you want to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "create it"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create a new project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Number of members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Number of bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Newest bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Oldest bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "show"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The Dashboard is currently deactivated."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download Mobile Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Get it on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Are you sure?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Import JSON"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Choose file"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download project's data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Bill items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download the list of bills with owner, amount, reason,... "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settle plans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download the list of transactions needed to settle the current bills."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Can't remember the password?"
|
||||||
|
msgstr "หากจำรหัสผ่านไม่ได้"
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "ยกเลิก"
|
||||||
|
|
||||||
|
msgid "Privacy Settings"
|
||||||
|
msgstr "การตั้งค่าความเป็นส่วนตัว"
|
||||||
|
|
||||||
|
msgid "Edit the project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This will remove all bills and participants in this project!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit this bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add a bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Everyone"
|
||||||
|
msgstr "ทุกคน"
|
||||||
|
|
||||||
|
msgid "No one"
|
||||||
|
msgstr "ไม่มีใคร"
|
||||||
|
|
||||||
|
msgid "More options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add participant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Edit this member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "john.doe@example.com, mary.moe@site.com"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send the invitations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "ดาวน์โหลด"
|
||||||
|
|
||||||
|
msgid "Disabled Project History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disabled Project History & IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled Project History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disabled IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled Project History & IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled IP Address Recording"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "History Settings Changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: %(property_name)s changed to %(after)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Confirm Remove IP Adresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Are you sure you want to delete all recorded IP addresses from this "
|
||||||
|
"project?\n"
|
||||||
|
" The rest of the project history will be unaffected. This "
|
||||||
|
"action cannot be undone."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Confirm deletion"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete Confirmation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Are you sure you want to erase all history for this project? This action "
|
||||||
|
"cannot be undone."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: added %(owers_list_str)s to owers list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" <i>This project has history disabled. New actions won't "
|
||||||
|
"appear below. You can enable history on the</i>\n"
|
||||||
|
" <a href=\"%(url)s\">settings page</a>\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" <i>The table below reflects actions recorded prior to "
|
||||||
|
"disabling project history. You can\n"
|
||||||
|
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
|
||||||
|
"data-target=\"#confirm-erase\">clear project history</a> to remove "
|
||||||
|
"them.</i></p>\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Some entries below contain IP addresses, even though this project has IP "
|
||||||
|
"recording disabled. "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete stored IP addresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No history to erase"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Clear Project History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No IP Addresses to erase"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Delete Stored IP Addresses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Event"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IP address recording can be enabled on the settings page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IP address recording can be disabled on the settings page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "From IP"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project %(name)s added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project private code changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project renamed to %(new_project_name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project contact email changed to %(new_email)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project settings modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s deactivated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s reactivated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s renamed to %(new_name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s renamed to %(new_description)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Amount"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Amount in %(currency)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Project %(name)s changed in an unknown way"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Bill %(name)s changed in an unknown way"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Participant %(name)s changed in an unknown way"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Nothing to list"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Someone probably cleared the project history."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Manage your shared <br />expenses, easily"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Try out the demo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You're sharing a house?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Going on holidays with friends?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Simply sharing money with others?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "We can help!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log in to an existing project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Log in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "can't remember your password?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Don\\'t reuse a personal password. Choose a private code and send it to "
|
||||||
|
"your friends"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Account manager"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Statistics"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Languages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Projects"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Start a new project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Other projects :"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "switch to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Logout"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Mobile Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Documentation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Administation Dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "\"I hate money\" is free software"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "you can contribute and improve it!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "%(amount)s each"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "you sure?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invite people"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You should start by adding participants"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add a new bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Newer bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Older bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "When?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Who paid?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "For what?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "How much?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Added on %(date)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-format
|
||||||
|
msgid "Everyone but %(excluded)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No bills"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Nothing to list yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You probably want to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "add a bill"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "add participants"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password reminder"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"A link to reset your password has been sent to you, please check your "
|
||||||
|
"emails."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Return to home page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Your projects"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reset your password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Invite people to join this project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Share Identifier & code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"You can share the project identifier and the private code by any "
|
||||||
|
"communication means."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Identifier:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Share the Link"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You can directly share the following link via your prefered medium"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Send via Emails"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Specify a (comma separated) list of email adresses you want to notify "
|
||||||
|
"about the\n"
|
||||||
|
" creation of this budget management project and we will "
|
||||||
|
"send them an email for you."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Who pays?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "To whom?"
|
||||||
|
msgstr "ให้ใคร?"
|
||||||
|
|
||||||
|
msgid "Who?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "deactivate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "reactivate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Spent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Expenses by Month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Period"
|
||||||
|
msgstr ""
|
Binary file not shown.
|
@ -1,19 +1,18 @@
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
"POT-Creation-Date: 2021-07-17 17:31+0200\n"
|
||||||
"PO-Revision-Date: 2020-10-12 04:47+0000\n"
|
"PO-Revision-Date: 2021-10-10 05:05+0000\n"
|
||||||
"Last-Translator: Jwen921 <yangjingwen0921@gmail.com>\n"
|
"Last-Translator: Frank.wu <me@wuzhiping.top>\n"
|
||||||
|
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
|
||||||
|
"i-hate-money/i-hate-money/zh_Hans/>\n"
|
||||||
"Language: zh_Hans\n"
|
"Language: zh_Hans\n"
|
||||||
"Language-Team: Chinese (Simplified) "
|
|
||||||
"<https://hosted.weblate.org/projects/i-hate-money/i-hate-money/zh_Hans/>"
|
|
||||||
"\n"
|
|
||||||
"Plural-Forms: nplurals=1; plural=0\n"
|
|
||||||
"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"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
"X-Generator: Weblate 4.9-dev\n"
|
||||||
"Generated-By: Babel 2.9.0\n"
|
"Generated-By: Babel 2.9.0\n"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -24,12 +23,11 @@ msgstr "金额或符号无效。仅限数字与+-*/符号。"
|
||||||
msgid "Project name"
|
msgid "Project name"
|
||||||
msgstr "账目名称"
|
msgstr "账目名称"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "New private code"
|
msgid "New private code"
|
||||||
msgstr "共享密钥"
|
msgstr "新的私人代码"
|
||||||
|
|
||||||
msgid "Enter a new code if you want to change it"
|
msgid "Enter a new code if you want to change it"
|
||||||
msgstr ""
|
msgstr "如要更改,请输入新代码"
|
||||||
|
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "邮箱"
|
msgstr "邮箱"
|
||||||
|
@ -46,7 +44,7 @@ msgstr "默认货币"
|
||||||
msgid ""
|
msgid ""
|
||||||
"This project cannot be set to 'no currency' because it contains bills in "
|
"This project cannot be set to 'no currency' because it contains bills in "
|
||||||
"multiple currencies."
|
"multiple currencies."
|
||||||
msgstr ""
|
msgstr "此项目不能设置为“无货币”,因为它包含多种货币的账单。"
|
||||||
|
|
||||||
msgid "Import previously exported JSON file"
|
msgid "Import previously exported JSON file"
|
||||||
msgstr "导入之前的JSON 文件"
|
msgstr "导入之前的JSON 文件"
|
||||||
|
@ -70,15 +68,13 @@ msgid ""
|
||||||
msgstr "账目(“%(project)s”)已存在,请选择一个新名称"
|
msgstr "账目(“%(project)s”)已存在,请选择一个新名称"
|
||||||
|
|
||||||
msgid "Enter private code to confirm deletion"
|
msgid "Enter private code to confirm deletion"
|
||||||
msgstr ""
|
msgstr "请输入专用代码以确认删除"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "未知项目"
|
msgstr "未知错误"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Invalid private code."
|
msgid "Invalid private code."
|
||||||
msgstr "共享密钥"
|
msgstr "无效的私人代码。"
|
||||||
|
|
||||||
msgid "Get in"
|
msgid "Get in"
|
||||||
msgstr "进入"
|
msgstr "进入"
|
||||||
|
@ -171,40 +167,40 @@ msgstr "此邮箱%(email)s不存在"
|
||||||
|
|
||||||
#. List with two items only
|
#. List with two items only
|
||||||
msgid "{dual_object_0} and {dual_object_1}"
|
msgid "{dual_object_0} and {dual_object_1}"
|
||||||
msgstr ""
|
msgstr "{dual_object_0} 和 {dual_object_1}"
|
||||||
|
|
||||||
#. Last two items of a list with more than 3 items
|
#. Last two items of a list with more than 3 items
|
||||||
msgid "{previous_object}, and {end_object}"
|
msgid "{previous_object}, and {end_object}"
|
||||||
msgstr ""
|
msgstr "{previous_object} 和 {end_object}"
|
||||||
|
|
||||||
#. Two items in a middle of a list with more than 5 objects
|
#. Two items in a middle of a list with more than 5 objects
|
||||||
msgid "{previous_object}, {next_object}"
|
msgid "{previous_object}, {next_object}"
|
||||||
msgstr ""
|
msgstr "{previous_object},{next_object}"
|
||||||
|
|
||||||
#. First two items of a list with more than 3 items
|
#. First two items of a list with more than 3 items
|
||||||
msgid "{start_object}, {next_object}"
|
msgid "{start_object}, {next_object}"
|
||||||
msgstr ""
|
msgstr "{start_object}, {next_object}"
|
||||||
|
|
||||||
msgid "No Currency"
|
msgid "No Currency"
|
||||||
msgstr "没有货币"
|
msgstr "无货币"
|
||||||
|
|
||||||
#. Form error with only one error
|
#. Form error with only one error
|
||||||
msgid "{prefix}: {error}"
|
msgid "{prefix}: {error}"
|
||||||
msgstr ""
|
msgstr "{prefix}: {error}"
|
||||||
|
|
||||||
#. Form error with a list of errors
|
#. Form error with a list of errors
|
||||||
msgid "{prefix}:<br />{errors}"
|
msgid "{prefix}:<br />{errors}"
|
||||||
msgstr ""
|
msgstr "{prefix}:<br />{errors}"
|
||||||
|
|
||||||
msgid "Too many failed login attempts, please retry later."
|
msgid "Too many failed login attempts, please retry later."
|
||||||
msgstr "输入错误太多次了,请稍后重试。"
|
msgstr "登录失败次数过多,请稍后重试。"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "This admin password is not the right one. Only %(num)d attempts left."
|
msgid "This admin password is not the right one. Only %(num)d attempts left."
|
||||||
msgstr "管理密码有误,只剩 %(num)d次尝试机会"
|
msgstr "管理密码有误,只剩 %(num)d次尝试机会。"
|
||||||
|
|
||||||
msgid "You either provided a bad token or no project identifier."
|
msgid "You either provided a bad token or no project identifier."
|
||||||
msgstr "你输入了错误的符号或没有项目标识符。"
|
msgstr "你输入了错误的令牌或没有项目标识符。"
|
||||||
|
|
||||||
msgid "This private code is not the right one"
|
msgid "This private code is not the right one"
|
||||||
msgstr "专用码不正确"
|
msgstr "专用码不正确"
|
||||||
|
@ -241,7 +237,7 @@ msgid "Unknown project"
|
||||||
msgstr "未知项目"
|
msgstr "未知项目"
|
||||||
|
|
||||||
msgid "Password successfully reset."
|
msgid "Password successfully reset."
|
||||||
msgstr "密码重置成功"
|
msgstr "密码重置成功。"
|
||||||
|
|
||||||
msgid "Project successfully uploaded"
|
msgid "Project successfully uploaded"
|
||||||
msgstr "项目成功上传"
|
msgstr "项目成功上传"
|
||||||
|
@ -253,7 +249,7 @@ msgid "Project successfully deleted"
|
||||||
msgstr "项目成功删除"
|
msgstr "项目成功删除"
|
||||||
|
|
||||||
msgid "Error deleting project"
|
msgid "Error deleting project"
|
||||||
msgstr ""
|
msgstr "删除项目时出错"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "You have been invited to share your expenses for %(project)s"
|
msgid "You have been invited to share your expenses for %(project)s"
|
||||||
|
@ -273,20 +269,20 @@ msgid "%(member)s has been added"
|
||||||
msgstr "已添加%(member)s"
|
msgstr "已添加%(member)s"
|
||||||
|
|
||||||
msgid "Error activating member"
|
msgid "Error activating member"
|
||||||
msgstr ""
|
msgstr "激活成员时出错"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(name)s is part of this project again"
|
msgid "%(name)s is part of this project again"
|
||||||
msgstr "%(name)s 已经在项目里了"
|
msgstr "%(name)s 已经在项目里了"
|
||||||
|
|
||||||
msgid "Error removing member"
|
msgid "Error removing member"
|
||||||
msgstr ""
|
msgstr "删除成员时出错"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"User '%(name)s' has been deactivated. It will still appear in the users "
|
"User '%(name)s' has been deactivated. It will still appear in the users "
|
||||||
"list until its balance becomes zero."
|
"list until its balance becomes zero."
|
||||||
msgstr "用户 '%(name)s'已被暂停,在余额为0之前会继续显示在用户列表里"
|
msgstr "用户 '%(name)s'已被暂停,在余额为0之前会继续显示在用户列表里。"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "User '%(name)s' has been removed"
|
msgid "User '%(name)s' has been removed"
|
||||||
|
@ -300,7 +296,7 @@ msgid "The bill has been added"
|
||||||
msgstr "帐单已添加"
|
msgstr "帐单已添加"
|
||||||
|
|
||||||
msgid "Error deleting bill"
|
msgid "Error deleting bill"
|
||||||
msgstr ""
|
msgstr "删除账单时出错"
|
||||||
|
|
||||||
msgid "The bill has been deleted"
|
msgid "The bill has been deleted"
|
||||||
msgstr "账单已删除"
|
msgstr "账单已删除"
|
||||||
|
@ -308,26 +304,23 @@ msgstr "账单已删除"
|
||||||
msgid "The bill has been modified"
|
msgid "The bill has been modified"
|
||||||
msgstr "帐单已修改"
|
msgstr "帐单已修改"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Error deleting project history"
|
msgid "Error deleting project history"
|
||||||
msgstr "启用项目历史"
|
msgstr "删除项目历史记录时出错"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Deleted project history."
|
msgid "Deleted project history."
|
||||||
msgstr "启用项目历史"
|
msgstr "已删除的项目历史记录。"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Error deleting recorded IP addresses"
|
msgid "Error deleting recorded IP addresses"
|
||||||
msgstr "删除已储存的IP地址"
|
msgstr "删除记录的IP地址时出错"
|
||||||
|
|
||||||
msgid "Deleted recorded IP addresses in project history."
|
msgid "Deleted recorded IP addresses in project history."
|
||||||
msgstr ""
|
msgstr "删除项目历史记录中的 IP 地址。"
|
||||||
|
|
||||||
msgid "Sorry, we were unable to find the page you've asked for."
|
msgid "Sorry, we were unable to find the page you've asked for."
|
||||||
msgstr "对不起,未找到该页面"
|
msgstr "抱歉,我们无法找到您要求的页面。"
|
||||||
|
|
||||||
msgid "The best thing to do is probably to get back to the main page."
|
msgid "The best thing to do is probably to get back to the main page."
|
||||||
msgstr "最好的办法是返回主页"
|
msgstr "最好的办法是返回主页。"
|
||||||
|
|
||||||
msgid "Back to the list"
|
msgid "Back to the list"
|
||||||
msgstr "返回列表"
|
msgstr "返回列表"
|
||||||
|
@ -375,26 +368,22 @@ msgid "show"
|
||||||
msgstr "显示"
|
msgstr "显示"
|
||||||
|
|
||||||
msgid "The Dashboard is currently deactivated."
|
msgid "The Dashboard is currently deactivated."
|
||||||
msgstr "操作面板失效"
|
msgstr "操作面板失效。"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Download Mobile Application"
|
msgid "Download Mobile Application"
|
||||||
msgstr "手机软件"
|
msgstr "下载移动应用程序"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Get it on"
|
msgid "Get it on"
|
||||||
msgstr "进入"
|
msgstr "获取"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr "确定?"
|
msgstr "是否确定?"
|
||||||
|
|
||||||
msgid "Edit project"
|
msgid "Edit project"
|
||||||
msgstr "编辑项目"
|
msgstr "编辑项目"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Delete project"
|
msgid "Delete project"
|
||||||
msgstr "编辑项目"
|
msgstr "删除项目"
|
||||||
|
|
||||||
msgid "Import JSON"
|
msgid "Import JSON"
|
||||||
msgstr "导入json文件"
|
msgstr "导入json文件"
|
||||||
|
@ -430,7 +419,7 @@ msgid "Edit the project"
|
||||||
msgstr "编辑项目"
|
msgstr "编辑项目"
|
||||||
|
|
||||||
msgid "This will remove all bills and participants in this project!"
|
msgid "This will remove all bills and participants in this project!"
|
||||||
msgstr ""
|
msgstr "这将删除此项目的所有账单和参与者!"
|
||||||
|
|
||||||
msgid "Edit this bill"
|
msgid "Edit this bill"
|
||||||
msgstr "编辑帐单"
|
msgstr "编辑帐单"
|
||||||
|
@ -442,10 +431,10 @@ msgid "Everyone"
|
||||||
msgstr "每个人"
|
msgstr "每个人"
|
||||||
|
|
||||||
msgid "No one"
|
msgid "No one"
|
||||||
msgstr ""
|
msgstr "无人"
|
||||||
|
|
||||||
msgid "More options"
|
msgid "More options"
|
||||||
msgstr ""
|
msgstr "更多选项"
|
||||||
|
|
||||||
msgid "Add participant"
|
msgid "Add participant"
|
||||||
msgstr "添加参与人"
|
msgstr "添加参与人"
|
||||||
|
@ -485,11 +474,11 @@ msgstr "历史设置改变"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s"
|
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s"
|
||||||
msgstr ""
|
msgstr "账单 %(name)s: %(property_name)s 从 %(before)s 改为 %(after)s"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s: %(property_name)s changed to %(after)s"
|
msgid "Bill %(name)s: %(property_name)s changed to %(after)s"
|
||||||
msgstr ""
|
msgstr "账单 %(name)s: %(property_name)s 改为 %(after)s"
|
||||||
|
|
||||||
msgid "Confirm Remove IP Adresses"
|
msgid "Confirm Remove IP Adresses"
|
||||||
msgstr "确认移除IP地址"
|
msgstr "确认移除IP地址"
|
||||||
|
@ -503,7 +492,6 @@ msgstr ""
|
||||||
"你确定要删除此项目里所有的IP地址吗?\n"
|
"你确定要删除此项目里所有的IP地址吗?\n"
|
||||||
"项目其他内容不受影响,此操作不可撤回。"
|
"项目其他内容不受影响,此操作不可撤回。"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Confirm deletion"
|
msgid "Confirm deletion"
|
||||||
msgstr "确认删除"
|
msgstr "确认删除"
|
||||||
|
|
||||||
|
@ -520,11 +508,11 @@ msgstr "确定删除此项目所有记录?此操作不可撤回。"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s: added %(owers_list_str)s to owers list"
|
msgid "Bill %(name)s: added %(owers_list_str)s to owers list"
|
||||||
msgstr ""
|
msgstr "帐单 %(name)s:将 %(owers_list_str)s 添加到所有者列表"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
|
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
|
||||||
msgstr ""
|
msgstr "账单 %(name)s:从所有者列表中删除了 %(owers_list_str)s"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -590,51 +578,51 @@ msgstr "IP地址记录可在设置里禁用"
|
||||||
msgid "From IP"
|
msgid "From IP"
|
||||||
msgstr "从IP"
|
msgstr "从IP"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Project %(name)s added"
|
msgid "Project %(name)s added"
|
||||||
msgstr "账目名称"
|
msgstr "项目 %(name)s 已添加"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s added"
|
msgid "Bill %(name)s added"
|
||||||
msgstr "帐单已添加"
|
msgstr "帐单 %(name)s 已添加"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s added"
|
msgid "Participant %(name)s added"
|
||||||
msgstr ""
|
msgstr "成员 %(name)s 已添加"
|
||||||
|
|
||||||
msgid "Project private code changed"
|
msgid "Project private code changed"
|
||||||
msgstr "项目专用码已更改"
|
msgstr "项目专用码已更改"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Project renamed to %(new_project_name)s"
|
msgid "Project renamed to %(new_project_name)s"
|
||||||
msgstr "项目的标识符是%(project)s"
|
msgstr "项目的标识符是 %(new_project_name)s"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Project contact email changed to %(new_email)s"
|
msgid "Project contact email changed to %(new_email)s"
|
||||||
msgstr "项目联系邮箱更改为"
|
msgstr "项目联系邮箱更改为 %(new_email)s"
|
||||||
|
|
||||||
msgid "Project settings modified"
|
msgid "Project settings modified"
|
||||||
msgstr "项目设置已修改"
|
msgstr "项目设置已修改"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s deactivated"
|
msgid "Participant %(name)s deactivated"
|
||||||
msgstr ""
|
msgstr "成员 %(name)s 已停用"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s reactivated"
|
msgid "Participant %(name)s reactivated"
|
||||||
msgstr ""
|
msgstr "成员 %(name)s 被重新激活"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s renamed to %(new_name)s"
|
msgid "Participant %(name)s renamed to %(new_name)s"
|
||||||
msgstr ""
|
msgstr "成员 %(name)s 重命名为 %(new_name)s"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s renamed to %(new_description)s"
|
msgid "Bill %(name)s renamed to %(new_description)s"
|
||||||
msgstr ""
|
msgstr "账单 %(name)s 更名为 %(new_description)s"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s"
|
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s"
|
||||||
msgstr ""
|
msgstr "成员 %(name)s:权重从%(old_weight)s变为%(new_weight)s"
|
||||||
|
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr "数量"
|
msgstr "数量"
|
||||||
|
@ -643,33 +631,33 @@ msgstr "数量"
|
||||||
msgid "Amount in %(currency)s"
|
msgid "Amount in %(currency)s"
|
||||||
msgstr "%(currency)s的数量是"
|
msgstr "%(currency)s的数量是"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s modified"
|
msgid "Bill %(name)s modified"
|
||||||
msgstr "帐单已修改"
|
msgstr "帐单 %(name)s 已修改"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s modified"
|
msgid "Participant %(name)s modified"
|
||||||
msgstr ""
|
msgstr "成员 %(name)s 已修改"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s removed"
|
msgid "Bill %(name)s removed"
|
||||||
msgstr "用户 '%(name)s'已被移除"
|
msgstr "账单 %(name)s 已被移除"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s removed"
|
msgid "Participant %(name)s removed"
|
||||||
msgstr "用户 '%(name)s'已被移除"
|
msgstr "用户 %(name)s 已被移除"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Project %(name)s changed in an unknown way"
|
msgid "Project %(name)s changed in an unknown way"
|
||||||
msgstr "未知的改变"
|
msgstr "项目 %(name)s 以未知方式更改"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Bill %(name)s changed in an unknown way"
|
msgid "Bill %(name)s changed in an unknown way"
|
||||||
msgstr "未知的改变"
|
msgstr "账单 %(name)s 以一种未知的方式更改"
|
||||||
|
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
msgid "Participant %(name)s changed in an unknown way"
|
msgid "Participant %(name)s changed in an unknown way"
|
||||||
msgstr "未知的改变"
|
msgstr "成员 %(name)s 以未知方式更改"
|
||||||
|
|
||||||
msgid "Nothing to list"
|
msgid "Nothing to list"
|
||||||
msgstr "无列表"
|
msgstr "无列表"
|
||||||
|
@ -815,7 +803,7 @@ msgid "No bills"
|
||||||
msgstr "没有账单"
|
msgstr "没有账单"
|
||||||
|
|
||||||
msgid "Nothing to list yet."
|
msgid "Nothing to list yet."
|
||||||
msgstr "没有列表"
|
msgstr "没有列表。"
|
||||||
|
|
||||||
msgid "You probably want to"
|
msgid "You probably want to"
|
||||||
msgstr "你想要"
|
msgstr "你想要"
|
||||||
|
|
|
@ -271,7 +271,7 @@ def get_members(file):
|
||||||
|
|
||||||
|
|
||||||
def same_bill(bill1, bill2):
|
def same_bill(bill1, bill2):
|
||||||
attr = ["what", "payer_name", "payer_weight", "amount", "date", "owers"]
|
attr = ["what", "payer_name", "payer_weight", "amount", "currency", "date", "owers"]
|
||||||
for a in attr:
|
for a in attr:
|
||||||
if bill1[a] != bill2[a]:
|
if bill1[a] != bill2[a]:
|
||||||
return False
|
return False
|
||||||
|
@ -370,6 +370,7 @@ def localize_list(items, surround_with_em=True):
|
||||||
|
|
||||||
|
|
||||||
def render_localized_currency(code, detailed=True):
|
def render_localized_currency(code, detailed=True):
|
||||||
|
# We cannot use CurrencyConvertor.no_currency here because of circular dependencies
|
||||||
if code == "XXX":
|
if code == "XXX":
|
||||||
return _("No Currency")
|
return _("No Currency")
|
||||||
locale = get_locale() or "en_US"
|
locale = get_locale() or "en_US"
|
||||||
|
|
|
@ -36,6 +36,7 @@ from sqlalchemy_continuum import Operation
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
|
from ihatemoney.currency_convertor import CurrencyConverter
|
||||||
from ihatemoney.forms import (
|
from ihatemoney.forms import (
|
||||||
AdminAuthenticationForm,
|
AdminAuthenticationForm,
|
||||||
AuthenticationForm,
|
AuthenticationForm,
|
||||||
|
@ -142,7 +143,8 @@ def pull_project(endpoint, values):
|
||||||
raise Redirect303(url_for(".create_project", project_id=project_id))
|
raise Redirect303(url_for(".create_project", project_id=project_id))
|
||||||
|
|
||||||
is_admin = session.get("is_admin")
|
is_admin = session.get("is_admin")
|
||||||
if session.get(project.id) or is_admin:
|
is_invitation = endpoint == "main.join_project"
|
||||||
|
if session.get(project.id) or is_admin or is_invitation:
|
||||||
# add project into kwargs and call the original function
|
# add project into kwargs and call the original function
|
||||||
g.project = project
|
g.project = project
|
||||||
else:
|
else:
|
||||||
|
@ -194,28 +196,38 @@ def admin():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/<project_id>/join/<string:token>", methods=["GET"])
|
||||||
|
def join_project(token):
|
||||||
|
project_id = g.project.id
|
||||||
|
verified_project_id = Project.verify_token(
|
||||||
|
token, token_type="auth", project_id=project_id
|
||||||
|
)
|
||||||
|
if verified_project_id != project_id:
|
||||||
|
flash(_("Provided token is invalid"), "danger")
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
# maintain a list of visited projects
|
||||||
|
if "projects" not in session:
|
||||||
|
session["projects"] = []
|
||||||
|
# add the project on the top of the list
|
||||||
|
session["projects"].insert(0, (project_id, g.project.name))
|
||||||
|
session[project_id] = True
|
||||||
|
# Set session to permanent to make language choice persist
|
||||||
|
session.permanent = True
|
||||||
|
session.update()
|
||||||
|
return redirect(url_for(".list_bills"))
|
||||||
|
|
||||||
|
|
||||||
@main.route("/authenticate", methods=["GET", "POST"])
|
@main.route("/authenticate", methods=["GET", "POST"])
|
||||||
def authenticate(project_id=None):
|
def authenticate(project_id=None):
|
||||||
"""Authentication form"""
|
"""Authentication form"""
|
||||||
form = AuthenticationForm()
|
form = AuthenticationForm()
|
||||||
# Try to get project_id from token first
|
|
||||||
token = request.args.get("token")
|
|
||||||
if token:
|
|
||||||
project_id = Project.verify_token(token, token_type="non_timed_token")
|
|
||||||
token_auth = True
|
|
||||||
else:
|
|
||||||
if not form.id.data and request.args.get("project_id"):
|
|
||||||
form.id.data = request.args["project_id"]
|
|
||||||
project_id = form.id.data
|
|
||||||
token_auth = False
|
|
||||||
if project_id is None:
|
|
||||||
# User doesn't provide project identifier or a valid token
|
|
||||||
# return to authenticate form
|
|
||||||
msg = _("You either provided a bad token or no project identifier.")
|
|
||||||
form["id"].errors = [msg]
|
|
||||||
return render_template("authenticate.html", form=form)
|
|
||||||
|
|
||||||
project = Project.query.get(project_id)
|
if not form.id.data and request.args.get("project_id"):
|
||||||
|
form.id.data = request.args["project_id"]
|
||||||
|
project_id = form.id.data
|
||||||
|
|
||||||
|
project = Project.query.get(project_id) if project_id is not None else None
|
||||||
if not project:
|
if not project:
|
||||||
# If the user try to connect to an unexisting project, we will
|
# If the user try to connect to an unexisting project, we will
|
||||||
# propose him a link to the creation form.
|
# propose him a link to the creation form.
|
||||||
|
@ -228,13 +240,9 @@ def authenticate(project_id=None):
|
||||||
setattr(g, "project", project)
|
setattr(g, "project", project)
|
||||||
return redirect(url_for(".list_bills"))
|
return redirect(url_for(".list_bills"))
|
||||||
|
|
||||||
# else do form authentication or token authentication
|
# else do form authentication authentication
|
||||||
is_post_auth = request.method == "POST" and form.validate()
|
is_post_auth = request.method == "POST" and form.validate()
|
||||||
if (
|
if is_post_auth and check_password_hash(project.password, form.password.data):
|
||||||
is_post_auth
|
|
||||||
and check_password_hash(project.password, form.password.data)
|
|
||||||
or token_auth
|
|
||||||
):
|
|
||||||
# maintain a list of visited projects
|
# maintain a list of visited projects
|
||||||
if "projects" not in session:
|
if "projects" not in session:
|
||||||
session["projects"] = []
|
session["projects"] = []
|
||||||
|
@ -253,9 +261,16 @@ def authenticate(project_id=None):
|
||||||
return render_template("authenticate.html", form=form)
|
return render_template("authenticate.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_form():
|
||||||
|
if current_app.config.get("ENABLE_CAPTCHA", False):
|
||||||
|
ProjectForm.enable_captcha()
|
||||||
|
|
||||||
|
return ProjectForm()
|
||||||
|
|
||||||
|
|
||||||
@main.route("/", strict_slashes=False)
|
@main.route("/", strict_slashes=False)
|
||||||
def home():
|
def home():
|
||||||
project_form = ProjectForm()
|
project_form = get_project_form()
|
||||||
auth_form = AuthenticationForm()
|
auth_form = AuthenticationForm()
|
||||||
is_demo_project_activated = current_app.config["ACTIVATE_DEMO_PROJECT"]
|
is_demo_project_activated = current_app.config["ACTIVATE_DEMO_PROJECT"]
|
||||||
is_public_project_creation_allowed = current_app.config[
|
is_public_project_creation_allowed = current_app.config[
|
||||||
|
@ -280,7 +295,7 @@ def mobile():
|
||||||
@main.route("/create", methods=["GET", "POST"])
|
@main.route("/create", methods=["GET", "POST"])
|
||||||
@requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
|
@requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
|
||||||
def create_project():
|
def create_project():
|
||||||
form = ProjectForm()
|
form = get_project_form()
|
||||||
if request.method == "GET" and "project_id" in request.values:
|
if request.method == "GET" and "project_id" in request.values:
|
||||||
form.name.data = request.values["project_id"]
|
form.name.data = request.values["project_id"]
|
||||||
|
|
||||||
|
@ -380,7 +395,7 @@ def reset_password():
|
||||||
return render_template(
|
return render_template(
|
||||||
"reset_password.html", form=form, error=_("No token provided")
|
"reset_password.html", form=form, error=_("No token provided")
|
||||||
)
|
)
|
||||||
project_id = Project.verify_token(token)
|
project_id = Project.verify_token(token, token_type="reset")
|
||||||
if not project_id:
|
if not project_id:
|
||||||
return render_template(
|
return render_template(
|
||||||
"reset_password.html", form=form, error=_("Invalid token")
|
"reset_password.html", form=form, error=_("Invalid token")
|
||||||
|
@ -412,8 +427,8 @@ def edit_project():
|
||||||
flash(_("Project successfully uploaded"))
|
flash(_("Project successfully uploaded"))
|
||||||
|
|
||||||
return redirect(url_for("main.list_bills"))
|
return redirect(url_for("main.list_bills"))
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
flash(_("Invalid JSON"), category="danger")
|
flash(e.args[0], category="danger")
|
||||||
|
|
||||||
# Edit form
|
# Edit form
|
||||||
if edit_form.validate_on_submit():
|
if edit_form.validate_on_submit():
|
||||||
|
@ -447,17 +462,37 @@ def import_project(file, project):
|
||||||
json_file = json.load(file)
|
json_file = json.load(file)
|
||||||
|
|
||||||
# Check if JSON is correct
|
# Check if JSON is correct
|
||||||
attr = ["what", "payer_name", "payer_weight", "amount", "date", "owers"]
|
attr = ["what", "payer_name", "payer_weight", "amount", "currency", "date", "owers"]
|
||||||
attr.sort()
|
attr.sort()
|
||||||
|
currencies = set()
|
||||||
for e in json_file:
|
for e in json_file:
|
||||||
|
# If currency is absent, empty, or explicitly set to XXX
|
||||||
|
# set it to project default.
|
||||||
|
if e.get("currency", "") in ["", "XXX"]:
|
||||||
|
e["currency"] = project.default_currency
|
||||||
if len(e) != len(attr):
|
if len(e) != len(attr):
|
||||||
raise ValueError
|
raise ValueError(_("Invalid JSON"))
|
||||||
list_attr = []
|
list_attr = []
|
||||||
for i in e:
|
for i in e:
|
||||||
list_attr.append(i)
|
list_attr.append(i)
|
||||||
list_attr.sort()
|
list_attr.sort()
|
||||||
if list_attr != attr:
|
if list_attr != attr:
|
||||||
raise ValueError
|
raise ValueError(_("Invalid JSON"))
|
||||||
|
# Keep track of currencies
|
||||||
|
currencies.add(e["currency"])
|
||||||
|
|
||||||
|
# Additional checks if project has no default currency
|
||||||
|
if project.default_currency == CurrencyConverter.no_currency:
|
||||||
|
# If bills have currencies, they must be consistent
|
||||||
|
if len(currencies - {CurrencyConverter.no_currency}) >= 2:
|
||||||
|
raise ValueError(
|
||||||
|
_(
|
||||||
|
"Cannot add bills in multiple currencies to a project without default currency"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Strip currency from bills (since it's the same for every bill)
|
||||||
|
for e in json_file:
|
||||||
|
e["currency"] = CurrencyConverter.no_currency
|
||||||
|
|
||||||
# From json : export list of members
|
# From json : export list of members
|
||||||
members_json = get_members(json_file)
|
members_json = get_members(json_file)
|
||||||
|
@ -505,10 +540,10 @@ def import_project(file, project):
|
||||||
form = get_billform_for(project)
|
form = get_billform_for(project)
|
||||||
form.what = b["what"]
|
form.what = b["what"]
|
||||||
form.amount = b["amount"]
|
form.amount = b["amount"]
|
||||||
|
form.original_currency = b["currency"]
|
||||||
form.date = parse(b["date"])
|
form.date = parse(b["date"])
|
||||||
form.payer = id_dict[b["payer_name"]]
|
form.payer = id_dict[b["payer_name"]]
|
||||||
form.payed_for = owers_id
|
form.payed_for = owers_id
|
||||||
form.original_currency = b.get("original_currency")
|
|
||||||
|
|
||||||
db.session.add(form.fake_form(bill, project))
|
db.session.add(form.fake_form(bill, project))
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ install_requires =
|
||||||
Flask-Migrate>=2.5.3,<4 # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b)
|
Flask-Migrate>=2.5.3,<4 # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b)
|
||||||
Flask-RESTful>=0.3.9,<1
|
Flask-RESTful>=0.3.9,<1
|
||||||
Flask-SQLAlchemy>=2.4,<3
|
Flask-SQLAlchemy>=2.4,<3
|
||||||
|
Flask-Talisman>=0.8,<1
|
||||||
Flask-WTF>=0.14.3,<1
|
Flask-WTF>=0.14.3,<1
|
||||||
WTForms>=2.3.1,<2.4
|
WTForms>=2.3.1,<2.4
|
||||||
Flask>=2,<3
|
Flask>=2,<3
|
||||||
|
@ -56,7 +57,7 @@ dev =
|
||||||
PyMySQL>=0.9,<1.1
|
PyMySQL>=0.9,<1.1
|
||||||
|
|
||||||
doc =
|
doc =
|
||||||
Sphinx==4.1.2
|
Sphinx==4.2.0
|
||||||
docutils==0.17.1
|
docutils==0.17.1
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
|
|
Loading…
Reference in a new issue