Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

money_manager wrapper attempt fails for managers associated on "objects" #2

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dc2fd9e
after failed hasattr check on csl object attempted to make use of the…
reinbach Nov 8, 2011
23be6cc
Updating money field to handle nulls w/o blowing up.
mcordes Dec 2, 2011
174eaa9
Merge pull request #1 from mcordes/master
reinbach Dec 2, 2011
b21b0a9
Update setup.py
mcordes Dec 6, 2011
d0af2f5
Merge pull request #2 from mcordes/master
reinbach Dec 6, 2011
16a4381
Update the db_prep_ prototypes to work with Django 1.2+
akumria Apr 8, 2012
1f06c92
Update version of py-moneyed required.
akumria Apr 8, 2012
3153398
So that migration works, specify the default currency in text form.
akumria Apr 8, 2012
90cf99a
Merge pull request #3 from akumria/master
reinbach Apr 8, 2012
bbd11f7
Py-moneyed 0.4 has been released, so we can simplify installation eve…
akumria Apr 9, 2012
fb7d843
Spellcheck and note South is optional.
akumria Apr 9, 2012
c01d29b
Update dependencies so installation is simpler.
akumria Apr 9, 2012
1c7487e
Switch to Django 1.2+ method of specifying databases.
akumria Apr 9, 2012
371fc8f
Update to the Django 1.2+ db_prep_save call.
akumria Apr 9, 2012
8cdf631
Increase version and switch location to new upstream.
akumria Apr 9, 2012
040cf08
Correct pip installation command
akumria Apr 9, 2012
60a0ae0
Fixup py-moneyed version reference.
akumria Apr 9, 2012
e0501fc
Merge pull request #4 from akumria/master
reinbach Apr 10, 2012
0043377
updated pip install command
reinbach Apr 10, 2012
6e34e2b
PEP8ify
mariocesar Apr 11, 2012
875494a
Merge pull request #7 from akumria/fixup-pep8
reinbach Apr 11, 2012
634002b
South support: Declare default attribute values.
PiDelport Aug 1, 2012
ecc092b
Merge pull request #8 from pjdelport/master
reinbach Aug 2, 2012
770328e
Allow django-money to be specified as read-only in a model
akumria Sep 28, 2012
94823e1
Merge pull request #9 from akumria/allow-readonly
reinbach Sep 30, 2012
c634bd6
Some minor enhancements:
Sep 30, 2012
54f30b8
Add descriptions to the custom fields.
akumria Oct 9, 2012
26458dc
Correctly serialize the field.
akumria Oct 9, 2012
f59520d
Merge pull request #10 from akumria/serialize_properly
reinbach Oct 9, 2012
059a97d
Fix AttributeError when Model inherit a manager
rach Oct 9, 2012
09643c4
Merge pull request #11 from rach/master
reinbach Oct 9, 2012
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions MANIFEST
@@ -0,0 +1,8 @@
setup.py
djmoney/__init__.py
djmoney/forms/__init__.py
djmoney/forms/fields.py
djmoney/forms/widgets.py
djmoney/models/__init__.py
djmoney/models/fields.py
djmoney/models/managers.py
47 changes: 34 additions & 13 deletions README.md
Expand Up @@ -17,20 +17,21 @@ Via py-moneyed, django-moneyed gets:
Installation Installation
------------ ------------


Django-money currently needs a special version of py-moneyed to work (2011-05-15). This will be resolved as soon as Django-money currently needs py-moneyed v0.4 (or later) to work.
my fork of it is approved and merged into py-moneyed main branch.


Until then, install py-moneyed from here: You can install django-money by doing:


git clone https://jakewins@github.com/jakewins/py-moneyed.git pip install django-money
cd py-moneyed
python setup.py install


And then, install py-moneyed like so: This will automatically install the appropriate dependencies.


git clone https://jakewins@github.com/jakewins/django-money.git You can obtain the source code for django-money from here:
cd django-money
python setup.py install https://github.com/reinbach/django-money

And the source for py-moneyed from here:

https://github.com/limist/py-moneyed


Model usage Model usage
----- -----
Expand All @@ -43,7 +44,7 @@ Use as normal model fields


