#### *WARNING* This is *extremely* slow.

#### EXAMPLE
#### person = Person.objects.all()[0]
#### person.lock()
#### person.lock(nowait=True)
#### person.unlock()

from django.db import models
from django.db import connection, transaction
from django.db import IntegrityError
from django.db import DatabaseError
import time
import logging
import random

""" Provides table locking functionality"""
class Lock(models.Model):
    pkid = models.IntegerField(editable=False, blank=False, null=False)
    table = models.CharField(max_length=31, editable=False, blank=False, null=False)
    ts = models.FloatField(default=time.time, editable=False, blank=False, null=False)
    seed = models.FloatField(default=random.random(), editable=False, blank=False, null=False)

    class Meta:
        unique_together = (
            ('pkid', 'table')
        )

        verbose_name = "Row object lock"
        verbose_name_plural = "Row object locks"

class LockObject():
    def __init__(self, id, seed):
        self.id = id
        self.seed = seed

    def unlock(self):
        # perform clean
        clean()

        # Create cursor
        cursor = connection.cursor()

        # Overwrite timeout
        c = cursor.execute("DELETE FROM de_lock WHERE id = %s AND seed = %s", [self.id, self.seed])
        transaction.commit_unless_managed()
        if c==0:
            raise DatabaseError, (9001, "Unable to unlock object")

def clean():
    # Create cursor
    cursor = connection.cursor()
    # delete timed out locks
    c = cursor.execute("DELETE FROM de_lock WHERE (%s-`ts`)>10"%time.time())
    # commit changes
    transaction.commit_unless_managed()


def lock(self, nowait=False):
    # perform clean
    clean()
    # Store our start time
    start = time.time()

    # Grab our PKID
    if not self.id:
        raise IntegrityError, (9004, "Object has no PKID, lock aborted")

    pkid = self.id

    # Loop forever until timeout is reached
    while True:
        try:
            # Call locking method
            return _lock(self, pkid)

        except IntegrityError, e:
            # Check if we are being told not to wait
            if nowait:
                raise

            # Calculate remaining time
            remaining = (time.time()-start)

            # Check if we have been trying to do this for more than 10 seconds
            if (time.time()-start)>10:
                raise IntegrityError, "Timeout after 10 seconds whilst attempting to lock PKID (Reason: %s)"%e

            logging.info("(%s remaining) Lock attempt failed: %s"%(remaining, e))

        # Sleep for 1 second in between attempts
        time.sleep(1)

def unlock(self):
    if not hasattr(self, "_lock_object"):
        raise Exception, "Object does not appear to be locked"
    self._lock_object.unlock()

def _lock(self, pkid):
    # Create cursor
    cursor = connection.cursor()
    table = self._meta.db_table

    try:
        # Generate a new seed
        seed = random.random()
        ts=time.time()

        # Create new lock entry
        try:
            # Attempt to create the new lock
            c = cursor.execute("INSERT INTO de_lock (`pkid`, `table`, `ts`, `seed`) VALUES (%s, %s, %s, %s)", [pkid, table, ts, seed])
            transaction.commit_unless_managed()
            # Ensure we found one
            if c==0:
                raise DatabaseError, (9002, "Unable to lock object")

            logging.info("Created new lock on %s for PKID %s"%(table,pkid))
            _id = cursor.lastrowid

            # Define the lock object
            self._lock_object = LockObject(_id, seed)

        except IntegrityError, e:
            # Check if the error is 1062 (duplicate key)
            if e[0]==1062:
                # Check if we have a lock entry
                c = cursor.execute("SELECT * from de_lock WHERE pkid = %s AND `table` = %s", [pkid, table]);

                # Ensure we found one
                if c==0:
                    raise DatabaseError, (9000, "Unable to lock object")

                # Extract values
                _id, _pkid, _table, _ts, _seed = cursor.fetchone()

                # Calculate seconds until timeout
                elapsed=(time.time()-_ts)
                remaining=10-elapsed

                # Check if timeout is above 10 seconds
                if elapsed>=10:
                    # Overwrite timeout
                    logging.info("Lock timed out on %s for PKID %s (%s seconds old)"%(table, pkid, elapsed))
                    c = cursor.execute("DELETE FROM de_lock WHERE id = %s AND seed = %s", [_id, _seed])
                    transaction.commit_unless_managed()
                    if c==0:
                        raise DatabaseError, (9005, "Unable to lock object")

                    cursor.execute("INSERT INTO de_lock (`pkid`, `table`, `ts`, `seed`) VALUES (%s, %s, %s, %s)", [pkid, table, ts, seed])
                    transaction.commit_unless_managed()
                    if c==0:
                        raise DatabaseError, (9006, "Unable to create new lock")

                    # Grab the last row id
                    _id = cursor.lastrowid

                    logging.info("Created new lock on %s for PKID %s"%(table,pkid))
                    return LockObject(_id, seed)

                else:
                    # Disallow lock.
                    logging.info("Lock already exists on %s for PKID %s (Timeout in %s)"%(table, pkid, remaining))
                    raise IntegrityError, (9000, "Object is already locked")

            # Some other exception code, so raise as normal.
            else:
                raise

    except:
        raise