Skip to content

Commit

Permalink
Add support for py-moneyed 1.x
Browse files Browse the repository at this point in the history
  • Loading branch information
antonagestam authored and Stranger6667 committed May 20, 2021
1 parent 282c790 commit d992a25
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 41 deletions.
54 changes: 40 additions & 14 deletions djmoney/money.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import warnings
from functools import partial
from types import MappingProxyType

from django.conf import settings
from django.db.models import F
from django.utils import translation
from django.utils.deconstruct import deconstructible
from django.utils.html import avoid_wrapping, conditional_escape
from django.utils.safestring import mark_safe

import moneyed.l10n
import moneyed.localization
from moneyed import Currency, Money as DefaultMoney
from moneyed.localization import _FORMATTER, format_money

from .settings import DECIMAL_PLACES, DECIMAL_PLACES_DISPLAY
from .settings import DECIMAL_PLACES, DECIMAL_PLACES_DISPLAY, IS_DECIMAL_PLACES_DISPLAY_SET, MONEY_FORMAT


__all__ = ["Money", "Currency"]

_warn_decimal_places_display_deprecated = partial(
warnings.warn,
"`Money.decimal_places_display` is deprecated and will be removed in django-money 3.0.",
DeprecationWarning,
)


@deconstructible
class Money(DefaultMoney):
Expand All @@ -22,21 +33,25 @@ class Money(DefaultMoney):

use_l10n = None

def __init__(self, *args, decimal_places_display=None, **kwargs):
def __init__(self, *args, decimal_places_display=None, format_options=None, **kwargs):
self.decimal_places = kwargs.pop("decimal_places", DECIMAL_PLACES)
self._decimal_places_display = decimal_places_display
if decimal_places_display is not None:
_warn_decimal_places_display_deprecated()
self.format_options = MappingProxyType(format_options) if format_options is not None else None
super().__init__(*args, **kwargs)

@property
def decimal_places_display(self):
_warn_decimal_places_display_deprecated()
if self._decimal_places_display is None:
return DECIMAL_PLACES_DISPLAY.get(self.currency.code, self.decimal_places)

return self._decimal_places_display

@decimal_places_display.setter
def decimal_places_display(self, value):
""" Set number of digits being displayed - `None` resets to `DECIMAL_PLACES_DISPLAY` setting """
_warn_decimal_places_display_deprecated()
self._decimal_places_display = value

def _copy_attributes(self, source, target):
Expand Down Expand Up @@ -97,13 +112,21 @@ def is_localized(self):
return self.use_l10n

def __str__(self):
kwargs = {"money": self, "decimal_places": self.decimal_places_display}
if self.is_localized:
locale = get_current_locale()
if locale:
kwargs["locale"] = locale

return format_money(**kwargs)
if self._decimal_places_display is not None or IS_DECIMAL_PLACES_DISPLAY_SET:
kwargs = {"money": self, "decimal_places": self.decimal_places_display}
if self.is_localized:
locale = get_current_locale(for_babel=False)
if locale:
kwargs["locale"] = locale
return moneyed.localization.format_money(**kwargs)
format_options = {
**MONEY_FORMAT,
**(self.format_options or {}),
}
locale = get_current_locale()
if locale:
format_options["locale"] = locale
return moneyed.l10n.format_money(self, **format_options)

def __html__(self):
return mark_safe(avoid_wrapping(conditional_escape(str(self))))
Expand Down Expand Up @@ -146,16 +169,19 @@ def __rmod__(self, other):
__rmul__ = __mul__


def get_current_locale():
def get_current_locale(for_babel=True):
# get_language can return None starting from Django 1.8
language = translation.get_language() or settings.LANGUAGE_CODE
locale = translation.to_locale(language)

if locale.upper() in _FORMATTER.formatting_definitions:
if for_babel:
return locale

if locale.upper() in moneyed.localization._FORMATTER.formatting_definitions:
return locale

locale = ("{}_{}".format(locale, locale)).upper()
if locale in _FORMATTER.formatting_definitions:
if locale in moneyed.localization._FORMATTER.formatting_definitions:
return locale

return ""
Expand Down
15 changes: 12 additions & 3 deletions djmoney/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import operator
import warnings
from types import MappingProxyType

from django.conf import settings

Expand All @@ -23,9 +25,14 @@

CURRENCY_CHOICES.sort(key=operator.itemgetter(1, 0))
DECIMAL_PLACES = getattr(settings, "CURRENCY_DECIMAL_PLACES", 2)
DECIMAL_PLACES_DISPLAY = getattr(
settings, "CURRENCY_DECIMAL_PLACES_DISPLAY", {currency[0]: DECIMAL_PLACES for currency in CURRENCY_CHOICES}
)
_decimal_display_value = getattr(settings, "CURRENCY_DECIMAL_PLACES_DISPLAY", None)
if _decimal_display_value is None:
warnings.warn(
"`CURRENCY_DECIMAL_PLACES_DISPLAY` is deprecated and will be removed in django-money 3.0.",
DeprecationWarning,
)
DECIMAL_PLACES_DISPLAY = _decimal_display_value or {currency[0]: DECIMAL_PLACES for currency in CURRENCY_CHOICES}
IS_DECIMAL_PLACES_DISPLAY_SET = _decimal_display_value is not None