class BankAccount(models.Model): class BankAccount(models.Model):


balance = MoneyField(max_digits=10, decimal_places=2, default_currency=moneyed.USD) balance = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')




Searching for models with money fields: Searching for models with money fields:
Expand All @@ -58,6 +59,28 @@ Searching for models with money fields:
BankAccount.objects.filter(balance__gt=Money(1, USD)) BankAccount.objects.filter(balance__gt=Money(1, USD))
# Returns the "account" object # Returns the "account" object


If you use South to handle model migration, things will "Just Work" out of the box.
South is an optional dependency and things will work fine without it.

Adding a new Currency
---------------------

Currencies are listed on moneyed, and this modules use this to provide a choice
list on the admin, also for validation.

To add a new currency available on all the project, you can simple add this two
lines on your `settings.py` file

from moneyed import add_currency
add_currency(code='BOB', numeric='068', name='Peso boliviano', countries=('BOLIVIA',))

To restrict the currencies listed on the project set a `CURRENCIES` variable with
a list of Currency codes on `settings.py`

CURRENCIES = ('USD', 'BOB')

**The list has to contain valid Currency codes**

Important note on model managers Important note on model managers
-------------------------------- --------------------------------


Expand Down Expand Up @@ -85,5 +108,3 @@ you also need to manually decorate those custom methods, like so:
@understand_money @understand_money
def my_custom_method(*args,**kwargs): def my_custom_method(*args,**kwargs):
# Awesome stuff # Awesome stuff


70 changes: 70 additions & 0 deletions djmoney/__init__.py
@@ -0,0 +1,70 @@
from django.db import models
from django.utils.encoding import smart_unicode
from django.utils import formats
from django.utils import timezone

from django.core.exceptions import ObjectDoesNotExist
from django.contrib.admin.util import lookup_field
from django.utils.safestring import mark_safe
from django.utils.html import conditional_escape
from django.db.models.fields.related import ManyToManyRel

from django.contrib.admin import util as admin_util


def djmoney_display_for_field(value, field):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE

try:
if field.flatchoices:
return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
# NullBooleanField needs special-case null-handling, so it comes
# before the general null test.
elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
return _boolean_icon(value)
elif value is None:
return EMPTY_CHANGELIST_VALUE
elif isinstance(field, models.DateTimeField):
return formats.localize(timezone.localtime(value))
elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
return formats.localize(value)
elif isinstance(field, models.DecimalField):
return formats.number_format(value, field.decimal_places)
elif isinstance(field, models.FloatField):
return formats.number_format(value)
else:
return smart_unicode(value)
except:
return smart_unicode(value)
admin_util.display_for_field = djmoney_display_for_field

def djmoney_contents(self):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin

try:
f, attr, value = lookup_field(field, obj, model_admin)
except (AttributeError, ValueError, ObjectDoesNotExist):
result_repr = EMPTY_CHANGELIST_VALUE
else:
if f is None:
boolean = getattr(attr, "boolean", False)
if boolean:
result_repr = _boolean_icon(value)
else:
result_repr = smart_unicode(value)
if getattr(attr, "allow_tags", False):
result_repr = mark_safe(result_repr)
else:
if value is None:
result_repr = EMPTY_CHANGELIST_VALUE
elif isinstance(f.rel, ManyToManyRel):
result_repr = ", ".join(map(unicode, value.all()))
else:
result_repr = djmoney_display_for_field(value, f)
return conditional_escape(result_repr)

from django.contrib.admin.helpers import AdminReadonlyField
AdminReadonlyField.contents = djmoney_contents
Empty file modified djmoney/forms/__init__.py
Whitespace-only changes.
1 change: 1 addition & 0 deletions djmoney/forms/fields.py
Expand Up @@ -5,6 +5,7 @@


__all__ = ('MoneyField',) __all__ = ('MoneyField',)



class MoneyField(forms.DecimalField): class MoneyField(forms.DecimalField):


