forked from 0x2620/pandora
include copy of django_extension, dont install django from git
This commit is contained in:
parent
055018f12e
commit
3f7215035a
200 changed files with 14119 additions and 4 deletions
|
|
@ -0,0 +1,287 @@
|
|||
"""
|
||||
Django Extensions additional model fields
|
||||
"""
|
||||
import re
|
||||
import six
|
||||
try:
|
||||
import uuid
|
||||
HAS_UUID = True
|
||||
except ImportError:
|
||||
HAS_UUID = False
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.db.models import DateTimeField, CharField, SlugField
|
||||
|
||||
try:
|
||||
from django.utils.timezone import now as datetime_now
|
||||
assert datetime_now
|
||||
except ImportError:
|
||||
import datetime
|
||||
datetime_now = datetime.datetime.now
|
||||
|
||||
try:
|
||||
from django.utils.encoding import force_unicode # NOQA
|
||||
except ImportError:
|
||||
from django.utils.encoding import force_text as force_unicode # NOQA
|
||||
|
||||
|
||||
class AutoSlugField(SlugField):
|
||||
""" AutoSlugField
|
||||
|
||||
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:
|
||||
http://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.separator = kwargs.pop('separator', six.u('-'))
|
||||
self.overwrite = kwargs.pop('overwrite', False)
|
||||
self.allow_duplicates = kwargs.pop('allow_duplicates', False)
|
||||
super(AutoSlugField, self).__init__(*args, **kwargs)
|
||||
|
||||
def _slug_strip(self, value):
|
||||
"""
|
||||
Cleans 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 get_queryset(self, model_cls, slug_field):
|
||||
for field, model in model_cls._meta.get_fields_with_model():
|
||||
if model and field == slug_field:
|
||||
return model._default_manager.all()
|
||||
return model_cls._default_manager.all()
|
||||
|
||||
def slugify_func(self, content):
|
||||
if content:
|
||||
return slugify(content)
|
||||
return ''
|
||||
|
||||
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 field: self.slugify_func(getattr(model_instance, field))
|
||||
slug = self.separator.join(map(slug_for_field, self._populate_from))
|
||||
next = 2
|
||||
else:
|
||||
# get slug from the current model instance
|
||||
slug = getattr(model_instance, self.attname)
|
||||
# model_instance is being modified, and overwrite is False,
|
||||
# so instead of doing anything, just return the current slug
|
||||
return slug
|
||||
|
||||
# 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
|
||||
|
||||
if self.allow_duplicates:
|
||||
return slug
|
||||
|
||||
# exclude the current model instance from the queryset used in finding
|
||||
# the next valid slug
|
||||
queryset = self.get_queryset(model_instance.__class__, slug_field)
|
||||
if model_instance.pk:
|
||||
queryset = queryset.exclude(pk=model_instance.pk)
|
||||
|
||||
# form a kwarg dict used to impliment any unique_together contraints
|
||||
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 pre_save(self, model_instance, add):
|
||||
value = force_unicode(self.create_slug(model_instance, add))
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "SlugField"
|
||||
|
||||
def south_field_triple(self):
|
||||
"Returns a suitable description of this field for South."
|
||||
# We'll just introspect the _actual_ field.
|
||||
from south.modelsinspector import introspector
|
||||
field_class = '%s.AutoSlugField' % self.__module__
|
||||
args, kwargs = introspector(self)
|
||||
kwargs.update({
|
||||
'populate_from': repr(self._populate_from),
|
||||
'separator': repr(self.separator),
|
||||
'overwrite': repr(self.overwrite),
|
||||
'allow_duplicates': repr(self.allow_duplicates),
|
||||
})
|
||||
# That's our definition!
|
||||
return (field_class, args, kwargs)
|
||||
|
||||
|
||||
class CreationDateTimeField(DateTimeField):
|
||||
""" CreationDateTimeField
|
||||
|
||||
By default, sets editable=False, blank=True, default=datetime.now
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('editable', False)
|
||||
kwargs.setdefault('blank', True)
|
||||
kwargs.setdefault('default', datetime_now)
|
||||
DateTimeField.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DateTimeField"
|
||||
|
||||
def south_field_triple(self):
|
||||
"Returns a suitable description of this field for South."
|
||||
# We'll just introspect ourselves, since we inherit.
|
||||
from south.modelsinspector import introspector
|
||||
field_class = "django.db.models.fields.DateTimeField"
|
||||
args, kwargs = introspector(self)
|
||||
return (field_class, args, kwargs)
|
||||
|
||||
|
||||
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_now()
|
||||
setattr(model, self.attname, value)
|
||||
return value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DateTimeField"
|
||||
|
||||
def south_field_triple(self):
|
||||
"Returns a suitable description of this field for South."
|
||||
# We'll just introspect ourselves, since we inherit.
|
||||
from south.modelsinspector import introspector
|
||||
field_class = "django.db.models.fields.DateTimeField"
|
||||
args, kwargs = introspector(self)
|
||||
return (field_class, args, kwargs)
|
||||
|
||||
|
||||
class UUIDVersionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UUIDField(CharField):
|
||||
""" UUIDField
|
||||
|
||||
By default uses UUID version 4 (randomly generated UUID).
|
||||
|
||||
The field support all uuid versions which are natively supported by the uuid python module, except version 2.
|
||||
For more information see: http://docs.python.org/lib/module-uuid.html
|
||||
"""
|
||||
|
||||
def __init__(self, verbose_name=None, name=None, auto=True, version=4, node=None, clock_seq=None, namespace=None, **kwargs):
|
||||
if not HAS_UUID:
|
||||
raise ImproperlyConfigured("'uuid' module is required for UUIDField. (Do you have Python 2.5 or higher installed ?)")
|
||||
kwargs.setdefault('max_length', 36)
|
||||
if auto:
|
||||
self.empty_strings_allowed = False
|
||||
kwargs['blank'] = True
|
||||
kwargs.setdefault('editable', False)
|
||||
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
|
||||
CharField.__init__(self, verbose_name, name, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return CharField.__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):
|
||||
value = super(UUIDField, self).pre_save(model_instance, add)
|
||||
if self.auto and add and value is None:
|
||||
value = force_unicode(self.create_uuid())
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
else:
|
||||
if self.auto and not value:
|
||||
value = force_unicode(self.create_uuid())
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
if self.auto:
|
||||
return None
|
||||
return super(UUIDField, self).formfield(**kwargs)
|
||||
|
||||
def south_field_triple(self):
|
||||
"Returns a suitable description of this field for South."
|
||||
# We'll just introspect the _actual_ field.
|
||||
from south.modelsinspector import introspector
|
||||
field_class = "django.db.models.fields.CharField"
|
||||
args, kwargs = introspector(self)
|
||||
# That's our definition!
|
||||
return (field_class, args, kwargs)
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import six
|
||||
from django.db import models
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
import warnings
|
||||
|
||||
try:
|
||||
from keyczar import keyczar
|
||||
except ImportError:
|
||||
raise ImportError('Using an encrypted field requires the Keyczar module. '
|
||||
'You can obtain Keyczar from http://www.keyczar.org/.')
|
||||
|
||||
|
||||
class EncryptionWarning(RuntimeWarning):
|
||||
pass
|
||||
|
||||
|
||||
class BaseEncryptedField(models.Field):
|
||||
prefix = 'enc_str:::'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not hasattr(settings, 'ENCRYPTED_FIELD_KEYS_DIR'):
|
||||
raise ImproperlyConfigured('You must set the settings.ENCRYPTED_FIELD_KEYS_DIR '
|
||||
'setting to your Keyczar keys directory.')
|
||||
crypt_class = self.get_crypt_class()
|
||||
self.crypt = crypt_class.Read(settings.ENCRYPTED_FIELD_KEYS_DIR)
|
||||
|
||||
# Encrypted size is larger than unencrypted
|
||||
self.unencrypted_length = max_length = kwargs.get('max_length', None)
|
||||
if max_length:
|
||||
max_length = len(self.prefix) + len(self.crypt.Encrypt('x' * max_length))
|
||||
# TODO: Re-examine if this logic will actually make a large-enough
|
||||
# max-length for unicode strings that have non-ascii characters in them.
|
||||
kwargs['max_length'] = max_length
|
||||
|
||||
super(BaseEncryptedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_crypt_class(self):
|
||||
"""
|
||||
Get the Keyczar class to use.
|
||||
|
||||
The class can be customized with the ENCRYPTED_FIELD_MODE setting. By default,
|
||||
this setting is DECRYPT_AND_ENCRYPT. Set this to ENCRYPT to disable decryption.
|
||||
This is necessary if you are only providing public keys to Keyczar.
|
||||
|
||||
Returns:
|
||||
keyczar.Encrypter if ENCRYPTED_FIELD_MODE is ENCRYPT.
|
||||
keyczar.Crypter if ENCRYPTED_FIELD_MODE is DECRYPT_AND_ENCRYPT.
|
||||
|
||||
Override this method to customize the type of Keyczar class returned.
|
||||
"""
|
||||
|
||||
crypt_type = getattr(settings, 'ENCRYPTED_FIELD_MODE', 'DECRYPT_AND_ENCRYPT')
|
||||
if crypt_type == 'ENCRYPT':
|
||||
crypt_class_name = 'Encrypter'
|
||||
elif crypt_type == 'DECRYPT_AND_ENCRYPT':
|
||||
crypt_class_name = 'Crypter'
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
'ENCRYPTED_FIELD_MODE must be either DECRYPT_AND_ENCRYPT '
|
||||
'or ENCRYPT, not %s.' % crypt_type)
|
||||
return getattr(keyczar, crypt_class_name)
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(self.crypt.primary_key, keyczar.keys.RsaPublicKey):
|
||||
retval = value
|
||||
elif value and (value.startswith(self.prefix)):
|
||||
if hasattr(self.crypt, 'Decrypt'):
|
||||
retval = self.crypt.Decrypt(value[len(self.prefix):])
|
||||
if retval:
|
||||
retval = retval.decode('utf-8')
|
||||
else:
|
||||
retval = value
|
||||
else:
|
||||
retval = value
|
||||
return retval
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if value and not value.startswith(self.prefix):
|
||||
# We need to encode a unicode string into a byte string, first.
|
||||
# keyczar expects a bytestring, not a unicode string.
|
||||
if type(value) == six.types.UnicodeType:
|
||||
value = value.encode('utf-8')
|
||||
# Truncated encrypted content is unreadable,
|
||||
# so truncate before encryption
|
||||
max_length = self.unencrypted_length
|
||||
if max_length and len(value) > max_length:
|
||||
warnings.warn("Truncating field %s from %d to %d bytes" % (
|
||||
self.name, len(value), max_length), EncryptionWarning
|
||||
)
|
||||
value = value[:max_length]
|
||||
|
||||
value = self.prefix + self.crypt.Encrypt(value)
|
||||
return value
|
||||
|
||||
|
||||
class EncryptedTextField(six.with_metaclass(models.SubfieldBase,
|
||||
BaseEncryptedField)):
|
||||
def get_internal_type(self):
|
||||
return 'TextField'
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'widget': forms.Textarea}
|
||||
defaults.update(kwargs)
|
||||
return super(EncryptedTextField, self).formfield(**defaults)
|
||||
|
||||
def south_field_triple(self):
|
||||
"Returns a suitable description of this field for South."
|
||||
# We'll just introspect the _actual_ field.
|
||||
from south.modelsinspector import introspector
|
||||
field_class = "django.db.models.fields.TextField"
|
||||
args, kwargs = introspector(self)
|
||||
# That's our definition!
|
||||
return (field_class, args, kwargs)
|
||||
|
||||
|
||||
class EncryptedCharField(six.with_metaclass(models.SubfieldBase,
|
||||
BaseEncryptedField)):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EncryptedCharField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "CharField"
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'max_length': self.max_length}
|
||||
defaults.update(kwargs)
|
||||
return super(EncryptedCharField, self).formfield(**defaults)
|
||||
|
||||
def south_field_triple(self):
|
||||
"Returns a suitable description of this field for South."
|
||||
# We'll just introspect the _actual_ field.
|
||||
from south.modelsinspector import introspector
|
||||
field_class = "django.db.models.fields.CharField"
|
||||
args, kwargs = introspector(self)
|
||||
# That's our definition!
|
||||
return (field_class, args, kwargs)
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
"""
|
||||
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 six
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
try:
|
||||
# Django <= 1.6 backwards compatibility
|
||||
from django.utils import simplejson as json
|
||||
except ImportError:
|
||||
# Django >= 1.7
|
||||
import json
|
||||
|
||||
|
||||
def dumps(value):
|
||||
return DjangoJSONEncoder().encode(value)
|
||||
|
||||
|
||||
def loads(txt):
|
||||
value = json.loads(
|
||||
txt,
|
||||
parse_float=Decimal,
|
||||
encoding=settings.DEFAULT_CHARSET
|
||||
)
|
||||
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 JSONList(list):
|
||||
"""
|
||||
As above
|
||||
"""
|
||||
def __repr__(self):
|
||||
return dumps(self)
|
||||
|
||||
|
||||
class JSONField(six.with_metaclass(models.SubfieldBase, models.TextField)):
|
||||
"""JSONField is a generic textfield that neatly serializes/unserializes
|
||||
JSON objects seamlessly. Main thingy must be a dict object."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
default = kwargs.get('default', None)
|
||||
if default is None:
|
||||
kwargs['default'] = '{}'
|
||||
elif isinstance(default, (list, dict)):
|
||||
kwargs['default'] = dumps(default)
|
||||
models.TextField.__init__(self, *args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert our string value to JSON after we load it from the DB"""
|
||||
if value is None or value == '':
|
||||
return {}
|
||||
elif isinstance(value, six.string_types):
|
||||
res = loads(value)
|
||||
if isinstance(res, dict):
|
||||
return JSONDict(**res)
|
||||
else:
|
||||
return JSONList(res)
|
||||
|
||||
else:
|
||||
return value
|
||||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
"""Convert our JSON object to a string before we save"""
|
||||
if not isinstance(value, (list, dict)):
|
||||
return super(JSONField, self).get_db_prep_save("", connection=connection)
|
||||
else:
|
||||
return super(JSONField, self).get_db_prep_save(dumps(value),
|
||||
connection=connection)
|
||||
|
||||
def south_field_triple(self):
|
||||
"Returns a suitable description of this field for South."
|
||||
# We'll just introspect the _actual_ field.
|
||||
from south.modelsinspector import introspector
|
||||
field_class = "django.db.models.fields.TextField"
|
||||
args, kwargs = introspector(self)
|
||||
# That's our definition!
|
||||
return (field_class, args, kwargs)
|
||||
78
contrib/django_extensions/django_extensions/db/models.py
Normal file
78
contrib/django_extensions/django_extensions/db/models.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""
|
||||
Django Extensions abstract base model classes.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_extensions.db.fields import (ModificationDateTimeField,
|
||||
CreationDateTimeField, AutoSlugField)
|
||||
|
||||
try:
|
||||
from django.utils.timezone import now as datetime_now
|
||||
assert datetime_now
|
||||
except ImportError:
|
||||
import datetime
|
||||
datetime_now = datetime.datetime.now
|
||||
|
||||
|
||||
class TimeStampedModel(models.Model):
|
||||
""" TimeStampedModel
|
||||
An abstract base class model that provides self-managed "created" and
|
||||
"modified" fields.
|
||||
"""
|
||||
created = CreationDateTimeField(_('created'))
|
||||
modified = ModificationDateTimeField(_('modified'))
|
||||
|
||||
class Meta:
|
||||
get_latest_by = 'modified'
|
||||
ordering = ('-modified', '-created',)
|
||||
abstract = True
|
||||
|
||||
|
||||
class TitleSlugDescriptionModel(models.Model):
|
||||
""" TitleSlugDescriptionModel
|
||||
An abstract base class model that provides title and description fields
|
||||
and a self-managed "slug" field that populates from the title.
|
||||
"""
|
||||
title = models.CharField(_('title'), max_length=255)
|
||||
slug = AutoSlugField(_('slug'), populate_from='title')
|
||||
description = models.TextField(_('description'), blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class ActivatorModelManager(models.Manager):
|
||||
""" ActivatorModelManager
|
||||
Manager to return instances of ActivatorModel: SomeModel.objects.active() / .inactive()
|
||||
"""
|
||||
def active(self):
|
||||
""" Returns active instances of ActivatorModel: SomeModel.objects.active() """
|
||||
return self.get_query_set().filter(status=ActivatorModel.ACTIVE_STATUS)
|
||||
|
||||
def inactive(self):
|
||||
""" Returns inactive instances of ActivatorModel: SomeModel.objects.inactive() """
|
||||
return self.get_query_set().filter(status=ActivatorModel.INACTIVE_STATUS)
|
||||
|
||||
|
||||
class ActivatorModel(models.Model):
|
||||
""" ActivatorModel
|
||||
An abstract base class model that provides activate and deactivate fields.
|
||||
"""
|
||||
INACTIVE_STATUS, ACTIVE_STATUS = range(2)
|
||||
STATUS_CHOICES = (
|
||||
(INACTIVE_STATUS, _('Inactive')),
|
||||
(ACTIVE_STATUS, _('Active')),
|
||||
)
|
||||
status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=ACTIVE_STATUS)
|
||||
activate_date = models.DateTimeField(blank=True, null=True, help_text=_('keep empty for an immediate activation'))
|
||||
deactivate_date = models.DateTimeField(blank=True, null=True, help_text=_('keep empty for indefinite activation'))
|
||||
objects = ActivatorModelManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('status', '-activate_date',)
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.activate_date:
|
||||
self.activate_date = datetime_now()
|
||||
super(ActivatorModel, self).save(*args, **kwargs)
|
||||
Loading…
Add table
Add a link
Reference in a new issue