from __future__ import unicode_literals
try:
from urllib import parse as urlparse
except ImportError:
import urlparse # Python 2
from django import forms
from django.db.models.fields import CharField, BLANK_CHOICE_DASH
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.html import escape as escape_html
from django.utils.functional import lazy
from django_countries import countries, ioc_data, widgets
from django_countries.conf import settings
class TemporaryEscape(object):
__slots__ = ['country', 'original_escape']
def __init__(self, country):
self.country = country
def __bool__(self):
return self.country._escape
__nonzero__ = __bool__
def __enter__(self):
self.original_escape = self.country._escape
self.country._escape = True
def __exit__(self, type, value, traceback):
self.country._escape = self.original_escape
@python_2_unicode_compatible
class Country(object):
def __init__(self, code, flag_url=None, str_attr='code'):
self.code = code
self.flag_url = flag_url
self._escape = False
self._str_attr = str_attr
def __str__(self):
return force_text(getattr(self, self._str_attr) or '')
def __eq__(self, other):
return force_text(self.code or '') == force_text(other or '')
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(force_text(self))
def __repr__(self):
args = ['code={country.code!r}']
if self.flag_url is not None:
args.append('flag_url={country.flag_url!r}')
if self._str_attr != 'code':
args.append('str_attr={country._str_attr!r}')
args = ', '.join(args).format(country=self)
return '{name}({args})'.format(name=self.__class__.__name__, args=args)
def __bool__(self):
return bool(self.code)
__nonzero__ = __bool__ # Python 2 compatibility.
def __len__(self):
return len(force_text(self))
@property
def escape(self):
return TemporaryEscape(self)
def maybe_escape(self, text):
if not self.escape:
return text
return escape_html(text)
@property
def name(self):
return self.maybe_escape(countries.name(self.code))
@property
def alpha3(self):
return countries.alpha3(self.code)
@property
def numeric(self):
return countries.numeric(self.code)
@property
def numeric_padded(self):
return countries.numeric(self.code, padded=True)
@property
def flag(self):
if not self.code:
return ''
flag_url = self.flag_url
if flag_url is None:
flag_url = settings.COUNTRIES_FLAG_URL
url = flag_url.format(
code_upper=self.code, code=self.code.lower())
if not url:
return ''
url = urlparse.urljoin(settings.STATIC_URL, url)
return self.maybe_escape(url)
@staticmethod
def country_from_ioc(ioc_code, flag_url=''):
code = ioc_data.IOC_TO_ISO.get(ioc_code, '')
if code == '':
return None
return Country(code, flag_url=flag_url)
@property
def ioc_code(self):
return ioc_data.ISO_TO_IOC.get(self.code, '')
class CountryDescriptor(object):
"""
A descriptor for country fields on a model instance. Returns a Country when
accessed so you can do things like::
>>> from people import Person
>>> person = Person.object.get(name='Chris')
>>> person.country.name
'New Zealand'
>>> person.country.flag
'/static/flags/nz.gif'
"""
def __init__(self, field):
self.field = field
def __get__(self, instance=None, owner=None):
if instance is None:
raise AttributeError(
"The '%s' attribute can only be accessed from %s instances."
% (self.field.name, owner.__name__))
return Country(
code=instance.__dict__[self.field.name],
flag_url=self.field.countries_flag_url,
)
def __set__(self, instance, value):
if value is not None:
value = force_text(value)
instance.__dict__[self.field.name] = value
class LazyTypedChoiceField(widgets.LazyChoicesMixin, forms.TypedChoiceField):
"""
A form TypedChoiceField that respects choices being a lazy object.
"""
widget = widgets.LazySelect
def _set_choices(self, value):
"""
Also update the widget's choices.
"""
super(LazyTypedChoiceField, self)._set_choices(value)
self.widget.choices = value
class CountryField(CharField):
"""
A country field for Django models that provides all ISO 3166-1 countries as
choices.
"""
descriptor_class = CountryDescriptor
def __init__(self, *args, **kwargs):
countries_class = kwargs.pop('countries', None)
self.countries = countries_class() if countries_class else countries
self.countries_flag_url = kwargs.pop('countries_flag_url', None)
self.blank_label = kwargs.pop('blank_label', None)
kwargs.update({
'max_length': 2,
'choices': self.countries,
})
super(CharField, self).__init__(*args, **kwargs)
def get_internal_type(self):
return "CharField"
def contribute_to_class(self, cls, name):
super(CountryField, self).contribute_to_class(cls, name)
setattr(cls, self.name, self.descriptor_class(self))
def get_prep_lookup(self, lookup_type, value):
if hasattr(value, 'code'):
value = value.code
return super(CountryField, self).get_prep_lookup(lookup_type, value)
def pre_save(self, *args, **kwargs):
"Returns field's value just before saving."
value = super(CharField, self).pre_save(*args, **kwargs)
return self.get_prep_value(value)
def get_prep_value(self, value):
"Returns field's value prepared for saving into a database."
# Convert the Country to unicode for database insertion.
if value is None or getattr(value, 'code', '') is None:
return None
return force_text(value)
def deconstruct(self):
"""
Remove choices from deconstructed field, as this is the country list
and not user editable.
Not including the ``blank_label`` property, as this isn't database
related.
"""
name, path, args, kwargs = super(CountryField, self).deconstruct()
kwargs.pop('choices')
if self.countries is not countries:
# Include the countries class if it's not the default countries
# instance.
kwargs['countries'] = self.countries.__class__
return name, path, args, kwargs
def get_choices(
self, include_blank=True, blank_choice=None, *args, **kwargs):
if blank_choice is None:
if self.blank_label is None:
blank_choice = BLANK_CHOICE_DASH
else:
blank_choice = [('', self.blank_label)]
return super(CountryField, self).get_choices(
include_blank=include_blank, blank_choice=blank_choice, *args,
**kwargs)
get_choices = lazy(get_choices, list)
def formfield(self, **kwargs):
argname = 'choices_form_class'
if argname not in kwargs:
kwargs[argname] = LazyTypedChoiceField
field = super(CharField, self).formfield(**kwargs)
if not isinstance(field, LazyTypedChoiceField):
field = self.legacy_formfield(**kwargs)
return field
def legacy_formfield(self, **kwargs):
"""
Legacy method to fix Django LTS not allowing a custom choices form
class.
"""
from django.utils.text import capfirst
defaults = {'required': not self.blank,
'label': capfirst(self.verbose_name),
'help_text': self.help_text}
if self.has_default():
if callable(self.default):
defaults['initial'] = self.default
defaults['show_hidden_initial'] = True
else:
defaults['initial'] = self.get_default()
include_blank = (self.blank or
not (self.has_default() or 'initial' in kwargs))
defaults['choices'] = self.get_choices(include_blank=include_blank)
defaults['coerce'] = self.to_python
if self.null:
defaults['empty_value'] = None
form_class = LazyTypedChoiceField
# Many of the subclass-specific formfield arguments (min_value,
# max_value) don't apply for choice fields, so be sure to only pass
# the values that TypedChoiceField will understand.
for k in kwargs.keys():
if k not in ('coerce', 'empty_value', 'choices', 'required',
'widget', 'label', 'initial', 'help_text',
'error_messages', 'show_hidden_initial'):
del kwargs[k]
defaults.update(kwargs)
return form_class(**defaults)
# If south is installed, ensure that CountryField will be introspected just
# like a normal CharField.
try: # pragma: no cover
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ['^django_countries\.fields\.CountryField'])
except ImportError:
pass