def __init__(self, currency_widget=None, *args, **kwargs): def __init__(self, currency_widget=None, *args, **kwargs):
Expand Down
8 changes: 8 additions & 0 deletions djmoney/forms/widgets.py
@@ -1,13 +1,21 @@
from django import forms from django import forms
from django.conf import settings
from moneyed import Money, CURRENCIES, DEFAULT_CURRENCY_CODE from moneyed import Money, CURRENCIES, DEFAULT_CURRENCY_CODE
from decimal import Decimal from decimal import Decimal
import operator import operator


__all__ = ('InputMoneyWidget', 'CurrencySelectWidget',) __all__ = ('InputMoneyWidget', 'CurrencySelectWidget',)


PROJECT_CURRENCIES = getattr(settings, 'CURRENCIES', None)

if PROJECT_CURRENCIES:
CURRENCY_CHOICES = [(code, CURRENCIES[code].name) for code in PROJECT_CURRENCIES]
else:
CURRENCY_CHOICES = [(c.code, c.name) for i, c in CURRENCIES.items() if c.code != DEFAULT_CURRENCY_CODE] CURRENCY_CHOICES = [(c.code, c.name) for i, c in CURRENCIES.items() if c.code != DEFAULT_CURRENCY_CODE]

CURRENCY_CHOICES.sort(key=operator.itemgetter(1)) CURRENCY_CHOICES.sort(key=operator.itemgetter(1))



class CurrencySelectWidget(forms.Select): class CurrencySelectWidget(forms.Select):
def __init__(self, attrs=None, choices=CURRENCY_CHOICES): def __init__(self, attrs=None, choices=CURRENCY_CHOICES):
super(CurrencySelectWidget, self).__init__(attrs, choices) super(CurrencySelectWidget, self).__init__(attrs, choices)
Expand Down
40 changes: 28 additions & 12 deletions djmoney/models/fields.py
Expand Up @@ -11,19 +11,25 @@
currency_field_name = lambda name: "%s_currency" % name currency_field_name = lambda name: "%s_currency" % name
SUPPORTED_LOOKUPS = ('exact', 'lt', 'gt', 'lte', 'gte') SUPPORTED_LOOKUPS = ('exact', 'lt', 'gt', 'lte', 'gte')



class NotSupportedLookup(Exception): class NotSupportedLookup(Exception):
def __init__(self, lookup): def __init__(self, lookup):
self.lookup = lookup self.lookup = lookup

def __str__(self): def __str__(self):
return "Lookup '%s' is not supported for MoneyField" % self.lookup return "Lookup '%s' is not supported for MoneyField" % self.lookup



class MoneyFieldProxy(object): class MoneyFieldProxy(object):
def __init__(self, field): def __init__(self, field):
self.field = field self.field = field
self.currency_field_name = currency_field_name(self.field.name) self.currency_field_name = currency_field_name(self.field.name)


def _money_from_obj(self, obj): def _money_from_obj(self, obj):
return Money(obj.__dict__[self.field.name], obj.__dict__[self.currency_field_name]) amount = obj.__dict__[self.field.name]
if amount is None:
return None
return Money(amount, obj.__dict__[self.currency_field_name])


def __get__(self, obj, type=None): def __get__(self, obj, type=None):
if obj is None: if obj is None:
Expand All @@ -37,12 +43,15 @@ def __set__(self, obj, value):
obj.__dict__[self.field.name] = value.amount obj.__dict__[self.field.name] = value.amount
setattr(obj, self.currency_field_name, smart_unicode(value.currency)) setattr(obj, self.currency_field_name, smart_unicode(value.currency))
else: else:
if value: value = str(value) if value:
value = str(value)
obj.__dict__[self.field.name] = self.field.to_python(value) obj.__dict__[self.field.name] = self.field.to_python(value)




class CurrencyField(models.CharField): class CurrencyField(models.CharField):


description = "A field which stores currency."

