Updates
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MongoDB model fields emulating Django Extensions' additional model fields
|
||||
|
||||
These fields are essentially identical to existing Extensions fields.
|
||||
"""
|
||||
|
||||
import re
|
||||
import datetime
|
||||
from django import forms
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mongoengine.fields import StringField, DateTimeField
|
||||
|
||||
import uuid
|
||||
|
||||
|
||||
class SlugField(StringField):
|
||||
description = _("String (up to %(max_length)s)")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["max_length"] = kwargs.get("max_length", 50)
|
||||
# Set db_index=True unless it's been set manually.
|
||||
if "db_index" not in kwargs:
|
||||
kwargs["db_index"] = True
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "SlugField"
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {"form_class": forms.SlugField}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class AutoSlugField(SlugField):
|
||||
"""
|
||||
AutoSlugField, adapted for MongoDB
|
||||
|
||||
By default, sets editable=False, blank=True.
|
||||
|
||||
Required arguments:
|
||||
|
||||
populate_from
|
||||
Specifies which field or list of fields the slug is populated from.
|
||||
|
||||
Optional arguments:
|
||||
|
||||
separator
|
||||
Defines the used separator (default: '-')
|
||||
|
||||
overwrite
|
||||
If set to True, overwrites the slug on every save (default: False)
|
||||
|
||||
Inspired by SmileyChris' Unique Slugify snippet:
|
||||
https://www.djangosnippets.org/snippets/690/
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("blank", True)
|
||||
kwargs.setdefault("editable", False)
|
||||
|
||||
populate_from = kwargs.pop("populate_from", None)
|
||||
if populate_from is None:
|
||||
raise ValueError("missing 'populate_from' argument")
|
||||
else:
|
||||
self._populate_from = populate_from
|
||||
|
||||
self.slugify_function = kwargs.pop("slugify_function", slugify)
|
||||
self.separator = kwargs.pop("separator", str("-"))
|
||||
self.overwrite = kwargs.pop("overwrite", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _slug_strip(self, value):
|
||||
"""
|
||||
Clean up a slug by removing slug separator characters that occur at
|
||||
the beginning or end of a slug.
|
||||
|
||||
If an alternate separator is used, it will also replace any instances
|
||||
of the default '-' separator with the new separator.
|
||||
"""
|
||||
re_sep = "(?:-|%s)" % re.escape(self.separator)
|
||||
value = re.sub("%s+" % re_sep, self.separator, value)
|
||||
return re.sub(r"^%s+|%s+$" % (re_sep, re_sep), "", value)
|
||||
|
||||
def slugify_func(self, content):
|
||||
return self.slugify_function(content)
|
||||
|
||||
def create_slug(self, model_instance, add):
|
||||
# get fields to populate from and slug field to set
|
||||
if not isinstance(self._populate_from, (list, tuple)):
|
||||
self._populate_from = (self._populate_from,)
|
||||
slug_field = model_instance._meta.get_field(self.attname)
|
||||
|
||||
if add or self.overwrite:
|
||||
# slugify the original field content and set next step to 2
|
||||
slug_for_field = lambda lookup_value: self.slugify_func(
|
||||
self.get_slug_fields(model_instance, lookup_value)
|
||||
)
|
||||
slug = self.separator.join(map(slug_for_field, self._populate_from))
|
||||
next = 2
|
||||
else:
|
||||
# get slug from the current model instance and calculate next
|
||||
# step from its number, clean-up
|
||||
slug = self._slug_strip(getattr(model_instance, self.attname))
|
||||
next = slug.split(self.separator)[-1]
|
||||
if next.isdigit():
|
||||
slug = self.separator.join(slug.split(self.separator)[:-1])
|
||||
next = int(next)
|
||||
else:
|
||||
next = 2
|
||||
|
||||
# strip slug depending on max_length attribute of the slug field
|
||||
# and clean-up
|
||||
slug_len = slug_field.max_length
|
||||
if slug_len:
|
||||
slug = slug[:slug_len]
|
||||
slug = self._slug_strip(slug)
|
||||
original_slug = slug
|
||||
|
||||
# exclude the current model instance from the queryset used in finding
|
||||
# the next valid slug
|
||||
queryset = model_instance.__class__._default_manager.all()
|
||||
if model_instance.pk:
|
||||
queryset = queryset.exclude(pk=model_instance.pk)
|
||||
|
||||
# form a kwarg dict used to impliment any unique_together constraints
|
||||
kwargs = {}
|
||||
for params in model_instance._meta.unique_together:
|
||||
if self.attname in params:
|
||||
for param in params:
|
||||
kwargs[param] = getattr(model_instance, param, None)
|
||||
kwargs[self.attname] = slug
|
||||
|
||||
# increases the number while searching for the next valid slug
|
||||
# depending on the given slug, clean-up
|
||||
while not slug or queryset.filter(**kwargs):
|
||||
slug = original_slug
|
||||
end = "%s%s" % (self.separator, next)
|
||||
end_len = len(end)
|
||||
if slug_len and len(slug) + end_len > slug_len:
|
||||
slug = slug[: slug_len - end_len]
|
||||
slug = self._slug_strip(slug)
|
||||
slug = "%s%s" % (slug, end)
|
||||
kwargs[self.attname] = slug
|
||||
next += 1
|
||||
return slug
|
||||
|
||||
def get_slug_fields(self, model_instance, lookup_value):
|
||||
lookup_value_path = lookup_value.split(LOOKUP_SEP)
|
||||
attr = model_instance
|
||||
for elem in lookup_value_path:
|
||||
try:
|
||||
attr = getattr(attr, elem)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
"value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format( # noqa: E501
|
||||
elem, lookup_value, attr, elem
|
||||
)
|
||||
)
|
||||
|
||||
if callable(attr):
|
||||
return "%s" % attr()
|
||||
|
||||
return attr
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
value = str(self.create_slug(model_instance, add))
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "SlugField"
|
||||
|
||||
|
||||
class CreationDateTimeField(DateTimeField):
|
||||
"""
|
||||
CreationDateTimeField
|
||||
|
||||
By default, sets editable=False, blank=True, default=datetime.now
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("default", datetime.datetime.now)
|
||||
DateTimeField.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DateTimeField"
|
||||
|
||||
|
||||
class ModificationDateTimeField(CreationDateTimeField):
|
||||
"""
|
||||
ModificationDateTimeField
|
||||
|
||||
By default, sets editable=False, blank=True, default=datetime.now
|
||||
|
||||
Sets value to datetime.now() on each save of the model.
|
||||
"""
|
||||
|
||||
def pre_save(self, model, add):
|
||||
value = datetime.datetime.now()
|
||||
setattr(model, self.attname, value)
|
||||
return value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DateTimeField"
|
||||
|
||||
|
||||
class UUIDVersionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UUIDField(StringField):
|
||||
"""
|
||||
UUIDField
|
||||
|
||||
By default uses UUID version 1 (generate from host ID, sequence number and current time)
|
||||
|
||||
The field support all uuid versions which are natively supported by the uuid python module.
|
||||
For more information see: https://docs.python.org/lib/module-uuid.html
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verbose_name=None,
|
||||
name=None,
|
||||
auto=True,
|
||||
version=1,
|
||||
node=None,
|
||||
clock_seq=None,
|
||||
namespace=None,
|
||||
**kwargs,
|
||||
):
|
||||
kwargs["max_length"] = 36
|
||||
self.auto = auto
|
||||
self.version = version
|
||||
if version == 1:
|
||||
self.node, self.clock_seq = node, clock_seq
|
||||
elif version == 3 or version == 5:
|
||||
self.namespace, self.name = namespace, name
|
||||
StringField.__init__(self, verbose_name, name, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return StringField.__name__
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
if self.primary_key:
|
||||
assert not cls._meta.has_auto_field, (
|
||||
"A model can't have more than one AutoField: %s %s %s; have %s"
|
||||
% (self, cls, name, cls._meta.auto_field)
|
||||
)
|
||||
super().contribute_to_class(cls, name)
|
||||
cls._meta.has_auto_field = True
|
||||
cls._meta.auto_field = self
|
||||
else:
|
||||
super().contribute_to_class(cls, name)
|
||||
|
||||
def create_uuid(self):
|
||||
if not self.version or self.version == 4:
|
||||
return uuid.uuid4()
|
||||
elif self.version == 1:
|
||||
return uuid.uuid1(self.node, self.clock_seq)
|
||||
elif self.version == 2:
|
||||
raise UUIDVersionError("UUID version 2 is not supported.")
|
||||
elif self.version == 3:
|
||||
return uuid.uuid3(self.namespace, self.name)
|
||||
elif self.version == 5:
|
||||
return uuid.uuid5(self.namespace, self.name)
|
||||
else:
|
||||
raise UUIDVersionError("UUID version %s is not valid." % self.version)
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
if self.auto and add:
|
||||
value = str(self.create_uuid())
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
else:
|
||||
value = super().pre_save(model_instance, add)
|
||||
if self.auto and not value:
|
||||
value = str(self.create_uuid())
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JSONField automatically serializes most Python terms to JSON data.
|
||||
Creates a TEXT field with a default value of "{}". See test_json.py for
|
||||
more information.
|
||||
|
||||
from django.db import models
|
||||
from django_extensions.db.fields import json
|
||||
|
||||
class LOL(models.Model):
|
||||
extra = json.JSONField()
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import json
|
||||
from django.conf import settings
|
||||
from mongoengine.fields import StringField
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Decimal):
|
||||
return str(obj)
|
||||
elif isinstance(obj, datetime.datetime):
|
||||
assert settings.TIME_ZONE == "UTC"
|
||||
return obj.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def dumps(value):
|
||||
assert isinstance(value, dict)
|
||||
return JSONEncoder().encode(value)
|
||||
|
||||
|
||||
def loads(txt):
|
||||
value = json.loads(txt, parse_float=Decimal)
|
||||
assert isinstance(value, dict)
|
||||
return value
|
||||
|
||||
|
||||
class JSONDict(dict):
|
||||
"""
|
||||
Hack so repr() called by dumpdata will output JSON instead of
|
||||
Python formatted data. This way fixtures will work!
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return dumps(self)
|
||||
|
||||
|
||||
class JSONField(StringField):
|
||||
"""
|
||||
JSONField is a generic textfield that neatly serializes/unserializes
|
||||
JSON objects seamlessly. Main object must be a dict object.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "default" not in kwargs:
|
||||
kwargs["default"] = "{}"
|
||||
StringField.__init__(self, *args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert our string value to JSON after we load it from the DB"""
|
||||
if not value:
|
||||
return {}
|
||||
elif isinstance(value, str):
|
||||
res = loads(value)
|
||||
assert isinstance(res, dict)
|
||||
return JSONDict(**res)
|
||||
else:
|
||||
return value
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
"""Convert our JSON object to a string before we save"""
|
||||
if not value:
|
||||
return super().get_db_prep_save("")
|
||||
else:
|
||||
return super().get_db_prep_save(dumps(value))
|
||||
Reference in New Issue
Block a user