#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.db import models, transaction
from django.db.models.fields import FieldDoesNotExist

class InjectingModelBase(models.base.ModelBase):
    """This helper metaclass is used by PositionalSortMixIn.
    This metaclass injects the additional IntegerField position,
    which holds the information about the position of the item
    in the list."""

    def __new__(cls, name, bases, attrs):
        """Metaclass constructor calling Django and then modifying
        the resulting class"""
        # get the class which was alreeady built by Django
        child = models.base.ModelBase.__new__(cls, name, bases, attrs)

        # try to add some more or less neccessary fields
        try:
            # add the IntegerField
            position_field = models.IntegerField(editable=False, unique=True)
            try:
                # try to get the `position` field
                child._meta.get_field('position')
            except FieldDoesNotExist:
                # it was not found - create it now
                child.add_to_class('position', position_field)

        except AttributeError:
            # add_to_class was not yet added to the class.
            # No problem, this is called twice by Django, add_to_class 
            # will appear later
            pass
        # we're done - output the class, it's ready for use
        return child

class PositionalSortMixIn(object):
    """This mix-in class implements a user defined order in the database.
    To apply this mix-in you need to inherit from it before you inherit
    from `models.Model`. It adds an IntegerField called `position` to your 
    model. Be careful, it overwrites any existing field that you might have 
    defined. Additionally, this mix-in changes the default ordering
    behavior to order by the position field.
    
    Take care: your model needs to have a manager which returns all objects
    set as default manager, that is, the first defined manager. It does not
    need tp be named `objects`. Future versions of this Mixin may inject its
    own, private manager"""
    # get a metaclass which injects the neccessary fields
    __metaclass__ = InjectingModelBase

    def __init__(self, *args, **kwargs):
        """Initialize the class and set up some magic"""
        # call the parent - the ensure it behaves exactly like 
        # the original model
        models.Model.__init__(self, *args, **kwargs)

        # set position as the first field to order.
        # of course this gets overridden when using database queries which
        # request another ordering method (order_by)
        if 'position' not in self._meta.ordering:
            self._meta.ordering = ['position'] + list(self._meta.ordering)

    def get_object_at_offset(self, offset):
        """Get the object whose position is `offset` positions away 
        from it's own."""
        # get the class in which this was mixed in
        model_class = self.__class__
        try:
            return model_class._default_manager.get(position=self.position+offse
t)
        except model_class.DoesNotExist:
            # no such model? no deal, just return None
            return None

    # some shortcuts, convenience methods
    get_next = lambda self: self.get_object_at_offset(1)
    get_previous = lambda self: self.get_object_at_offset(-1)

    @transaction.commit_on_success
    def move_down(self):
        """Moves element one position down"""
        model_class = self.__class__
        # get the element after this one
        one_after = self.get_next()

        if not one_after:
            # already the last element
            return

        # flip the positions
        # a spare position field is needed
        final = model_class._default_manager.all().order_by('-position')[0].posi
tion + 1
        # move the object after this one to the bottom of the list
        one_after.position = final
        one_after.save()
        # move this object one down
        self.position += 1
        self.save()
        # move the element that was after this one before this one now
        one_after.position = self.position - 1
        one_after.save()

    @transaction.commit_on_success
    def move_up(self):
        """Moves element one position up"""
        model_class = self.__class__
        # get the element before this one
        one_before = self.get_previous()

        if not one_before:
            # already the first
            return

        # flip the positions:
        # first get a spare position field
        final = model_class._default_manager.all().order_by('-position')[0].posi
tion + 1
        # move the object that was before to the exact last position
        one_before.position = final
        one_before.save()
        # now, move this object one position up
        self.position -= 1
        self.save()
        # finally, move the object that was before to the position after this ob
ject
        one_before.position = self.position + 1
        one_before.save()

    @transaction.commit_on_success
    def insert_after(self, other):
        """Inserts an object in the database so that the objects will be ordered
 just
        behind the `other` object - this has to be of the same type, of course""
"
        # we only need to call another method and prepare the proper parameters
        self.insert_at(other.position + 1)

    @transaction.commit_on_success
    def insert_at(self, position):
        """Saves the object at a specified position.
        The `position` field """
        # get the class reference
        model_class = self.__class__
        # the position in what we want to put this in
        self.position = position
        # get all the objects which are at or after the currrent position
        # order them from back to front because of the unique `position` constra
int
        objects_after = model_class._default_manager.filter(position__gte=positi
on).order_by('-position')
        for element in objects_after:
            element.position += 1
            element.save()
        self.save()

    @transaction.commit_on_success
    def swap_position(self, other):
        """Swaps the position with some other class instance"""
        # save the current position
        current_position = self.position
        # set own position to special, temporary position
        self.position = -1
        self.save()
        self.position, other.position = other.position, current_position
        for obj in (other, self):
            obj.save()

    def save(self):
        """Saves the model to the database.
        It populates the `position` field of the model automatically if there 
        is no such field set. In this case, the element will be appended at
        the end of the list."""
        model_class = self.__class__
        # is there a position saved? (explicitly testing None because 0 would be
 false as well)
        if self.position == None:
            # no, it was empty. Find one
            try:
                # get the last object
                last = model_class._default_manager.all().order_by('-position')[
0]
                # the position of the last element
                self.position = last.position + 1
            except IndexError:
                # IndexError happened: the query did not return any objects
                # so this has to be the first
                self.position = 0

        # save the now properly set-up model
        return models.Model.save(self)

    def delete(self):
        """Deletes the item from the list."""
        model_class = self.__class__
        # get all objects with a position greater than this objects position
        objects_after = model_class._default_manager.filter(position__gt=self.po
sition)
        # now we remove this model instance
        # so the `position` is free and other instances can fill this gap
        models.Model.delete(self)

        # iterate through all objects which were found
        for element in objects_after:
            # decrease the position in the list (means: move forward)
            element.position -= 1
            element.save()