Skip to content

Instantly share code, notes, and snippets.

@davidmarble
Created November 28, 2011 21:02
Show Gist options
  • Save davidmarble/1402045 to your computer and use it in GitHub Desktop.
Save davidmarble/1402045 to your computer and use it in GitHub Desktop.
Monkey patch django built-in models so that any fields can be overwritten or added
from django.db.models.fields import Field
import types
def patch_model(model_to_patch, class_to_patch_with):
"""
Adapted from http://www.ravelsoft.com/blog/2010/patchmodel-hacky-solution-extending-authuser-class
Monkey patch a django model with additional or replacement fields and methods.
All fields and methods that didn't exist previously are added.
Existing methods with the same name are replaced with
<methodname>__overridden. Existing fields with the same name
are deleted and replaced with the new field.
The class used to patch the model must be an old-style class.
Example:
from django.contrib.auth.models import User
from compatriot.utils import patch_model
class UserOverride:
email = models.EmailField(_('e-mail address'), unique=True)
new_field = models.CharField(_('new field'), max_length=10)
def save(self, *args, **kwargs):
# Custom save
# Call original save() method
self.save__overridden(*args, **kwargs)
# More custom save
patch_model(User, UserOverride)
@param model_to_patch: Class to patch
@param class_to_patch_with: Class with definitions of new fields and methods.
"""
# The _meta attribute is where the definition of the fields is stored in
# django model classes.
patched_meta = getattr(model_to_patch, '_meta')
for name, obj in class_to_patch_with.__dict__.items():
# If the attribute is a field, delete it if it already exists.
if isinstance(obj, Field):
index = -1
for field_table in (patched_meta.local_fields,
patched_meta.local_many_to_many):
for i in xrange (0, len(field_table)):
field = field_table[i]
if field.name == name:
index = i
# The creation_counter is used by django to know in
# which order the database columns are declared. We
# get it to ensure that when we override a field it
# will be declared in the same position as before.
creation_counter = field_table[i].creation_counter
break
if index != -1:
field_table.pop(index)
obj.creation_counter = creation_counter
break
# Add "__overridden" to method names if they already exist.
elif isinstance(obj, types.FunctionType) or isinstance(obj, property):
if getattr(model_to_patch, name, None):
setattr(model_to_patch, name + '__overridden', getattr(model_to_patch, name))
if isinstance(obj, types.FunctionType):
obj = types.UnboundMethodType(obj, None, model_to_patch)
# Add the new field/method name and object to the model.
model_to_patch.add_to_class(name, obj)
@davidmarble
Copy link
Author

You can use this to override / extend the built-in django user model like below. I've commented out the original user model fields for reference.

class UserOverride:
    """
    Use this old-style python class to override/extend 
    `django.contrib.auth.models.User`.
    """

    # ###############################################
    # Default User fields (copy and edit to override)
    # ###############################################
    # username = models.CharField(_('username'), max_length=30, unique=True, 
        # help_text=_("Required. 30 characters or fewer. Letters, numbers "
        # "and ./+/-/_ characters"))
    username = models.CharField(_('username'), max_length=30, unique=True, 
        help_text=_("Required. 30 characters or fewer. Letters and numbers "
        "only."))
    # first_name = models.CharField(_('first name'), max_length=30, blank=True)
    # last_name = models.CharField(_('last name'), max_length=30, blank=True)
    # email = models.EmailField(_('e-mail address'), blank=True, unique=True)
    email = models.EmailField(_('e-mail address'), unique=True)
    # password = models.CharField(_('password'), max_length=128, 
        # help_text=_("Use '[algo]$[salt]$[hexdigest]' or use the "
        # "<a href=\"password/\">change password form</a>."))
    # is_staff = models.BooleanField(_('staff status'), default=False, 
        # help_text=_("Designates whether the user can log into this admin "
        # "site."))
    # is_active = models.BooleanField(_('active'), default=True, 
        # help_text=_("Designates whether this user should be treated as "
        # "active. Unselect this instead of deleting accounts."))
    # is_superuser = models.BooleanField(_('superuser status'), default=False, 
        # help_text=_("Designates that this user has all permissions without "
        # "explicitly assigning them."))
    # last_login = models.DateTimeField(_('last login'), default=datetime.datetime.now)
    # date_joined = models.DateTimeField(_('date joined'), default=datetime.datetime.now)
    # groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True,
        # help_text=_("In addition to the permissions manually assigned, this "
        # "user will also get all permissions granted to each group he/she is in."))
    # user_permissions = models.ManyToManyField(Permission, 
        # verbose_name=_('user permissions'), blank=True)

    # ######################
    # Additional User fields
    # ######################

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment