"""
Demonstration to use concepts in Django.

A concept defines a generic model but doesn't implement the model itself.
Concepts can be produced and consumed.
A producer isn't aware of anything, we set details at runtime.
A consumer isn't aware of the producer, but only knows the concept.
"""
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.importlib import import_module


def load_class(fqc):
    """
    Load a class by its fully qualified classname and return the class.
    """
    try:
        mod_name, klass_name = fqc.rsplit('.', 1)
        mod = import_module(mod_name)
    except ImportError, e:
        raise ImproperlyConfigured(('Error importing module {0}: "{1}"'.format(mod_name, e)))
    try:
        klass = getattr(mod, klass_name)
    except AttributeError:
        raise ImproperlyConfigured(('Module "{0}" does not define a "{1}" class'.format(mod_name, klass_name)))
    return klass


class Product(models.Model):
    """
    A normal model that will be a producer.

    >>> product = Product.objects.create(name="regular_test")
    >>> product.name
    'regular_test'
    """
    name = models.CharField(max_length=255)


class ProductConcept(object):
    """
    The Product concept.
    A Product has a field description.
    """
    mapping = {}

    @property
    def description(self):
        """
        Proxy the function call to the producer
        """
        return getattr(self, self.mapping['description'])

    @description.setter
    def description(self, value):
        """
        Proxy the set operation to the producer
        """
        setattr(self, self.mapping['description'], value)

    def __unicode__(self):
        return self.description


# Map the concepts to producers
CONCEPTS = [
    ('{0}.{1}'.format(Product.__module__, Product.__name__), {
        'description': 'name',
    }),
]

class Meta:
    proxy = True

# Set the producer at runtime
for producer, mapping in CONCEPTS:
    attrs = {
        '__metaclass__': Meta,
        '__module__': __name__,
        'mapping': mapping,
    }
    ProductConcept = type('ProductConcept', (load_class(producer), ProductConcept,), attrs)


class Test(object):
    """
    >>> obj = ProductConcept.objects.create(description="producer_test")
    >>> obj.description
    'producer_test'
    >>> ProductConcept.objects.filter(**{
    ...     ProductConcept.mapping['description']: 'producer_test',
    ... })
    [<ProductConcept: producer_test>]
    """


class Order(models.Model):
    """
    A normal model that consumes a concept in a foreign key.

    >>> product = ProductConcept.objects.create(description="consumer_test")
    >>> order = Order.objects.create(product=product)
    >>> order.product.description
    'consumer_test'
    """
    product = models.ForeignKey(ProductConcept)