OPEN_EXCHANGE_RATES_URL = getattr(settings, "OPEN_EXCHANGE_RATES_URL", "https://openexchangerates.org/api/latest.json")
OPEN_EXCHANGE_RATES_APP_ID = getattr(settings, "OPEN_EXCHANGE_RATES_APP_ID", None)
Expand All @@ -36,3 +43,5 @@
RATES_CACHE_TIMEOUT = getattr(settings, "RATES_CACHE_TIMEOUT", 600)

CURRENCY_CODE_MAX_LENGTH = getattr(settings, "CURRENCY_CODE_MAX_LENGTH", 3)

MONEY_FORMAT = MappingProxyType(getattr(settings, "MONEY_FORMAT", {}))
5 changes: 5 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
[pytest]
DJANGO_SETTINGS_MODULE=tests.settings
filterwarnings =
error::DeprecationWarning
ignore:This module and all its contents is deprecated in favour of new moneyed.l10n.format_money\.:DeprecationWarning
ignore:`Money\.decimal_places_display` is deprecated and will be removed in django-money 3\.0\.:DeprecationWarning
ignore:`CURRENCY_DECIMAL_PLACES_DISPLAY` is deprecated and will be removed in django-money 3\.0\.:DeprecationWarning
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def find_version():
maintainer_email="greg@reinbach.com",
license="BSD",
packages=find_packages(include=["djmoney", "djmoney.*"]),
install_requires=["setuptools", "Django>=1.11", "py-moneyed>=0.8,<1.0"],
install_requires=["setuptools", "Django>=1.11", "py-moneyed>=1.2,<2.0"],
python_requires=">=3.5",
platforms=["Any"],
keywords=["django", "py-money", "money"],
Expand Down
12 changes: 10 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest import mock

import pytest

from djmoney.contrib.exchange.models import ExchangeBackend, Rate, get_default_backend_name
Expand All @@ -10,12 +12,12 @@ def m2m_object():
return ModelWithDefaultAsInt.objects.create(money=Money(100, "USD"))


@pytest.fixture()
@pytest.fixture
def backend():
return ExchangeBackend.objects.create(name=get_default_backend_name(), base_currency="USD")


@pytest.fixture()
@pytest.fixture
def autoconversion(backend, settings):
settings.AUTO_CONVERT_MONEY = True
Rate.objects.create(currency="EUR", value="0.88", backend=backend)
Expand All @@ -29,3 +31,9 @@ def concrete_instance(m2m_object):


pytest_plugins = "pytester"


@pytest.yield_fixture
def legacy_formatting():
with mock.patch("djmoney.money.IS_DECIMAL_PLACES_DISPLAY_SET", True):
yield
2 changes: 1 addition & 1 deletion tests/contrib/exchange/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,4 @@ def test_without_installed_exchange(testdir):
"""
)
)
result.stdout.fnmatch_lines(["US$1.00"])
result.stdout.fnmatch_lines(["$1.00"])
8 changes: 4 additions & 4 deletions tests/contrib/test_django_rest_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ def test_serializer_with_fields(self):
@pytest.mark.parametrize(
"value, error",
(
(Money(50, "EUR"), "Ensure this value is greater than or equal to 100.00."),
(Money(1500, "EUR"), "Ensure this value is less than or equal to 1,000.00."),
(Money(50, "EUR"), "Ensure this value is greater than or equal to 100.00."),
(Money(1500, "EUR"), "Ensure this value is less than or equal to 1,000.00."),
(Money(40, "USD"), "Ensure this value is greater than or equal to $50.00."),
(Money(600, "USD"), "Ensure this value is less than or equal to $500.00."),
(Money(400, "NOK"), "Ensure this value is greater than or equal to 500.00 Nkr."),
(Money(950, "NOK"), "Ensure this value is less than or equal to 900.00 Nkr."),
(Money(400, "NOK"), "Ensure this value is greater than or equal to NOK500.00."),
(Money(950, "NOK"), "Ensure this value is less than or equal to NOK900.00."),
(Money(5, "SEK"), "Ensure this value is greater than or equal to 10."),
(Money(1600, "SEK"), "Ensure this value is less than or equal to 1500."),
),
Expand Down
2 changes: 1 addition & 1 deletion tests/migrations/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def test_rename_field(self):
print(instance.new_field)
"""
)
result.stdout.fnmatch_lines(["US$10.00"])
result.stdout.fnmatch_lines(["$10.00"])

