Updates
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from timezone_field.fields import TimeZoneField
|
||||
from timezone_field.forms import TimeZoneFormField
|
||||
|
||||
__version__ = "7.1"
|
||||
__all__ = ["TimeZoneField", "TimeZoneFormField"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
from django import VERSION, conf
|
||||
|
||||
from .base import TimeZoneNotFoundError
|
||||
|
||||
USE_PYTZ_DEFAULT = getattr(conf.settings, "USE_DEPRECATED_PYTZ", VERSION < (4, 0))
|
||||
|
||||
tz_backend_cache = {}
|
||||
|
||||
|
||||
def get_tz_backend(use_pytz):
|
||||
use_pytz = USE_PYTZ_DEFAULT if use_pytz is None else use_pytz
|
||||
if use_pytz not in tz_backend_cache:
|
||||
if use_pytz:
|
||||
from .pytz import PYTZBackend
|
||||
|
||||
klass = PYTZBackend
|
||||
else:
|
||||
from .zoneinfo import ZoneInfoBackend
|
||||
|
||||
klass = ZoneInfoBackend
|
||||
tz_backend_cache[use_pytz] = klass()
|
||||
return tz_backend_cache[use_pytz]
|
||||
|
||||
|
||||
__all__ = ["TimeZoneNotFoundError", "get_tz_backend"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class TimeZoneNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TimeZoneBackend(ABC):
|
||||
utc_tzobj = None
|
||||
all_tzstrs = None
|
||||
base_tzstrs = None
|
||||
|
||||
@abstractmethod
|
||||
def is_tzobj(self, value):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_tzobj(self, tzstr):
|
||||
pass
|
||||
@@ -0,0 +1,18 @@
|
||||
import pytz
|
||||
|
||||
from .base import TimeZoneBackend, TimeZoneNotFoundError
|
||||
|
||||
|
||||
class PYTZBackend(TimeZoneBackend):
|
||||
utc_tzobj = pytz.utc
|
||||
all_tzstrs = pytz.all_timezones
|
||||
base_tzstrs = pytz.common_timezones
|
||||
|
||||
def is_tzobj(self, value):
|
||||
return value is pytz.UTC or isinstance(value, pytz.tzinfo.BaseTzInfo)
|
||||
|
||||
def to_tzobj(self, tzstr):
|
||||
try:
|
||||
return pytz.timezone(tzstr)
|
||||
except pytz.UnknownTimeZoneError as err:
|
||||
raise TimeZoneNotFoundError from err
|
||||
@@ -0,0 +1,27 @@
|
||||
try:
|
||||
import zoneinfo
|
||||
except ImportError:
|
||||
from backports import zoneinfo
|
||||
|
||||
from .base import TimeZoneBackend, TimeZoneNotFoundError
|
||||
|
||||
|
||||
class ZoneInfoBackend(TimeZoneBackend):
|
||||
utc_tzobj = zoneinfo.ZoneInfo("UTC")
|
||||
all_tzstrs = zoneinfo.available_timezones()
|
||||
base_tzstrs = zoneinfo.available_timezones()
|
||||
# Remove the "Factory" timezone as it can cause ValueError exceptions on
|
||||
# some systems, e.g. FreeBSD, if the system zoneinfo database is used.
|
||||
all_tzstrs.discard("Factory")
|
||||
base_tzstrs.discard("Factory")
|
||||
|
||||
def is_tzobj(self, value):
|
||||
return isinstance(value, zoneinfo.ZoneInfo)
|
||||
|
||||
def to_tzobj(self, tzstr):
|
||||
if tzstr in (None, ""):
|
||||
raise TimeZoneNotFoundError
|
||||
try:
|
||||
return zoneinfo.ZoneInfo(tzstr)
|
||||
except zoneinfo.ZoneInfoNotFoundError as err:
|
||||
raise TimeZoneNotFoundError from err
|
||||
@@ -0,0 +1,66 @@
|
||||
import datetime
|
||||
|
||||
from timezone_field.backends import get_tz_backend
|
||||
|
||||
|
||||
def normalize_standard(tztuple):
|
||||
"""Normalize timezone names by replacing special characters with space.
|
||||
|
||||
For proper sorting, using spaces makes comparisons more consistent.
|
||||
|
||||
:param str tztuple: tuple of timezone and representation
|
||||
"""
|
||||
return tztuple[1].translate(str.maketrans({"-": " ", "_": " "}))
|
||||
|
||||
|
||||
def normalize_gmt(tztuple):
|
||||
"""Normalize timezone GMT names for sorting.
|
||||
|
||||
For proper sorting, using GMT values as a positive or negative number.
|
||||
|
||||
:param str tztuple: tuple of timezone and representation
|
||||
"""
|
||||
gmt = tztuple[1].split()[0]
|
||||
cmp = gmt.replace("GMT", "").replace(":", "")
|
||||
return int(cmp)
|
||||
|
||||
|
||||
def standard(timezones):
|
||||
"""
|
||||
Given a list of timezones (either strings of timezone objects),
|
||||
return a list of choices with
|
||||
* values equal to what was passed in
|
||||
* display strings as the timezone name without underscores
|
||||
"""
|
||||
choices = []
|
||||
for tz in timezones:
|
||||
tz_str = str(tz)
|
||||
choices.append((tz, tz_str.replace("_", " ")))
|
||||
return sorted(choices, key=normalize_standard)
|
||||
|
||||
|
||||
def with_gmt_offset(timezones, now=None, use_pytz=None):
|
||||
"""
|
||||
Given a list of timezones (either strings of timezone objects),
|
||||
return a list of choices with
|
||||
* values equal to what was passed in
|
||||
* display strings formated with GMT offsets and without
|
||||
underscores. For example: "GMT-05:00 America/New York"
|
||||
* sorted by their timezone offset
|
||||
"""
|
||||
tz_backend = get_tz_backend(use_pytz)
|
||||
now = now or datetime.datetime.now(tz_backend.utc_tzobj)
|
||||
_choices = []
|
||||
for tz in timezones:
|
||||
tz_str = str(tz)
|
||||
now_tz = now.astimezone(tz_backend.to_tzobj(tz_str))
|
||||
delta = now_tz.replace(tzinfo=tz_backend.utc_tzobj) - now
|
||||
display = "GMT{sign}{gmt_diff} {timezone}".format(
|
||||
sign="+" if delta == abs(delta) else "-",
|
||||
gmt_diff=str(abs(delta)).zfill(8)[:-3],
|
||||
timezone=tz_str.replace("_", " "),
|
||||
)
|
||||
_choices.append((delta, tz, display))
|
||||
_choices.sort(key=lambda x: x[0])
|
||||
choices = [(one, two) for zero, one, two in _choices]
|
||||
return sorted(choices, key=normalize_gmt)
|
||||
@@ -0,0 +1,158 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from timezone_field.backends import TimeZoneNotFoundError, get_tz_backend
|
||||
from timezone_field.choices import standard, with_gmt_offset
|
||||
from timezone_field.utils import AutoDeserializedAttribute
|
||||
|
||||
|
||||
class TimeZoneField(models.Field):
|
||||
"""
|
||||
Provides database store for pytz timezone objects.
|
||||
|
||||
Valid inputs:
|
||||
* use_pytz=True:
|
||||
* any instance of pytz.tzinfo.DstTzInfo or pytz.tzinfo.StaticTzInfo
|
||||
* the pytz.UTC singleton
|
||||
* any string that validates against pytz.common_timezones. pytz will
|
||||
be used to build a timezone object from the string.
|
||||
* use_pytz=False:
|
||||
* any instance of zoneinfo.ZoneInfo
|
||||
* any string that validates against zoneinfo.available_timezones().
|
||||
* None and the empty string both represent 'no timezone'
|
||||
|
||||
Valid outputs:
|
||||
* None
|
||||
* use_pytz=True: instances of pytz.tzinfo.DstTzInfo,
|
||||
pytz.tzinfo.StaticTzInfo and the pytz.UTC singleton
|
||||
* use_pytz=False: instances of zoneinfo.ZoneInfo
|
||||
|
||||
Blank values are stored in the DB as the empty string. Timezones are stored
|
||||
in their string representation.
|
||||
|
||||
The `choices` kwarg can be specified as a list of either
|
||||
[<timezone object>, <str>] or [<str>, <str>]. Internally in memory, it is
|
||||
stored as [<timezone object>, <str>].
|
||||
"""
|
||||
|
||||
descriptor_class = AutoDeserializedAttribute
|
||||
|
||||
description = "A timezone object"
|
||||
|
||||
# NOTE: these defaults are excluded from migrations. If these are changed,
|
||||
# existing migration files will need to be accomodated.
|
||||
default_max_length = 63
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# allow some use of positional args up until the args we customize
|
||||
# https://github.com/mfogel/django-timezone-field/issues/42
|
||||
# https://github.com/django/django/blob/1.11.11/django/db/models/fields/__init__.py#L145
|
||||
if len(args) > 3:
|
||||
raise ValueError("Cannot specify max_length by positional arg")
|
||||
kwargs.setdefault("max_length", self.default_max_length)
|
||||
|
||||
self.use_pytz = kwargs.pop("use_pytz", None)
|
||||
self.tz_backend = get_tz_backend(self.use_pytz)
|
||||
self.default_tzs = [self.tz_backend.to_tzobj(v) for v in self.tz_backend.base_tzstrs]
|
||||
|
||||
if "choices" in kwargs:
|
||||
values, displays = zip(*kwargs["choices"])
|
||||
# Choices can be specified in two forms: either
|
||||
# [<timezone object>, <str>] or [<str>, <str>]
|
||||
#
|
||||
# The [<timezone object>, <str>] format is the one we actually
|
||||
# store the choices in memory because of
|
||||
# https://github.com/mfogel/django-timezone-field/issues/24
|
||||
#
|
||||
# The [<str>, <str>] format is supported because since django
|
||||
# can't deconstruct pytz.timezone objects, migration files must
|
||||
# use an alternate format. Representing the timezones as strings
|
||||
# is the obvious choice.
|
||||
if not self.tz_backend.is_tzobj(values[0]):
|
||||
# using force_str b/c of https://github.com/mfogel/django-timezone-field/issues/38
|
||||
values = [self.tz_backend.to_tzobj(force_str(v)) for v in values]
|
||||
else:
|
||||
values = self.default_tzs
|
||||
displays = None
|
||||
|
||||
self.choices_display = kwargs.pop("choices_display", None)
|
||||
if self.choices_display == "WITH_GMT_OFFSET":
|
||||
choices = with_gmt_offset(values, use_pytz=self.use_pytz)
|
||||
elif self.choices_display == "STANDARD":
|
||||
choices = standard(values)
|
||||
elif self.choices_display is None:
|
||||
choices = zip(values, displays) if displays else standard(values)
|
||||
else:
|
||||
raise ValueError(f"Unrecognized value for kwarg 'choices_display' of '{self.choices_display}'")
|
||||
|
||||
kwargs["choices"] = choices
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
if not self.tz_backend.is_tzobj(value):
|
||||
raise ValidationError(f"'{value}' is not a pytz timezone object")
|
||||
super().validate(value, model_instance)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if kwargs.get("max_length") == self.default_max_length:
|
||||
del kwargs["max_length"]
|
||||
|
||||
if self.use_pytz is not None:
|
||||
kwargs["use_pytz"] = self.use_pytz
|
||||
|
||||
if self.choices_display is not None:
|
||||
kwargs["choices_display"] = self.choices_display
|
||||
|
||||
# don't assume super().deconstruct() will pass us back our kwargs["choices"]
|
||||
# https://github.com/mfogel/django-timezone-field/issues/96
|
||||
if "choices" in kwargs:
|
||||
if self.choices_display is None:
|
||||
if kwargs["choices"] == standard(self.default_tzs):
|
||||
kwargs.pop("choices")
|
||||
else:
|
||||
values, _ = zip(*kwargs["choices"])
|
||||
if sorted(values, key=str) == sorted(self.default_tzs, key=str):
|
||||
kwargs.pop("choices")
|
||||
else:
|
||||
kwargs["choices"] = [(value, "") for value in values]
|
||||
|
||||
# django can't decontruct pytz objects, so transform choices
|
||||
# to [<str>, <str>] format for writing out to the migration
|
||||
if "choices" in kwargs:
|
||||
kwargs["choices"] = [(str(tz), n) for tz, n in kwargs["choices"]]
|
||||
|
||||
return name, path, args, kwargs
|
||||
|
||||
def get_internal_type(self):
|
||||
return "CharField"
|
||||
|
||||
def get_default(self):
|
||||
# allow defaults to be still specified as strings. Allows for easy
|
||||
# serialization into migration files
|
||||
value = super().get_default()
|
||||
return self._get_python_and_db_repr(value)[0]
|
||||
|
||||
def from_db_value(self, value, *_args):
|
||||
"Convert to pytz timezone object"
|
||||
return self._get_python_and_db_repr(value)[0]
|
||||
|
||||
def to_python(self, value):
|
||||
"Convert to pytz timezone object"
|
||||
return self._get_python_and_db_repr(value)[0]
|
||||
|
||||
def get_prep_value(self, value):
|
||||
"Convert to string describing a valid pytz timezone object"
|
||||
return self._get_python_and_db_repr(value)[1]
|
||||
|
||||
def _get_python_and_db_repr(self, value):
|
||||
"Returns a tuple of (python representation, db representation)"
|
||||
if value is None or value == "":
|
||||
return (None, "")
|
||||
if self.tz_backend.is_tzobj(value):
|
||||
return (value, str(value))
|
||||
try:
|
||||
return (self.tz_backend.to_tzobj(force_str(value)), force_str(value))
|
||||
except TimeZoneNotFoundError as err:
|
||||
raise ValidationError(f"Invalid timezone '{value}'") from err
|
||||
@@ -0,0 +1,42 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from timezone_field.backends import TimeZoneNotFoundError, get_tz_backend
|
||||
from timezone_field.choices import standard, with_gmt_offset
|
||||
|
||||
|
||||
def get_coerce(tz_backend):
|
||||
def coerce(val):
|
||||
try:
|
||||
return tz_backend.to_tzobj(val)
|
||||
except TimeZoneNotFoundError as err:
|
||||
raise ValidationError(f"Unknown time zone: '{val}'") from err
|
||||
|
||||
return coerce
|
||||
|
||||
|
||||
class TimeZoneFormField(forms.TypedChoiceField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.use_pytz = kwargs.pop("use_pytz", None)
|
||||
self.tz_backend = get_tz_backend(self.use_pytz)
|
||||
kwargs.setdefault("coerce", get_coerce(self.tz_backend))
|
||||
kwargs.setdefault("empty_value", None)
|
||||
|
||||
if "choices" in kwargs:
|
||||
values, displays = zip(*kwargs["choices"])
|
||||
else:
|
||||
values = self.tz_backend.base_tzstrs
|
||||
displays = None
|
||||
|
||||
choices_display = kwargs.pop("choices_display", None)
|
||||
if choices_display == "WITH_GMT_OFFSET":
|
||||
choices = with_gmt_offset(values, use_pytz=self.use_pytz)
|
||||
elif choices_display == "STANDARD":
|
||||
choices = standard(values)
|
||||
elif choices_display is None:
|
||||
choices = zip(values, displays) if displays else standard(values)
|
||||
else:
|
||||
raise ValueError(f"Unrecognized value for kwarg 'choices_display' of '{choices_display}'")
|
||||
|
||||
kwargs["choices"] = choices
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -0,0 +1 @@
|
||||
# intentionally left blank
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.fields import CharField
|
||||
|
||||
from timezone_field.backends import TimeZoneNotFoundError, get_tz_backend
|
||||
|
||||
|
||||
class TimeZoneSerializerField(CharField):
|
||||
default_error_messages = {
|
||||
"invalid": _("A valid timezone is required."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.use_pytz = kwargs.pop("use_pytz", None)
|
||||
self.tz_backend = get_tz_backend(use_pytz=self.use_pytz)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data_str = force_str(data)
|
||||
try:
|
||||
return self.tz_backend.to_tzobj(data_str)
|
||||
except TimeZoneNotFoundError:
|
||||
self.fail("invalid")
|
||||
|
||||
def to_representation(self, value):
|
||||
return str(value)
|
||||
@@ -0,0 +1,19 @@
|
||||
from django.db.models.query_utils import DeferredAttribute
|
||||
|
||||
|
||||
class AutoDeserializedAttribute(DeferredAttribute):
|
||||
"""
|
||||
Use as the descriptor_class for a Django custom field.
|
||||
Allows setting the field to a serialized (typically string) value,
|
||||
and immediately reflecting that as the deserialized `to_python` value.
|
||||
|
||||
(This requires that the field's `to_python` returns the same thing
|
||||
whether called with a serialized or deserialized value.)
|
||||
"""
|
||||
|
||||
# (Adapted from django.db.models.fields.subclassing.Creator,
|
||||
# which was included in Django 1.8 and earlier.)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
value = self.field.to_python(value)
|
||||
instance.__dict__[self.field.attname] = value
|
||||
Reference in New Issue
Block a user