diff --git a/.github/workflows/check-doc.yml b/.github/workflows/check-doc.yml new file mode 100644 index 000000000..06dcf3769 --- /dev/null +++ b/.github/workflows/check-doc.yml @@ -0,0 +1,26 @@ +name: Check doc + +on: + push: + branches: [ 'master', 'stable-*' ] + pull_request: + branches: [ 'master', 'stable-*' ] + +jobs: + + test_doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + cache: 'pip' + cache-dependency-path: '**/pyproject.toml' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Check we can generate documentation + run: tox -e docs diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index c8943b9d1..6c8a47b20 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -1,4 +1,4 @@ -name: CI to Docker Hub +name: Docker build on: push: @@ -18,7 +18,7 @@ jobs: - name: Test image # Checks we are able to run the container. run: docker compose -f docker-compose.test.yml run sut - build: + build_upload: runs-on: ubuntu-latest needs: test if: github.event_name != 'pull_request' diff --git a/.github/workflows/test-docs.yml b/.github/workflows/lint-and-tests.yml similarity index 68% rename from .github/workflows/test-docs.yml rename to .github/workflows/lint-and-tests.yml index 372ca8ae5..23f71fdc5 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/lint-and-tests.yml @@ -1,4 +1,4 @@ -name: Test & Docs +name: Lint & unit tests on: push: @@ -7,14 +7,31 @@ on: branches: [ 'master', 'stable-*' ] jobs: - build: - + lint: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + cache: 'pip' + cache-dependency-path: '**/pyproject.toml' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run Lint + run: tox -e lint - # Use postgresql and MariaDB versions of Debian buster + test: + # Dependency on linting to avoid running our expensive matrix test for nothing + needs: lint + runs-on: ubuntu-latest + # Use postgresql and MariaDB versions of Debian bookworm services: postgres: - image: postgres:11 + image: postgres:15 ports: - 5432:5432 env: @@ -24,7 +41,7 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mariadb: - image: mariadb:10.3 + image: mariadb:10.11 env: MARIADB_ROOT_PASSWORD: ihatemoney MARIADB_DATABASE: ihatemoney_ci @@ -39,24 +56,29 @@ jobs: python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] dependencies: [normal] database: [sqlite] - # Test other databases only with one version of Python (Debian buster has 3.7) + # Installation breaks with python 3.12, see https://github.com/spiral-project/ihatemoney/issues/1297 + exclude: + - python-version: "3.12" + dependencies: normal + database: sqlite + # Test other databases with only a few versions of Python (Debian bullseye has 3.9, bookworm has 3.11) include: - - python-version: 3.7 + - python-version: 3.9 dependencies: normal database: postgresql - - python-version: 3.7 + - python-version: 3.9 + dependencies: normal + database: mariadb + - python-version: 3.11 + dependencies: normal + database: postgresql + - python-version: 3.11 dependencies: normal database: mariadb # Try a few variants with the minimal versions supported - python-version: 3.7 dependencies: minimal database: sqlite - - python-version: 3.7 - dependencies: minimal - database: postgresql - - python-version: 3.7 - dependencies: minimal - database: mariadb - python-version: 3.9 dependencies: minimal database: sqlite @@ -66,6 +88,12 @@ jobs: - python-version: "3.11" dependencies: minimal database: sqlite + - python-version: "3.11" + dependencies: minimal + database: postgresql + - python-version: "3.11" + dependencies: minimal + database: mariadb - python-version: "3.12" dependencies: minimal database: sqlite @@ -101,6 +129,3 @@ jobs: if: matrix.database == 'mariadb' env: TESTING_SQLALCHEMY_DATABASE_URI: 'mysql+pymysql://root:ihatemoney@localhost:3306/ihatemoney_ci' - - name: Run Lint & Docs - run: tox -e lint_docs - if: matrix.python-version == '3.12' diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index 6a787e214..e1ddabe87 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -364,7 +364,12 @@ class BillForm(FlaskForm): payed_for = SelectMultipleField( _("For whom?"), validators=[DataRequired()], coerce=int ) - bill_type = SelectField(_("Bill Type"), choices=BillType.choices(), coerce=BillType, default=BillType.EXPENSE) + bill_type = SelectField( + _("Bill Type"), + choices=BillType.choices(), + coerce=BillType, + default=BillType.EXPENSE, + ) submit = SubmitField(_("Submit")) submit2 = SubmitField(_("Submit and add a new one")) diff --git a/ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py b/ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py index 9b1fc520b..d7e273306 100644 --- a/ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py +++ b/ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py @@ -19,7 +19,10 @@ def upgrade(): billtype_enum = sa.Enum(BillType) billtype_enum.create(op.get_bind(), checkfirst=True) - op.add_column("bill", sa.Column("bill_type", billtype_enum, server_default=BillType.EXPENSE.name)) + op.add_column( + "bill", + sa.Column("bill_type", billtype_enum, server_default=BillType.EXPENSE.name), + ) op.add_column("bill_version", sa.Column("bill_type", sa.UnicodeText())) @@ -28,4 +31,4 @@ def downgrade(): op.drop_column("bill_version", "bill_type") billtype_enum = sa.Enum(BillType) - billtype_enum.drop(op.get_bind()) \ No newline at end of file + billtype_enum.drop(op.get_bind()) diff --git a/ihatemoney/models.py b/ihatemoney/models.py index fcc172647..c7dee1856 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -1,6 +1,6 @@ from collections import defaultdict -from enum import Enum import datetime +from enum import Enum import itertools from dateutil.parser import parse @@ -22,7 +22,7 @@ from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.monkeypath_continuum import PatchedTransactionFactory -from ihatemoney.utils import generate_password_hash, get_members, same_bill, FormEnum +from ihatemoney.utils import generate_password_hash, get_members, same_bill from ihatemoney.versioning import ( ConditionalVersioningManager, LoggingMode, @@ -51,6 +51,7 @@ ], ) + class BillType(Enum): EXPENSE = "Expense" REIMBURSEMENT = "Reimbursement" @@ -131,7 +132,9 @@ def full_balance(self): if bill.bill_type == BillType.EXPENSE: should_receive[bill.payer.id] += bill.converted_amount for ower in bill.owers: - should_pay[ower.id] += (ower.weight * bill.converted_amount / total_weight) + should_pay[ower.id] += ( + ower.weight * bill.converted_amount / total_weight + ) if bill.bill_type == BillType.REIMBURSEMENT: should_receive[bill.payer.id] += bill.converted_amount @@ -563,7 +566,7 @@ def create_demo_project(): ("Alice", 20, ("Amina", "Alice"), "Beer !", "Expense"), ("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP", "Expense"), ) - for (payer, amount, owers, what, bill_type) in operations: + for payer, amount, owers, what, bill_type in operations: db.session.add( Bill( amount=amount, diff --git a/ihatemoney/tests/api_test.py b/ihatemoney/tests/api_test.py index ff94ebe7e..191790b64 100644 --- a/ihatemoney/tests/api_test.py +++ b/ihatemoney/tests/api_test.py @@ -1017,7 +1017,6 @@ def test_amount_too_high(self): def test_validate_bill_type(self): self.api_create("raclette") self.api_add_member("raclette", "zorglub") - req = self.client.post( "/api/projects/raclette/bills", @@ -1029,7 +1028,7 @@ def test_validate_bill_type(self): "bill_type": "wrong_bill_type", "amount": "50", }, - headers=self.get_auth("raclette") + headers=self.get_auth("raclette"), ) self.assertStatus(400, req) @@ -1044,7 +1043,7 @@ def test_validate_bill_type(self): "bill_type": "Expense", "amount": "50", }, - headers=self.get_auth("raclette") + headers=self.get_auth("raclette"), ) self.assertStatus(201, req) @@ -1063,7 +1062,7 @@ def test_default_bill_type(self): "payed_for": ["1"], "amount": "50", }, - headers=self.get_auth("raclette") + headers=self.get_auth("raclette"), ) self.assertStatus(201, req) @@ -1076,4 +1075,3 @@ def test_default_bill_type(self): # Bill type should now be "Expense" got = json.loads(req.data.decode("utf-8")) assert got["bill_type"] == "Expense" - diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py index 9ddeec8a8..e64090b92 100644 --- a/ihatemoney/tests/budget_test.py +++ b/ihatemoney/tests/budget_test.py @@ -716,8 +716,8 @@ def test_manage_bills(self): "amount": "17", }, ) - - #transfer bill should not affect balances at all + + # transfer bill should not affect balances at all self.client.post( "/raclette/add", data={ @@ -801,22 +801,22 @@ def test_reimbursement_bill(self): self.client.post("/rent/members/add", data={"name": "alice"}) members_ids = [m.id for m in self.get_project("rent").members] - # create a bill to test reimbursement + # create a bill to test reimbursement self.client.post( "/rent/add", data={ "date": "2022-12-12", "what": "december rent", - "payer": members_ids[0], #bob - "payed_for": members_ids, #bob and alice + "payer": members_ids[0], # bob + "payed_for": members_ids, # bob and alice "bill_type": "Expense", "amount": "1000", }, ) - #check balance + # check balance balance = self.get_project("rent").balance assert set(balance.values()), set([500 == -500]) - #check paid + # check paid bob_paid = self.get_project("rent").full_balance[2][members_ids[0]] alice_paid = self.get_project("rent").full_balance[2][members_ids[1]] assert bob_paid == 1000 @@ -828,8 +828,8 @@ def test_reimbursement_bill(self): data={ "date": "2022-12-13", "what": "reimbursement for rent", - "payer": members_ids[1], #alice - "payed_for": members_ids[0], #bob + "payer": members_ids[1], # alice + "payed_for": members_ids[0], # bob "bill_type": "Reimbursement", "amount": "500", }, @@ -837,94 +837,12 @@ def test_reimbursement_bill(self): balance = self.get_project("rent").balance assert set(balance.values()), set([0 == 0]) - #check paid + # check paid bob_paid = self.get_project("rent").full_balance[2][members_ids[0]] alice_paid = self.get_project("rent").full_balance[2][members_ids[1]] assert bob_paid == 500 assert alice_paid == 500 - def test_transfer_bill(self): - self.post_project("random") - # add two participants - self.client.post("/random/members/add", data={"name": "zorglub"}) - self.client.post("/random/members/add", data={"name": "fred"}) - - members_ids = [m.id for m in self.get_project("random").members] - self.client.post( - "/random/add", - data={ - "date": "2022-10-10", - "what": "Rent", - "payer": members_ids[0], #zorglub - "payed_for": members_ids, #zorglub + fred - "bill_type": "Expense", - "amount": "1000", - }, - ) - # test transfer bill (should not affect anything whatsoever) - self.client.post( - "/random/add", - data={ - "date": "2022-10-10", - "what": "Transfer of 500 to fred", - "payer": members_ids[0], #zorglub - "payed_for": members_ids[1], #fred - "bill_type": "Transfer", - "amount": "500", - }, - ) - balance = self.get_project("random").balance - assert set(balance.values()), set([500 == -500]) - - def test_reimbursement_bill(self): - self.post_project("rent") - - # add two participants - self.client.post("/rent/members/add", data={"name": "bob"}) - self.client.post("/rent/members/add", data={"name": "alice"}) - - members_ids = [m.id for m in self.get_project("rent").members] - # create a bill to test reimbursement - self.client.post( - "/rent/add", - data={ - "date": "2022-12-12", - "what": "december rent", - "payer": members_ids[0], #bob - "payed_for": members_ids, #bob and alice - "bill_type": "Expense", - "amount": "1000", - }, - ) - #check balance - balance = self.get_project("rent").balance - assert set(balance.values()), set([500 == -500]) - #check paid - bob_paid = self.get_project("rent").full_balance[2][members_ids[0]] - alice_paid = self.get_project("rent").full_balance[2][members_ids[1]] - assert bob_paid == 1000 - assert alice_paid == 0 - - # test reimbursement bill - self.client.post( - "/rent/add", - data={ - "date": "2022-12-13", - "what": "reimbursement for rent", - "payer": members_ids[1], #alice - "payed_for": members_ids[0], #bob - "bill_type": "Reimbursement", - "amount": "500", - }, - ) - - balance = self.get_project("rent").balance - assert set(balance.values()), set([0 == 0]) - #check paid - bob_paid = self.get_project("rent").full_balance[2][members_ids[0]] - alice_paid = self.get_project("rent").full_balance[2][members_ids[1]] - assert bob_paid == 500 - assert alice_paid == 500 def test_transfer_bill(self): self.post_project("random") @@ -938,8 +856,8 @@ def test_transfer_bill(self): data={ "date": "2022-10-10", "what": "Rent", - "payer": members_ids[0], #zorglub - "payed_for": members_ids, #zorglub + fred + "payer": members_ids[0], # zorglub + "payed_for": members_ids, # zorglub + fred "bill_type": "Expense", "amount": "1000", }, @@ -950,8 +868,8 @@ def test_transfer_bill(self): data={ "date": "2022-10-10", "what": "Transfer of 500 to fred", - "payer": members_ids[0], #zorglub - "payed_for": members_ids[1], #fred + "payer": members_ids[0], # zorglub + "payed_for": members_ids[1], # fred "bill_type": "Transfer", "amount": "500", }, @@ -1433,7 +1351,7 @@ def test_settle(self): for m, a in members.items(): assert abs(a - balance[m.id]) < 0.01 return - + def test_settle_button(self): self.post_project("raclette") @@ -1482,16 +1400,24 @@ def test_settle_button(self): ) project = self.get_project("raclette") transactions = project.get_transactions_to_settle_bill() - + count = 0 for t in transactions: - count+=1 - self.client.get("/raclette/settle"+"/"+str(t["amount"])+"/"+str(t["ower"].id)+"/"+str(t["receiver"].id)) + count += 1 + self.client.get( + "/raclette/settle" + + "/" + + str(t["amount"]) + + "/" + + str(t["ower"].id) + + "/" + + str(t["receiver"].id) + ) temp_transactions = project.get_transactions_to_settle_bill() - #test if the one has disappeared - assert len(temp_transactions) == len(transactions)-count - - #test if theres a new one with bill_type reimbursement + # test if the one has disappeared + assert len(temp_transactions) == len(transactions) - count + + # test if theres a new one with bill_type reimbursement bill = project.get_newest_bill() assert bill.bill_type == models.BillType.REIMBURSEMENT return @@ -2005,7 +1931,7 @@ def test_rss_feed(self): "payed_for": [1, 2, 3], "amount": "12", "original_currency": "EUR", - "bill_type": "Expense" + "bill_type": "Expense", }, ) self.client.post( @@ -2017,7 +1943,7 @@ def test_rss_feed(self): "payed_for": [1, 2], "amount": "15", "original_currency": "EUR", - "bill_type": "Expense" + "bill_type": "Expense", }, ) self.client.post( @@ -2029,7 +1955,7 @@ def test_rss_feed(self): "payed_for": [1, 2], "amount": "10", "original_currency": "EUR", - "bill_type": "Expense" + "bill_type": "Expense", }, ) @@ -2092,7 +2018,7 @@ def test_rss_feed_history_disabled(self): "payed_for": [1, 2, 3], "amount": "12", "original_currency": "EUR", - "bill_type": "Expense" + "bill_type": "Expense", }, ) self.client.post( @@ -2104,7 +2030,7 @@ def test_rss_feed_history_disabled(self): "payed_for": [1, 2], "amount": "15", "original_currency": "EUR", - "bill_type": "Expense" + "bill_type": "Expense", }, ) self.client.post( @@ -2116,7 +2042,7 @@ def test_rss_feed_history_disabled(self): "payed_for": [1, 2], "amount": "10", "original_currency": "EUR", - "bill_type": "Expense" + "bill_type": "Expense", }, ) @@ -2195,7 +2121,7 @@ def test_rss_if_modified_since_header(self): "payed_for": [1], "amount": "12", "original_currency": "XXX", - "bill_type": "Expense" + "bill_type": "Expense", }, follow_redirects=True, ) @@ -2364,7 +2290,7 @@ def test_remember_payer_per_project(self): "payer": members_ids[1], "payed_for": members_ids, "amount": "25", - "bill_type": "Expense" + "bill_type": "Expense", }, ) @@ -2382,7 +2308,7 @@ def test_remember_payer_per_project(self): "payer": members_ids_tartif[2], "payed_for": members_ids_tartif, "amount": "24", - "bill_type": "Expense" + "bill_type": "Expense", }, ) @@ -2417,7 +2343,7 @@ def test_remember_payed_for(self): "payer": members_ids[1], "payed_for": members_ids[1:], "amount": "25", - "bill_type": "Expense" + "bill_type": "Expense", }, ) diff --git a/tox.ini b/tox.ini index fe2c9ad99..8a0421faa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = true -envlist = py312,py311,py310,py39,py38,py37,lint_docs +envlist = py312,py311,py310,py39,py38,py37,lint,docs skip_missing_interpreters = True [testenv] @@ -16,15 +16,21 @@ deps = # To be sure we are importing ihatemoney pkg from pip-installed version changedir = /tmp -[testenv:lint_docs] +[testenv:lint] commands = black --check --target-version=py37 . isort --check --profile black . flake8 ihatemoney vermin --no-tips --violations -t=3.7- . +deps = + -e.[dev] +changedir = {toxinidir} + +[testenv:docs] +commands = sphinx-build -a -n -b html -d docs/_build/doctrees docs docs/_build/html deps = - -e.[dev,doc] + -e.[doc] changedir = {toxinidir} [flake8]