Compare commits

...

10 commits

Author SHA1 Message Date
Yida Chen
ec122223af
Merge 889576a114 into 794f26f767 2024-12-26 13:41:33 +01:00
ferid333
794f26f767 Update default_settings.py
Co-Authored-By: qurbanov <10328930+qurbanov@users.noreply.github.com>
2024-12-26 13:41:23 +01:00
ferid333
85eccb74b2 I added Azerbaijani Translation
I added Azerbaijani Translation

Co-Authored-By: qurbanov <10328930+qurbanov@users.noreply.github.com>
2024-12-26 13:41:23 +01:00
4b96e89422
Remove flake8 2024-12-26 08:35:49 +01:00
Jojo144
dcb61b62b1 Update contibuting.md with uv dependency 2024-12-26 08:33:51 +01:00
Jojo144
e00c39a62c Update ihatemoney/history.py
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2024-12-26 08:31:25 +01:00
Jojo144
2f099674ed Workaround to avoid history bug #1324 2024-12-26 08:31:25 +01:00
chestnutFan
889576a114
-Added tooltip for bills involving deactivated members
-Made involves_deactivated_members() a property of the Bill class
-Removed flashed messages
2024-12-20 01:09:45 +01:00
chestnutFan
72320c19d7
Moved the check function to the model 2024-12-20 01:03:33 +01:00
chestnutFan
d6d9fd2180
adding deactivated user check and according test function 2024-12-20 01:03:32 +01:00
10 changed files with 1332 additions and 55 deletions

View file

@ -78,6 +78,15 @@ Thanks again!
(setup-dev-environment)= (setup-dev-environment)=
## Set up a dev environment ## Set up a dev environment
### Requirements
In addition to general {ref}`requirements<system-requirements>`, you will need
**uv**. It recommended to install uv [system
wide](https://docs.astral.sh/uv/getting-started/installation/#standalone-installer)
as it is a kind of replacement for pip.
### Getting the sources
You must develop on top of the Git master branch: You must develop on top of the Git master branch:
git clone https://github.com/spiral-project/ihatemoney.git git clone https://github.com/spiral-project/ihatemoney.git

View file

@ -15,6 +15,7 @@ APPLICATION_ROOT = "/"
ENABLE_CAPTCHA = False ENABLE_CAPTCHA = False
LEGAL_LINK = "" LEGAL_LINK = ""
SUPPORTED_LANGUAGES = [ SUPPORTED_LANGUAGES = [
"az",
"ca", "ca",
"cs", "cs",
"de", "de",

View file

@ -38,7 +38,10 @@ def history_sort_key(history_item_dict):
def describe_version(version_obj): def describe_version(version_obj):
"""Use the base model str() function to describe a version object""" """Use the base model str() function to describe a version object"""
return parent_class(type(version_obj)).__str__(version_obj) if not version_obj:
return ""
else:
return parent_class(type(version_obj)).__str__(version_obj)
def describe_owers_change(version, human_readable_names): def describe_owers_change(version, human_readable_names):

View file

@ -752,6 +752,22 @@ class Bill(db.Model):
else: else:
return 0 return 0
@property
def involves_deactivated_members(self):
"""Check whether the bill contains deactivated member.
Return:
True if it contains deactivated member,
False if not.
"""
owers_id = [int(m.id) for m in self.owers]
bill_member_id_list = owers_id + [self.payer_id]
deactivated_member_number = (
Person.query.filter(Person.id.in_(bill_member_id_list))
.filter(Person.activated.is_(False))
.count()
)
return deactivated_member_number != 0
def __str__(self): def __str__(self):
return self.what return self.what

View file

@ -148,10 +148,22 @@
</span> </span>
</td> </td>
<td class="bill-actions d-flex align-items-center"> <td class="bill-actions d-flex align-items-center">
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a> <a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" data-toggle="tooltip"
{% if bill.involves_deactivated_members %}
title="Cannot be edited as deactivated members involved"
{% else %}
title="Click to edit this bill"
{% endif %}
>{{ _('edit') }}</a>
<form class="delete-bill" action="{{ url_for(".delete_bill", bill_id=bill.id) }}" method="POST"> <form class="delete-bill" action="{{ url_for(".delete_bill", bill_id=bill.id) }}" method="POST">
{{ csrf_form.csrf_token }} {{ csrf_form.csrf_token }}
<button class="action delete" type="submit" title="{{ _("delete") }}"></button> <button class="action delete" type="submit" data-toggle="tooltip"
{% if bill.involves_deactivated_members %}
title="Cannot be deleted as deactivated members involved"
{% else %}
title="Click to delete this bill"
{% endif %}
></button>
</form> </form>
{% if bill.external_link %} {% if bill.external_link %}
<a class="show" href="{{ bill.external_link }}" ref="noopener" target="_blank" title="{{ _("show") }}">{{ _('show') }} </a> <a class="show" href="{{ bill.external_link }}" ref="noopener" target="_blank" title="{{ _("show") }}">{{ _('show') }} </a>

View file

@ -872,6 +872,122 @@ class TestBudget(IhatemoneyTestCase):
balance = self.get_project("raclette").balance balance = self.get_project("raclette").balance
assert set(balance.values()) == set([6, -6]) assert set(balance.values()) == set([6, -6])
def test_edit_bill_with_deactivated_member(self):
"""
Bills involving deactivated members should not allowed to be edited or deleted.
"""
self.post_project("raclette")
# add two participants
self.client.post("/raclette/members/add", data={"name": "zorglub"})
self.client.post("/raclette/members/add", data={"name": "fred"})
members_ids = [m.id for m in self.get_project("raclette").members]
# create one bill
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"amount": "25",
},
)
bill = models.Bill.query.one()
self.assertEqual(bill.amount, 25)
# deactivate one user
self.client.post(
"/raclette/members/%s/delete" % self.get_project("raclette").members[-1].id
)
self.assertEqual(len(self.get_project("raclette").members), 2)
self.assertEqual(len(self.get_project("raclette").active_members), 1)
# editing would fail because the bill involves deactivated user
self.client.post(
f"/raclette/edit/{bill.id}",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"amount": "10",
},
)
bill = models.Bill.query.one()
self.assertNotEqual(bill.amount, 10, "bill edition")
# reactivate the user
self.client.post(
"/raclette/members/%s/reactivate"
% self.get_project("raclette").members[-1].id
)
self.assertEqual(len(self.get_project("raclette").active_members), 2)
# try to edit the bill again. It should succeed
self.client.post(
f"/raclette/edit/{bill.id}",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"amount": "10",
},
)
bill = models.Bill.query.one()
self.assertEqual(bill.amount, 10, "bill edition")
def test_delete_bill_with_deactivated_member(self):
"""
Bills involving deactivated members should not allowed to be edited or deleted.
"""
self.post_project("raclette")
# add two participants
self.client.post("/raclette/members/add", data={"name": "zorglub"})
self.client.post("/raclette/members/add", data={"name": "fred"})
members_ids = [m.id for m in self.get_project("raclette").members]
# create one bill
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"amount": "25",
},
)
bill = models.Bill.query.one()
self.assertEqual(bill.amount, 25)
# deactivate one user
self.client.post(
"/raclette/members/%s/delete" % self.get_project("raclette").members[-1].id
)
self.assertEqual(len(self.get_project("raclette").active_members), 1)
# deleting should fail because the bill involves deactivated user
response = self.client.get(f"/raclette/delete/{bill.id}")
self.assertEqual(response.status_code, 405)
self.assertEqual(1, len(models.Bill.query.all()), "bill deletion")
# reactivate the user
self.client.post(
"/raclette/members/%s/reactivate"
% self.get_project("raclette").members[-1].id
)
self.assertEqual(len(self.get_project("raclette").active_members), 2)
# try to delete the bill again. It should succeed
self.client.post(f"/raclette/delete/{bill.id}")
self.assertEqual(0, len(models.Bill.query.all()), "bill deletion")
def test_trimmed_members(self): def test_trimmed_members(self):
self.post_project("raclette") self.post_project("raclette")

File diff suppressed because it is too large Load diff

View file