def __init__(self, verbose_name=None, name=None, default=DEFAULT_CURRENCY, **kwargs): def __init__(self, verbose_name=None, name=None, default=DEFAULT_CURRENCY, **kwargs):
if isinstance(default, Currency): if isinstance(default, Currency):
default = default.code default = default.code
Expand All @@ -52,8 +61,11 @@ def __init__(self, verbose_name=None, name=None, default=DEFAULT_CURRENCY, **kwa
def get_internal_type(self): def get_internal_type(self):
return "CharField" return "CharField"



class MoneyField(models.DecimalField): class MoneyField(models.DecimalField):


description = "A field which stores both the currency and amount of money."

def __init__(self, verbose_name=None, name=None, def __init__(self, verbose_name=None, name=None,
max_digits=None, decimal_places=None, max_digits=None, decimal_places=None,
default=Decimal("0.0"), default_currency=DEFAULT_CURRENCY, **kwargs): default=Decimal("0.0"), default_currency=DEFAULT_CURRENCY, **kwargs):
Expand Down Expand Up @@ -90,23 +102,21 @@ def contribute_to_class(self, cls, name):


from managers import money_manager from managers import money_manager


if hasattr(cls, '_default_manager'): if getattr(cls, '_default_manager', None):
cls._default_manager = money_manager(cls._default_manager) cls._default_manager = money_manager(cls._default_manager)
elif hasattr(cls, 'objects'):
cls.objects = money_manager(cls._default_manager)
else: else:
cls.objects = money_manager(models.Manager) cls.objects = money_manager(models.Manager)


def get_db_prep_save(self, value): def get_db_prep_save(self, value, connection):
if isinstance(value, Money): if isinstance(value, Money):
value = value.amount value = value.amount
return super(MoneyField, self).get_db_prep_save(value) return super(MoneyField, self).get_db_prep_save(value, connection)


def get_db_prep_lookup(self, lookup_type, value): def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
if not lookup_type in SUPPORTED_LOOKUPS: if not lookup_type in SUPPORTED_LOOKUPS:
raise NotSupportedLookup(lookup_type) raise NotSupportedLookup(lookup_type)
value = self.get_db_prep_save(value) value = self.get_db_prep_save(value, connection)
return super(MoneyField, self).get_db_prep_lookup(lookup_type, value) return super(MoneyField, self).get_db_prep_lookup(lookup_type, value, connection, prepared)


def get_default(self): def get_default(self):
if isinstance(self.default, Money): if isinstance(self.default, Money):
Expand All @@ -119,17 +129,23 @@ def formfield(self, **kwargs):
defaults.update(kwargs) defaults.update(kwargs)
return super(MoneyField, self).formfield(**defaults) return super(MoneyField, self).formfield(**defaults)


def value_to_string(self, obj):
return obj.__dict__[self.attname].amount


## South support ## South support
try: try:
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules


rules = [ rules = [
((MoneyField,), ((MoneyField,),
[], # No positional args [], # No positional args
{'default_currency':('default_currency',{})}), {'default': ('default', {'default': Decimal('0.0')}),
'default_currency': ('default_currency', {'default': DEFAULT_CURRENCY})}),
((CurrencyField,), ((CurrencyField,),
[], # No positional args [], # No positional args
{}), # No new keyword args {'default': ('default', {'default': DEFAULT_CURRENCY.code}),
'max_length': ('max_length', {'default': 3})}),
] ]


add_introspection_rules(rules, ["^djmoney\.models"]) add_introspection_rules(rules, ["^djmoney\.models"])
Expand Down
6 changes: 5 additions & 1 deletion djmoney/models/managers.py
@@ -1,7 +1,7 @@

from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
from fields import currency_field_name from fields import currency_field_name



def _expand_money_params(kwargs): def _expand_money_params(kwargs):
from moneyed import Money from moneyed import Money
from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS
Expand All @@ -20,6 +20,7 @@ def _expand_money_params(kwargs):
kwargs.update(to_append) kwargs.update(to_append)
return kwargs return kwargs



def understands_money(func): def understands_money(func):
''' '''
Used to wrap a queryset method with logic to expand Used to wrap a queryset method with logic to expand
Expand All @@ -39,11 +40,13 @@ def decorator(*args, **kwargs):
RELEVANT_QUERYSET_METHODS = ['dates', 'distinct', 'extra', 'get', 'get_or_create', 'filter', 'complex_filter', RELEVANT_QUERYSET_METHODS = ['dates', 'distinct', 'extra', 'get', 'get_or_create', 'filter', 'complex_filter',
'exclude', 'in_bulk', 'iterator', 'latest', 'order_by', 'select_related', 'values'] 'exclude', 'in_bulk', 'iterator', 'latest', 'order_by', 'select_related', 'values']



