mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-05-02 21:44:19 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
248fec8b4c | ||
|
|
b34c0557fa | ||
|
|
2af4066aab | ||
|
|
d72ff3cdf5 | ||
|
|
63c69e5c6a | ||
|
|
78171183cc | ||
|
|
34a2b6bfd4 | ||
|
|
1dc24f855e | ||
|
|
1390aff07d |
@@ -90,10 +90,10 @@ class AccountBalanceAPITests(TestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_get_balance_unauthenticated(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
response = unauthenticated_client.get(
|
||||
f"/api/accounts/{self.account.id}/balance/"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -159,7 +159,7 @@ column_mapping:
|
||||
self.assertIn("import_run_id", response.data)
|
||||
|
||||
def test_unauthenticated_request(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
|
||||
csv_content = b"date,description,amount\n2025-01-01,Test,100"
|
||||
@@ -173,7 +173,7 @@ column_mapping:
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
@override_settings(
|
||||
@@ -266,11 +266,11 @@ column_mapping:
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_profiles_unauthenticated(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
response = unauthenticated_client.get("/api/import/profiles/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
@override_settings(
|
||||
@@ -397,8 +397,8 @@ column_mapping:
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_runs_unauthenticated(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
response = unauthenticated_client.get("/api/import/runs/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -1,6 +1,47 @@
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
import procrastinate
|
||||
from django.db import close_old_connections
|
||||
|
||||
|
||||
_CONNECTION_CLEANUP_WRAPPED = "_wygiwyh_connection_cleanup_wrapped"
|
||||
|
||||
|
||||
def _wrap_task_with_django_connection_cleanup(task):
|
||||
if getattr(task.func, _CONNECTION_CLEANUP_WRAPPED, False):
|
||||
return
|
||||
|
||||
func = task.func
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
async def async_wrapped(*args, **kwargs):
|
||||
close_old_connections()
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
finally:
|
||||
close_old_connections()
|
||||
|
||||
wrapped = async_wrapped
|
||||
else:
|
||||
|
||||
@functools.wraps(func)
|
||||
def sync_wrapped(*args, **kwargs):
|
||||
close_old_connections()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
close_old_connections()
|
||||
|
||||
wrapped = sync_wrapped
|
||||
|
||||
setattr(wrapped, _CONNECTION_CLEANUP_WRAPPED, True)
|
||||
task.func = wrapped
|
||||
|
||||
|
||||
def on_app_ready(app: procrastinate.App):
|
||||
"""This function is ran upon procrastinate initialization."""
|
||||
...
|
||||
for task in set(app.tasks.values()):
|
||||
_wrap_task_with_django_connection_cleanup(task)
|
||||
|
||||
1
app/apps/common/tests/__init__.py
Normal file
1
app/apps/common/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
89
app/apps/common/tests/test_procrastinate.py
Normal file
89
app/apps/common/tests/test_procrastinate.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import procrastinate
|
||||
from django.db import connection
|
||||
from django.test import SimpleTestCase, TransactionTestCase
|
||||
from procrastinate.testing import InMemoryConnector
|
||||
|
||||
from apps.common.procrastinate import on_app_ready
|
||||
|
||||
|
||||
def make_app_with_task(func):
|
||||
app = procrastinate.App(connector=InMemoryConnector())
|
||||
task = app.task(name="sample_task")(func)
|
||||
|
||||
return app, task
|
||||
|
||||
|
||||
class ProcrastinateConnectionCleanupTests(SimpleTestCase):
|
||||
def test_app_ready_closes_old_connections_around_sync_tasks(self):
|
||||
calls = []
|
||||
|
||||
def sample_task(value):
|
||||
calls.append(("task", value))
|
||||
return value * 2
|
||||
|
||||
app, task = make_app_with_task(sample_task)
|
||||
|
||||
with patch(
|
||||
"apps.common.procrastinate.close_old_connections",
|
||||
create=True,
|
||||
side_effect=lambda: calls.append(("cleanup", None)),
|
||||
):
|
||||
on_app_ready(app)
|
||||
|
||||
result = task.func(3)
|
||||
|
||||
self.assertEqual(result, 6)
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
("cleanup", None),
|
||||
("task", 3),
|
||||
("cleanup", None),
|
||||
],
|
||||
)
|
||||
|
||||
def test_app_ready_closes_old_connections_when_sync_task_raises(self):
|
||||
calls = []
|
||||
|
||||
def sample_task():
|
||||
calls.append(("task", None))
|
||||
raise RuntimeError("boom")
|
||||
|
||||
app, task = make_app_with_task(sample_task)
|
||||
|
||||
with patch(
|
||||
"apps.common.procrastinate.close_old_connections",
|
||||
create=True,
|
||||
side_effect=lambda: calls.append(("cleanup", None)),
|
||||
):
|
||||
on_app_ready(app)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
task.func()
|
||||
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
("cleanup", None),
|
||||
("task", None),
|
||||
("cleanup", None),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class ProcrastinateConnectionRecoveryTests(TransactionTestCase):
|
||||
def test_wrapped_task_recovers_from_closed_django_connection(self):
|
||||
def sample_task():
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
app, task = make_app_with_task(sample_task)
|
||||
on_app_ready(app)
|
||||
|
||||
connection.ensure_connection()
|
||||
connection.connection.close()
|
||||
|
||||
self.assertEqual(task.func(), 1)
|
||||
@@ -365,7 +365,9 @@ def check_for_transaction_rules(
|
||||
|
||||
if processed_action.set_category:
|
||||
value = simple.eval(processed_action.set_category)
|
||||
if isinstance(value, int):
|
||||
if value is None:
|
||||
transaction.category = None
|
||||
elif isinstance(value, int):
|
||||
transaction.category = TransactionCategory.objects.get(id=value)
|
||||
else:
|
||||
transaction.category = TransactionCategory.objects.get(name=value)
|
||||
@@ -458,7 +460,9 @@ def check_for_transaction_rules(
|
||||
transaction.account = account
|
||||
|
||||
elif field == TransactionRuleAction.Field.category:
|
||||
if isinstance(new_value, int):
|
||||
if new_value is None:
|
||||
transaction.category = None
|
||||
elif isinstance(new_value, int):
|
||||
category = TransactionCategory.objects.get(id=new_value)
|
||||
transaction.category = category
|
||||
elif isinstance(new_value, str):
|
||||
|
||||
1
app/apps/rules/tests/__init__.py
Normal file
1
app/apps/rules/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
82
app/apps/rules/tests/test_tasks.py
Normal file
82
app/apps/rules/tests/test_tasks.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from apps.accounts.models import Account
|
||||
from apps.currencies.models import Currency
|
||||
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
|
||||
from apps.rules.tasks import check_for_transaction_rules
|
||||
from apps.transactions.models import Transaction
|
||||
|
||||
|
||||
def run_check_for_transaction_rules_without_worker_wrapper(**kwargs):
|
||||
task_func = check_for_transaction_rules.func
|
||||
task_func = getattr(task_func, "__wrapped__", task_func)
|
||||
|
||||
return task_func(**kwargs)
|
||||
|
||||
|
||||
class CheckForTransactionRulesTests(TransactionTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(
|
||||
email="rules@example.com",
|
||||
password="testpass123",
|
||||
)
|
||||
self.currency = Currency.objects.create(
|
||||
code="USD",
|
||||
name="US Dollar",
|
||||
decimal_places=2,
|
||||
)
|
||||
self.account = Account.objects.create(
|
||||
name="Main Account",
|
||||
currency=self.currency,
|
||||
owner=self.user,
|
||||
)
|
||||
|
||||
@patch("apps.rules.signals.check_for_transaction_rules.defer")
|
||||
def test_update_or_create_action_can_clear_category_from_none_expression(
|
||||
self, mock_defer
|
||||
):
|
||||
source_transaction = Transaction.objects.create(
|
||||
account=self.account,
|
||||
type=Transaction.Type.EXPENSE,
|
||||
amount=Decimal("10.00"),
|
||||
date=date(2026, 5, 4),
|
||||
reference_date=date(2026, 5, 1),
|
||||
description="Source without category",
|
||||
category=None,
|
||||
owner=self.user,
|
||||
)
|
||||
rule = TransactionRule.objects.create(
|
||||
active=True,
|
||||
on_create=False,
|
||||
on_update=True,
|
||||
name="Copy transaction",
|
||||
trigger="True",
|
||||
owner=self.user,
|
||||
)
|
||||
UpdateOrCreateTransactionRuleAction.objects.create(
|
||||
rule=rule,
|
||||
set_account="account_id",
|
||||
set_type="'EX'",
|
||||
set_date="date",
|
||||
set_reference_date="reference_date",
|
||||
set_amount="amount",
|
||||
set_description="'Generated transaction'",
|
||||
set_category="category_name",
|
||||
)
|
||||
|
||||
run_check_for_transaction_rules_without_worker_wrapper(
|
||||
instance_id=source_transaction.id,
|
||||
user_id=self.user.id,
|
||||
signal="transaction_updated",
|
||||
)
|
||||
|
||||
generated_transaction = Transaction.objects.get(
|
||||
description="Generated transaction"
|
||||
)
|
||||
self.assertIsNone(generated_transaction.category)
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-16 02:24+0000\n"
|
||||
"PO-Revision-Date: 2026-03-31 13:24+0000\n"
|
||||
"PO-Revision-Date: 2026-05-01 07:24+0000\n"
|
||||
"Last-Translator: masttera <mail.masttera@gmail.com>\n"
|
||||
"Language-Team: Russian <https://translations.herculino.com/projects/wygiwyh/"
|
||||
"app/ru/>\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"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 5.16.2\n"
|
||||
"X-Generator: Weblate 5.17\n"
|
||||
|
||||
#: apps/accounts/forms.py:24
|
||||
msgid "Group name"
|
||||
@@ -1513,7 +1513,7 @@ msgstr ""
|
||||
#: templates/insights/fragments/category_overview/index.html:87
|
||||
#: templates/monthly_overview/fragments/monthly_summary.html:41
|
||||
msgid "Income"
|
||||
msgstr ""
|
||||
msgstr "Доход"
|
||||
|
||||
#: apps/transactions/models.py:289 apps/transactions/models.py:988
|
||||
#: templates/calendar_view/fragments/list.html:46
|
||||
@@ -2178,7 +2178,7 @@ msgstr "Вы уверены?"
|
||||
#: templates/rules/fragments/transaction_rule/view.html:97
|
||||
#: templates/tags/fragments/table.html:56
|
||||
msgid "You won't be able to revert this!"
|
||||
msgstr ""
|
||||
msgstr "Вы не сможете отменить это!"
|
||||
|
||||
#: templates/account_groups/fragments/list.html:60
|
||||
#: templates/accounts/fragments/list.html:77
|
||||
@@ -2237,7 +2237,7 @@ msgstr "Сверка балансов"
|
||||
|
||||
#: templates/accounts/fragments/add.html:5
|
||||
msgid "Add account"
|
||||
msgstr ""
|
||||
msgstr "Добавить счёт"
|
||||
|
||||
#: templates/accounts/fragments/edit.html:5
|
||||
msgid "Edit account"
|
||||
@@ -2688,7 +2688,7 @@ msgstr ""
|
||||
|
||||
#: templates/exchange_rates_services/fragments/list.html:17
|
||||
msgid "Fetch all"
|
||||
msgstr ""
|
||||
msgstr "Отметить все"
|
||||
|
||||
#: templates/exchange_rates_services/fragments/list.html:29
|
||||
msgid "Service"
|
||||
@@ -2719,7 +2719,7 @@ msgstr ""
|
||||
|
||||
#: templates/exchange_rates_services/fragments/list.html:77
|
||||
msgid "No services configured"
|
||||
msgstr ""
|
||||
msgstr "Службы не настроены"
|
||||
|
||||
#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:205
|
||||
msgid "Export and Restore"
|
||||
@@ -2751,7 +2751,7 @@ msgstr ""
|
||||
|
||||
#: templates/import_app/fragments/profiles/list.html:80
|
||||
msgid "No import profiles"
|
||||
msgstr ""
|
||||
msgstr "Нет профилей для импорта"
|
||||
|
||||
#: templates/import_app/fragments/profiles/list_presets.html:5
|
||||
msgid "Import Presets"
|
||||
@@ -2877,11 +2877,11 @@ msgstr ""
|
||||
|
||||
#: templates/includes/sidebar.html:69 templates/insights/pages/index.html:5
|
||||
msgid "Insights"
|
||||
msgstr ""
|
||||
msgstr "Аналитика"
|
||||
|
||||
#: templates/includes/sidebar.html:75
|
||||
msgid "Net Worth"
|
||||
msgstr ""
|
||||
msgstr "Чистый капитал"
|
||||
|
||||
#: templates/includes/sidebar.html:91
|
||||
msgid "Trash Can"
|
||||
@@ -2899,7 +2899,7 @@ msgstr "Трекер средней стоимости доллара"
|
||||
#: templates/mini_tools/unit_price_calculator.html:4
|
||||
#: templates/mini_tools/unit_price_calculator.html:9
|
||||
msgid "Unit Price Calculator"
|
||||
msgstr ""
|
||||
msgstr "Калькулятор цены за единицу"
|
||||
|
||||
#: templates/includes/sidebar.html:130
|
||||
#: templates/mini_tools/currency_converter/currency_converter.html:7
|
||||
@@ -2909,7 +2909,7 @@ msgstr "Конвертер валют"
|
||||
|
||||
#: templates/includes/sidebar.html:139
|
||||
msgid "Management"
|
||||
msgstr ""
|
||||
msgstr "Управление"
|
||||
|
||||
#: templates/includes/sidebar.html:190
|
||||
msgid "Automation"
|
||||
@@ -3025,7 +3025,7 @@ msgstr "Всё отлично!"
|
||||
|
||||
#: templates/insights/fragments/late_transactions.html:16
|
||||
msgid "No late transactions"
|
||||
msgstr ""
|
||||
msgstr "Нет просроченных транзакций"
|
||||
|
||||
#: templates/insights/fragments/latest_transactions.html:14
|
||||
msgid "No recent transactions"
|
||||
@@ -3123,31 +3123,31 @@ msgstr "Диапазон дат"
|
||||
|
||||
#: templates/insights/pages/index.html:79
|
||||
msgid "Account Flow"
|
||||
msgstr ""
|
||||
msgstr "Движение по счету"
|
||||
|
||||
#: templates/insights/pages/index.html:84
|
||||
msgid "Currency Flow"
|
||||
msgstr ""
|
||||
msgstr "Движение валюты"
|
||||
|
||||
#: templates/insights/pages/index.html:89
|
||||
msgid "Category Explorer"
|
||||
msgstr ""
|
||||
msgstr "Обзор по категориям"
|
||||
|
||||
#: templates/insights/pages/index.html:94
|
||||
msgid "Categories Overview"
|
||||
msgstr ""
|
||||
msgstr "Все категории"
|
||||
|
||||
#: templates/insights/pages/index.html:112
|
||||
msgid "Late Transactions"
|
||||
msgstr ""
|
||||
msgstr "Просроченные транзакции"
|
||||
|
||||
#: templates/insights/pages/index.html:117
|
||||
msgid "Latest Transactions"
|
||||
msgstr ""
|
||||
msgstr "Последние транзакции"
|
||||
|
||||
#: templates/insights/pages/index.html:122
|
||||
msgid "Emergency Fund"
|
||||
msgstr ""
|
||||
msgstr "Резервный фонд"
|
||||
|
||||
#: templates/insights/pages/index.html:127
|
||||
msgid "Year by Year"
|
||||
|
||||
Reference in New Issue
Block a user