@ -806,6 +806,10 @@ def delete_bill(bill_id):
if not bill: if not bill:
return redirect(url_for(".list_bills")) return redirect(url_for(".list_bills"))
# Check if the bill contains deactivated member. If yes, stop deleting.
if bill.involves_deactivated_members:
return redirect(url_for(".list_bills"))
db.session.delete(bill) db.session.delete(bill)
db.session.commit() db.session.commit()
flash(_("The bill has been deleted")) flash(_("The bill has been deleted"))
@ -820,6 +824,10 @@ def edit_bill(bill_id):
if not bill: if not bill:
raise NotFound() raise NotFound()
# Check if the bill contains deactivated member. If yes, stop editing.
if bill.involves_deactivated_members:
return redirect(url_for(".list_bills"))
form = get_billform_for(g.project, set_default=False) form = get_billform_for(g.project, set_default=False)
if request.method == "POST" and form.validate(): if request.method == "POST" and form.validate():

View file

@ -58,7 +58,6 @@ database = [
] ]
dev = [ dev = [
"ruff==0.8.4", "ruff==0.8.4",
"flake8==5.0.4",
"isort==5.11.5", "isort==5.11.5",
"vermin==1.6.0", "vermin==1.6.0",
"pytest>=6.2.5", "pytest>=6.2.5",

60
uv.lock
View file

@ -439,20 +439,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
] ]
[[package]]
name = "flake8"
version = "5.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mccabe" },
{ name = "pycodestyle" },
{ name = "pyflakes" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", size = 145862 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248", size = 61897 },
]
[[package]] [[package]]
name = "flask" name = "flask"
version = "2.3.3" version = "2.3.3"
@ -708,7 +694,6 @@ database = [
{ name = "pymysql" }, { name = "pymysql" },
] ]
dev = [ dev = [
{ name = "flake8" },
{ name = "isort" }, { name = "isort" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-flask" }, { name = "pytest-flask" },
@ -729,8 +714,7 @@ requires-dist = [
{ name = "debts", specifier = ">=0.5,<1" }, { name = "debts", specifier = ">=0.5,<1" },
{ name = "docutils", marker = "extra == 'doc'", specifier = "==0.20.1" }, { name = "docutils", marker = "extra == 'doc'", specifier = "==0.20.1" },
{ name = "email-validator", specifier = ">=1.0,<3" }, { name = "email-validator", specifier = ">=1.0,<3" },
{ name = "flake8", marker = "extra == 'dev'", specifier = "==5.0.4" }, { name = "flask", specifier = ">=2,<4" },
{ name = "flask", specifier = ">=2,<3" },
{ name = "flask-babel", specifier = ">=1.0,<4" }, { name = "flask-babel", specifier = ">=1.0,<4" },
{ name = "flask-cors", specifier = ">=3.0.8,<4" }, { name = "flask-cors", specifier = ">=3.0.8,<4" },
{ name = "flask-limiter", specifier = ">=2.6,<3" }, { name = "flask-limiter", specifier = ">=2.6,<3" },
@ -743,21 +727,21 @@ requires-dist = [
{ name = "isort", marker = "extra == 'dev'", specifier = "==5.11.5" }, { name = "isort", marker = "extra == 'dev'", specifier = "==5.11.5" },
{ name = "itsdangerous", specifier = ">=2,<3" }, { name = "itsdangerous", specifier = ">=2,<3" },
{ name = "jinja2", specifier = ">=3,<4" }, { name = "jinja2", specifier = ">=3,<4" },
{ name = "myst-parser", marker = "extra == 'doc'", specifier = ">=2,<3" }, { name = "myst-parser", marker = "extra == 'doc'", specifier = ">=2,<5" },
{ name = "psycopg2-binary", marker = "extra == 'database'", specifier = ">=2.9.2,<2.9.9" }, { name = "psycopg2-binary", marker = "extra == 'database'", specifier = ">=2.9.2,<2.9.9" },
{ name = "pymysql", marker = "extra == 'database'", specifier = ">=0.9,<1.1" }, { name = "pymysql", marker = "extra == 'database'", specifier = ">=0.9,<1.2" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=6.2.5" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.2.5" },
{ name = "pytest-flask", marker = "extra == 'dev'", specifier = ">=1.2.0" }, { name = "pytest-flask", marker = "extra == 'dev'", specifier = ">=1.2.0" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "qrcode", specifier = ">=7.1,<8" }, { name = "qrcode", specifier = ">=7.1,<8" },
{ name = "requests", specifier = ">=2.25,<3" }, { name = "requests", specifier = ">=2.25,<3" },
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.6.8" }, { name = "ruff", marker = "extra == 'dev'", specifier = "==0.6.8" },
{ name = "sphinx", marker = "extra == 'doc'", specifier = ">=7.0.1,<8" }, { name = "sphinx", marker = "extra == 'doc'", specifier = ">=7.0.1,<9" },
{ name = "sqlalchemy", specifier = ">=1.3.0,<1.5" }, { name = "sqlalchemy", specifier = ">=1.3.0,<1.5" },
{ name = "sqlalchemy-continuum", specifier = ">=1.3.12,<2" }, { name = "sqlalchemy-continuum", specifier = ">=1.3.12,<2" },
{ name = "vermin", marker = "extra == 'dev'", specifier = "==1.5.2" }, { name = "vermin", marker = "extra == 'dev'", specifier = "==1.6.0" },
{ name = "werkzeug", specifier = ">=2,<3" }, { name = "werkzeug", specifier = ">=2,<3" },
{ name = "wtforms", specifier = ">=2.3.3,<3.2" }, { name = "wtforms", specifier = ">=2.3.3,<3.3" },
{ name = "zest-releaser", marker = "extra == 'dev'", specifier = ">=6.20.1" }, { name = "zest-releaser", marker = "extra == 'dev'", specifier = ">=6.20.1" },
] ]
@ -994,15 +978,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 }, { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 },
] ]
[[package]]
name = "mccabe"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 },
]
[[package]] [[package]]
name = "mdit-py-plugins" name = "mdit-py-plugins"
version = "0.4.2" version = "0.4.2"
@ -1165,15 +1140,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/56/39189b7bf61744a627c18f3a919647f7eac78d849de1f8b2ad1a5c11f824/psycopg2_binary-2.9.8-cp39-cp39-win_amd64.whl", hash = "sha256:1f279ba74f0d6b374526e5976c626d2ac3b8333b6a7b08755c513f4d380d3add", size = 1177351 }, { url = "https://files.pythonhosted.org/packages/e7/56/39189b7bf61744a627c18f3a919647f7eac78d849de1f8b2ad1a5c11f824/psycopg2_binary-2.9.8-cp39-cp39-win_amd64.whl", hash = "sha256:1f279ba74f0d6b374526e5976c626d2ac3b8333b6a7b08755c513f4d380d3add", size = 1177351 },
] ]
[[package]]
name = "pycodestyle"
version = "2.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493 },
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.22" version = "2.22"
@ -1183,15 +1149,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
] ]
[[package]]
name = "pyflakes"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3", size = 66388 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", size = 66116 },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.18.0" version = "2.18.0"
@ -1695,10 +1652,11 @@ wheels = [
[[package]] [[package]]
name = "vermin" name = "vermin"
version = "1.5.2" version = "1.6.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3d/26/7b871396c33006c445c25ef7da605ecbd6cef830d577b496d2b73a554f9d/vermin-1.6.0.tar.gz", hash = "sha256:6266ca02f55d1c2aa189a610017c132eb2d1934f09e72a955b1eb3820ee6d4ef", size = 93181 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/46/ed/420955392d9c2743c93e0418928927e34aba355c716a70c6bdba209b930f/vermin-1.5.2-py2.py3-none-any.whl", hash = "sha256:c1566ad4e1c8e1b0e98cf5f7d69b691d44a578e2ce9c5aa1d418736bc4944b32", size = 89266 }, { url = "https://files.pythonhosted.org/packages/2e/98/1a2ca43e6d646421eea16ec19977e2e6d1ea9079bd9d873bfae513d43f1c/vermin-1.6.0-py2.py3-none-any.whl", hash = "sha256:f1fa9ee40f59983dc40e0477eb2b1fa8061a3df4c3b2bcf349add462a5610efb", size = 90845 },
] ]
[[package]] [[package]]