def add_money_comprehension_to_queryset(qs): def add_money_comprehension_to_queryset(qs):
# Decorate each relevant method with understand_money in the queryset given # Decorate each relevant method with understand_money in the queryset given
map(lambda attr: setattr(qs, attr, understands_money(getattr(qs, attr))), RELEVANT_QUERYSET_METHODS) map(lambda attr: setattr(qs, attr, understands_money(getattr(qs, attr))), RELEVANT_QUERYSET_METHODS)
return qs return qs



def money_manager(manager): def money_manager(manager):
''' '''
Wraps a model managers get_query_set method so that each query set it returns Wraps a model managers get_query_set method so that each query set it returns
Expand All @@ -53,6 +56,7 @@ def money_manager(manager):
use other managers special managers while still doing money queries. use other managers special managers while still doing money queries.
''' '''
old_get_query_set = manager.get_query_set old_get_query_set = manager.get_query_set

def get_query_set(*args, **kwargs): def get_query_set(*args, **kwargs):
return add_money_comprehension_to_queryset(old_get_query_set(*args, **kwargs)) return add_money_comprehension_to_queryset(old_get_query_set(*args, **kwargs))


Expand Down
16 changes: 6 additions & 10 deletions setup.py
Expand Up @@ -13,23 +13,19 @@




setup(name="django-money", setup(name="django-money",
version="0.1", version="0.2",
description="Adds support for using money and currency fields in django models and forms. Uses py-moneyed as money implementation, based on python-money django implementation.", description="Adds support for using money and currency fields in django models and forms. Uses py-moneyed as the money implementation.",
url="https://github.com/jakewins/django-money", url="https://github.com/reinbach/django-money",
packages=["djmoney", packages=["djmoney",
"djmoney.forms", "djmoney.forms",
"djmoney.models"], "djmoney.models"],
# Commented out, waiting for pull request to be fulfilled: https://github.com/limist/py-moneyed/pull/1 install_requires=['setuptools',
#install_requires=['setuptools', 'Django >= 1.2',
# 'Django >= 1.2', 'py-moneyed > 0.3'],
# 'py-moneyed > 0.3'],
package_dir={"": ""},
cmdclass=cmdclass, cmdclass=cmdclass,
classifiers=["Development Status :: 5 - Production/Stable", classifiers=["Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: BSD License", "License :: OSI Approved :: BSD License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
"Framework :: Django", ]) "Framework :: Django", ])


1 change: 0 additions & 1 deletion tests/__init__.py
@@ -1,3 +1,2 @@

from model_tests import * from model_tests import *
from form_tests import * from form_tests import *
1 change: 0 additions & 1 deletion tests/form_tests.py
Expand Up @@ -54,4 +54,3 @@ def testSave(self):
retrieved = ModelWithVanillaMoneyField.objects.get(pk=model.pk) retrieved = ModelWithVanillaMoneyField.objects.get(pk=model.pk)


self.assertEqual(Money(10, moneyed.SEK), retrieved.money) self.assertEqual(Money(10, moneyed.SEK), retrieved.money)

1 change: 1 addition & 0 deletions tests/model_tests.py
Expand Up @@ -9,6 +9,7 @@
import moneyed import moneyed
from testapp.models import ModelWithVanillaMoneyField, ModelRelatedToModelWithMoney from testapp.models import ModelWithVanillaMoneyField, ModelRelatedToModelWithMoney



class VanillaMoneyFieldTestCase(TestCase): class VanillaMoneyFieldTestCase(TestCase):


def testSaving(self): def testSaving(self):
Expand Down
5 changes: 4 additions & 1 deletion tests/runtests.py
Expand Up @@ -3,9 +3,12 @@


@author: jake @author: jake
''' '''
import sys, os, unittest import sys
import os
import unittest
import django.conf import django.conf



def setup(): def setup():
test_folder = os.path.abspath(os.path.dirname(__file__)) test_folder = os.path.abspath(os.path.dirname(__file__))
src_folder = os.path.abspath(test_folder + "/../") src_folder = os.path.abspath(test_folder + "/../")
Expand Down