def test_migrate_to_moneyfield(self):
self.make_default_migration(field="models.DecimalField(max_digits=10, decimal_places=2, null=True)")
Expand Down
18 changes: 18 additions & 0 deletions tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@
(Money("3.33", "EUR"), "3.33 €"), # Issue 90
),
)
def test_display_for_field_with_legacy_formatting(legacy_formatting, settings, value, expected):
settings.USE_L10N = True
# This locale has no definitions in py-moneyed, so it will work for localized money representation.
settings.LANGUAGE_CODE = "cs"
settings.DECIMAL_PLACES_DISPLAY = {}
assert admin_utils.display_for_field(value, MONEY_FIELD, "") == expected


@pytest.mark.parametrize(
"value, expected",
(
(Money(10, "RUB"), "10,00\xa0RUB"), # Issue 232
(Money(1234), "1\xa0234,00\xa0XYZ"), # Issue 220
(Money(1000, "SAR"), "1\xa0000,00\xa0SAR"), # Issue 196
(Money(1000, "PLN"), "1\xa0000,00\xa0PLN"), # Issue 102
(Money("3.33", "EUR"), "3,33\xa0€"), # Issue 90
),
)
def test_display_for_field(settings, value, expected):
settings.USE_L10N = True
# This locale has no definitions in py-moneyed, so it will work for localized money representation.
Expand Down
10 changes: 5 additions & 5 deletions tests/test_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ class TestValidation:
@pytest.mark.parametrize(
"value, error",
(
(Money(50, "EUR"), "Ensure this value is greater than or equal to 100.00."),
(Money(1500, "EUR"), "Ensure this value is less than or equal to 1,000.00."),
(Money(50, "EUR"), "Ensure this value is greater than or equal to 100.00."),
(Money(1500, "EUR"), "Ensure this value is less than or equal to 1,000.00."),
(Money(40, "USD"), "Ensure this value is greater than or equal to $50.00."),
(Money(600, "USD"), "Ensure this value is less than or equal to $500.00."),
(Money(400, "NOK"), "Ensure this value is greater than or equal to 500.00 Nkr."),
(Money(950, "NOK"), "Ensure this value is less than or equal to 900.00 Nkr."),
(Money(400, "NOK"), "Ensure this value is greater than or equal to NOK500.00."),
(Money(950, "NOK"), "Ensure this value is less than or equal to NOK900.00."),
(Money(5, "SEK"), "Ensure this value is greater than or equal to 10."),
(Money(1600, "SEK"), "Ensure this value is less than or equal to 1500."),
),
Expand Down Expand Up @@ -171,7 +171,7 @@ def test_positive_validator(self, value):
def test_default_django_validator(self):
form = MoneyModelFormWithValidation(data={"balance_0": 0, "balance_1": "GBP"})
assert not form.is_valid()
assert form.errors == {"balance": ["Ensure this value is greater than or equal to GB£100.00."]}
assert form.errors == {"balance": ["Ensure this value is greater than or equal to £100.00."]}


class TestDisabledField:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ def test_override_decorator():
When current locale is changed, Money instances should be represented correctly.
"""
with override("cs"):
assert str(Money(10, "CZK")) == "10.00 Kč"
assert str(Money(10, "CZK")) == "10,00 Kč"


def test_properties_access():
Expand Down
36 changes: 33 additions & 3 deletions tests/test_money.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@


def test_repr():
assert repr(Money("10.5", "USD")) == "<Money: 10.5 USD>"
assert repr(Money("10.5", "USD")) == "Money('10.5', 'USD')"


def test_legacy_repr():
assert repr(Money("10.5", "USD", decimal_places_display=2)) == "Money('10.5', 'USD')"


def test_html_safe():
assert Money("10.5", "EUR").__html__() == "10.50\xa0€"
assert Money("10.5", "EUR").__html__() == "€10.50"


def test_legacy_html_safe():
assert Money("10.5", "EUR", decimal_places_display=2).__html__() == "10.50\xa0€"


def test_html_unsafe():
Expand All @@ -35,7 +43,29 @@ def test_reverse_truediv_fails():
10 / Money(5, "USD")


@pytest.mark.parametrize("locale, expected", (("pl", "PL_PL"), ("pl_PL", "pl_PL")))
@pytest.mark.parametrize(
"locale, expected",
(
("pl", "PL_PL"),
("pl_PL", "pl_PL"),
),
)
def test_legacy_get_current_locale(locale, expected):
with override(locale):
assert get_current_locale(for_babel=False) == expected


@pytest.mark.parametrize(
"locale, expected",
(
("pl", "pl"),
("pl-pl", "pl_PL"),
("sv", "sv"),
("sv-se", "sv_SE"),
("en-us", "en_US"),
("en-gb", "en_GB"),
),
)
def test_get_current_locale(locale, expected):
with override(locale):
assert get_current_locale() == expected
Expand Down

0 comments on commit d992a25

Please sign in to comment.