├── example ├── __init__.py ├── computed │ ├── __init__.py │ ├── tests.py │ └── models.py ├── timestamp │ ├── __init__.py │ ├── tests.py │ └── models.py ├── urls.py ├── wsgi.py └── settings.py ├── requirements.txt ├── dev-requirements.txt ├── .gitignore ├── manage.py └── Readme.markdown /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/computed/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/timestamp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-extensions 3 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | ipython 3 | ipdb 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | *.csv 4 | *.pyc 5 | *.swp 6 | *.egg-info 7 | build 8 | dist 9 | .coverage 10 | nosetests.xml 11 | lettucetests.xml 12 | coverage.xml 13 | ghostdriver.log 14 | .harvest 15 | pep8.report 16 | .env 17 | *.sqlite 18 | node_modules 19 | .vagrant 20 | /*.sqlite3 -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = patterns('', 5 | # Examples: 6 | # url(r'^$', 'example.views.home', name='home'), 7 | # url(r'^blog/', include('blog.urls')), 8 | 9 | url(r'^admin/', include(admin.site.urls)), 10 | ) 11 | -------------------------------------------------------------------------------- /example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /example/timestamp/tests.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | 3 | from .models import TimestampedBooleanDescriptor 4 | 5 | 6 | class SomeObject(object): 7 | # Let's add our descriptor on the `timestamp` field 8 | boolean = TimestampedBooleanDescriptor('timestamp') 9 | 10 | def __init__(self, timestamp=None): 11 | self.timestamp = timestamp 12 | 13 | 14 | obj = SomeObject() 15 | obj.timestamp 16 | obj.boolean 17 | 18 | obj.timestamp = timezone.now() 19 | obj.boolean 20 | 21 | obj.boolean = False 22 | obj.timestamp 23 | obj.boolean = True 24 | obj.timestamp 25 | -------------------------------------------------------------------------------- /Readme.markdown: -------------------------------------------------------------------------------- 1 | # Django Descriptors 2 | 3 | This sample project is the companion of a [blog 4 | post](http://kevinastone.github.io/django-model-descriptors.html) on patterns 5 | for building better models and fields using Descriptors. 6 | 7 | ## Setup 8 | 9 | You're encouraged to setup a `virtualenv` to work in prior to configuring the 10 | dependencies. 11 | 12 | 1. Install Python Requirements 13 | 14 | pip install -r dev-requirements.txt 15 | 16 | 2. Setup the Database 17 | 18 | ./manage.py syncdb 19 | 20 | 3. Load the models 21 | 22 | ./manage.py shell_plus 23 | -------------------------------------------------------------------------------- /example/computed/tests.py: -------------------------------------------------------------------------------- 1 | from .models import ProxyFieldDescriptor, URLFieldProxy 2 | 3 | 4 | class SomeObject(object): 5 | # Let's add our descriptor on the `url` field substituting `URLFieldProxy` 6 | wormhole = ProxyFieldDescriptor('url', URLFieldProxy) 7 | 8 | def __init__(self, url): 9 | self.url = url 10 | 11 | 12 | obj = SomeObject('http://example.com/asdf') 13 | 14 | # Normal attribute access still works 15 | obj.url 16 | 17 | # Does obj.url have a hostname property? 18 | try: 19 | obj.url.hostname 20 | except AttributeError: 21 | pass 22 | 23 | # What about accessing our descriptor field? 24 | obj.wormhole 25 | 26 | # Let's access the descriptor's property 27 | obj.wormhole.hostname 28 | 29 | # As you can see, the descriptor returns our proxy 30 | type(obj.wormhole) 31 | 32 | # But the proxy still *acts* like our original url attribute 33 | obj.wormhole == obj.url 34 | -------------------------------------------------------------------------------- /example/computed/models.py: -------------------------------------------------------------------------------- 1 | import urlparse 2 | from django.db import models 3 | 4 | 5 | class OriginalBookmark(models.Model): 6 | url = models.URLField() 7 | 8 | @property 9 | def hostname(self): 10 | return urlparse.urlparse(self.url).hostname 11 | 12 | 13 | class URLFieldProxy(unicode): 14 | @property 15 | def hostname(self): 16 | return urlparse.urlparse(self).hostname 17 | 18 | 19 | class ProxyFieldDescriptor(object): 20 | def __init__(self, field_name, proxy_class=None): 21 | self.field_name = field_name 22 | self.proxy_class = proxy_class 23 | 24 | def __get__(self, instance=None, owner=None): 25 | # grab the original value before we proxy 26 | value = instance.__dict__[self.field_name] 27 | if value is None: 28 | # We can't proxy a None through a unicode sub-class 29 | return value 30 | return self.proxy_class(value) 31 | 32 | def __set__(self, instance, value): 33 | instance.__dict__[self.field_name] = value 34 | 35 | 36 | class SomeObject(object): 37 | wormhole = ProxyFieldDescriptor('url', URLFieldProxy) 38 | 39 | def __init__(self, url): 40 | self.url = url 41 | 42 | 43 | class HostnamedURLField(models.URLField): 44 | def contribute_to_class(self, cls, name): 45 | super(HostnamedURLField, self).contribute_to_class(cls, name) 46 | # Add our descriptor to this field in place of of the normal attribute 47 | setattr(cls, self.name, ProxyFieldDescriptor(self.name, URLFieldProxy)) 48 | 49 | 50 | class Bookmark(models.Model): 51 | url = HostnamedURLField() 52 | -------------------------------------------------------------------------------- /example/timestamp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | 5 | class OriginalBlogPost(models.Model): 6 | content = models.TextField() 7 | published_at = models.DateTimeField(null=True, default=None) 8 | 9 | 10 | class TimestampedBooleanDescriptor(object): 11 | def __init__(self, name): 12 | self.name = name 13 | 14 | def __get__(self, instance=None, owner=None): 15 | return instance.__dict__[self.name] is not None 16 | 17 | def __set__(self, instance, value): 18 | value = bool(value) 19 | if value != self.__get__(instance): 20 | if value: 21 | instance.__dict__[self.name] = timezone.now() 22 | else: 23 | instance.__dict__[self.name] = None 24 | 25 | 26 | class TimestampedBooleanField(models.DateTimeField): 27 | """ 28 | A Boolean field that also captures the timestamp when the value was set. 29 | 30 | This field stores a timestamp in the database when set. It can be accessed 31 | as a boolean using the property argument (when not provided, it defaults to 32 | is_{field_name}). 33 | """ 34 | def __init__(self, *args, **kwargs): 35 | self.property_name = kwargs.pop('property', None) 36 | kwargs['null'] = True 37 | super(TimestampedBooleanField, self).__init__(*args, **kwargs) 38 | 39 | def contribute_to_class(self, cls, name): 40 | super(TimestampedBooleanField, self).contribute_to_class(cls, name) 41 | # Use the defined boolean property name or pick a default 42 | property_name = self.property_name or 'is_{0}'.format(name) 43 | setattr(cls, property_name, TimestampedBooleanDescriptor(self.name)) 44 | 45 | 46 | class BlogPost(models.Model): 47 | content = models.TextField() 48 | published_at = TimestampedBooleanField(property='is_published') 49 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = '3gis@i626ac5ogkg@j$%d1p(n4fr(#c88thg)d+grt6gfi5o^1' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'django_extensions', 40 | 'example.computed', 41 | 'example.timestamp', 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ) 53 | 54 | ROOT_URLCONF = 'example.urls' 55 | 56 | WSGI_APPLICATION = 'example.wsgi.application' 57 | 58 | 59 | # Database 60 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 61 | 62 | DATABASES = { 63 | 'default': { 64 | 'ENGINE': 'django.db.backends.sqlite3', 65 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 66 | } 67 | } 68 | 69 | # Internationalization 70 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 71 | 72 | LANGUAGE_CODE = 'en-us' 73 | 74 | TIME_ZONE = 'UTC' 75 | 76 | USE_I18N = True 77 | 78 | USE_L10N = True 79 | 80 | USE_TZ = True 81 | 82 | 83 | # Static files (CSS, JavaScript, Images) 84 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 85 | 86 | STATIC_URL = '/static/' 87 | --------------------------------------------------------------------------------