spicycms/spicy.core

View on GitHub
src/spicy/core/service/api.py

Summary

Maintainability
C
1 day
Test Coverage
import traceback
from django.conf import settings
from django.conf.urls.defaults import patterns, url
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ImproperlyConfigured
from itertools import chain
from spicy.core.service.utils import MethodDecoratorAdaptor
from spicy.core.siteskin.decorators import ViewInterface
from spicy.utils import cached_property, load_module, find_templates
from spicy.utils import get_custom_model_class, print_warning
from spicy.utils import print_error, print_text, print_success


GENERIC_CONSUMER = 'GENERIC_CONSUMER'
TEXT_INCLUDE_TEMPLATE = "[inc pk='%s' service='%s']"


class WrongServiceAPI(Exception):
    "Your service must inherit ServiceInterface."


class ServiceDoesNotExist(Exception):
    "Your service must inherit ServiceInterface."


class ProviderSchemaError(Exception):
    "Schema is wrong or not defined."


class ProviderMetaUrlError(Exception):
    pass


class MetaUrl:
    """
    Meta URL class used to define dynamic url pattern for class based Provider
    or service views.
    """
    is_public = False

    def __init__(self, pattern, method, name, is_public=False):
        self.pattern = pattern
        self.method = method
        self.name = name
        self.is_public = is_public

    def __iter__(self):
        return iter((self.pattern, self.method, self.name, self.is_public))

    def __eq__(self, other_url):
        return (
            self.pattern == other_url.pattern and
            self.name != other_url.name)

    def __repr__(self):
        return '<%s :: url[%s]>' % (self.name, self.pattern)


class ProviderMeta(type):
    """
    Meta Provider class

    class A(Provider):
        @render_to('template.html')
        def view(self, consumer_type, consumer_id)
             return dict()

        @ajax_request
        def view(self, consumer_type, consumer_id)
             return APIResponse()
    """

    # TODO make tests for this meta implementation.
    def __new__(mcs, name, bases, attrs):
        attrs['_meta_urls'] = urls = []
        for attr, inst in attrs.iteritems():
            render_interface = inst
            if isinstance(inst, MethodDecoratorAdaptor):
                render_interface = inst.func

            if isinstance(render_interface, ViewInterface):
                url_name = '-service-preview' if attr == '__call__' else (
                    '-' + attr)
                url_pattern_base = r'^%(service_name)s' + url_name
                if inst.url_pattern is None:
                    meta_url = MetaUrl(
                        url_pattern_base +
                        '/(?P<consumer_type>[\w]+)/(?P<consumer_id>[\d]+)/$',
                        inst, attr, is_public=render_interface.is_public)
                else:
                    meta_url = MetaUrl(
                        url_pattern_base + inst.url_pattern, inst, attr,
                        is_public=render_interface.is_public)
                if urls.count(meta_url):
                    raise ProviderMetaUrlError(
                        "You defined the same url pattern for the different "
                        "provider's views. %s, %s" % (meta_url, urls))
                else:
                    urls.append(meta_url)
        return super(ProviderMeta, mcs).__new__(mcs, name, bases, attrs)


class Provider(object):
    """
    Provide common views, api methods for defined web-application service.
    Service choose provider instance using own schema.

    provider = api.register['service_name'][ConsumerDjangoModel]

    :param model: ManyToMany model for consumer 'app.ModelName'
    :type str:
    """
    __metaclass__ = ProviderMeta
    service = None
    model = 'service.TestProviderModel'

    create_form_mod = None
    form_mod = None

    is_inline = True

    template = None
    create_template = None

    def __init__(self, service):
        self.service = service
        self.model = get_custom_model_class(self.model)

        self.urls = []
        for meta_url in self._meta_urls:
            pattern, method, name, is_public = meta_url
            if name == '__call__':
                name = service.name
            else:
                name = service.name + '-' + name

            if isinstance(method, MethodDecoratorAdaptor):
                method.func.set_instance(self)
            else:
                method.set_instance(self)
            self.urls.append((
                url(pattern % {'service_name': service.name}, method,
                    name=name),
                is_public))

    def get_or_create(self, consumer, **kwargs):
        """
        Checking if ``self.model`` instance is exists. Creating new instance if
        does not exists.
        And always return boolean flag is_created=True if new instance was
         created while executing.

        Return tuple: is_created, instance
        """
        print_warning(
            'TODO: Deprecated method api.'
            'Returning instace instead tuple of (is_created, instance)')

        instance = self.get_instance(consumer, **kwargs)
        kwargs.pop('_quiet', None)
        return (
            instance if instance is not None else
            self.create_instance(consumer, **kwargs))

    def create_instance(self, consumer, **kwargs):
        ctype = ContentType.objects.get_for_model(consumer)
        return self.model.objects.create(
            consumer_id=consumer.id, consumer_type=ctype, **kwargs)

    def get_instance(self, consumer, **kwargs):
        is_quiet = kwargs.pop('_quiet', False)
        try:
            if not isinstance(consumer, basestring):
                ctype = ContentType.objects.get_for_model(consumer)
                return self.model.objects.get(
                    consumer_type=ctype, consumer_id=consumer.id, **kwargs)

            elif consumer == GENERIC_CONSUMER:
                return self.model.objects.get(**kwargs)

        except self.model.MultipleObjectsReturned:
            # XXX, required for xtag service debugging.
            # dublications delete then, open consumer(document) for editing.
            if not is_quiet:
                if not isinstance(consumer, basestring):
                    ctype = ContentType.objects.get_for_model(consumer)
                    print '@Error: dublicates@:', consumer, kwargs, (
                        self.model.objects.filter(
                            consumer_type__model=ctype,
                            consumer_id=consumer.id, **kwargs))
                elif consumer == GENERIC_CONSUMER:
                    print '@Error: dublicates@:', consumer, kwargs, (
                        self.model.objects.filter(**kwargs))

        except self.model.DoesNotExist:
            if not is_quiet:
                print '@Error: instance not found:', consumer, kwargs

    def filter(self, **kwargs):
        return self.model.objects.filter(**kwargs)

    def get_instances(self, consumer=None, **kwargs):
        if consumer is not None:
            if isinstance(consumer, basestring):
                kwargs['consumer_type__model'] = consumer
            else:
                ctype = ContentType.objects.get_for_model(consumer)
                kwargs['consumer_type'] = ctype
                kwargs['consumer_id'] = consumer.id

        return self.filter(**kwargs)

    @cached_property
    def form(self):
        return load_module(self.form_mod)

    @cached_property
    def create_form(self):
        """Create provider form method

        :return form
        """
        if self.create_form_mod is not None:
            return load_module(self.create_form_mod)
        try:
            return load_module(self.form_mod)
        except AttributeError:
            raise ImproperlyConfigured(
                _("Setup form_mod attributes at first."))

    def inline_formset(self, request, consumer, prefix='provider'):
        raise NotImplementedError()

    def create(self, request):
        # XXX DEPRECATED
        return {'form': (self.create_form or self.form)(prefix='provider')}

    def create_inline_form(self, request, consumer, prefix='provider'):
        post_data = request.POST.copy()

        if request.method == 'POST':
            ctype = ContentType.objects.get_for_model(consumer)

            # XXX REFACTORING
            #post_data['%s-service' % prefix] = str(self.service.instance.id)
            post_data['%s-consumer_id' % prefix] = str(consumer.id)
            post_data['%s-consumer_type' % prefix] = str(ctype.id)

            if self.create_form is not None:
                return self.create_form(post_data, prefix=prefix)
            return self.form(post_data, prefix=prefix)

    def edit_inline_form(self, request, consumer, prefix='provider'):
        if request.method == 'POST':
            instance = self.get_instance(consumer)
            return self.form(request.POST, prefix=prefix, instance=instance)


class Interface(object):
    """
    Service interface

    Service register providers for different type of consumer using
    content_type schema.

    :param stype:
    :type: str
    :param name:
    :type: str
    :param label:
    :type: str

    :param template:
    :type: str
    :param is_default: ???
    :type: str
    """
    stype = None
    name = 'service'
    label = _('Service label')
    schema = dict(GENERIC_CONSUMER=Provider)

    template = None
    is_default = True

    def __init__(self):
        self.__providers = None

        if not isinstance(self.schema, dict):
            raise ProviderSchemaError(
                'Provider schema is not defined correctly.')

        self.is_default = self.is_default

        # initialize all providers
        self.__providers = dict(
            [(ctype, prv(self))
             for ctype, prv in self.schema.iteritems()])

    def print_schema(self):
        return '%s' % self.__providers

    def __getitem__(self, consumer):
        """
        Get provider instance for the defined content_type.

        :param consumer: consumer content_type string
        :type: str or model

        :return : concrete provider for defined consumer
        """
        if not isinstance(consumer, basestring):
            ctype = ContentType.objects.get_for_model(consumer)
            consumer = ctype.model

        if consumer in self.__providers:
            return self.__providers[consumer]
        elif GENERIC_CONSUMER in self.__providers:
            if settings.DEBUG:
                print_text('[{0}] Use GENERIC provider for: {1}'.format(
                    self.name, consumer))
            return self.__providers[GENERIC_CONSUMER]

        raise ProviderSchemaError(
            'Provider is not defined for the ContentType "%s"'
            ' service "%s", schema "%s"'

            % (consumer, self.label, self.schema))

    def create_provider_instance(self, consumer, **kwargs):
        return self[consumer].create_instance(consumer, **kwargs)

    def get_provider_instance(self, consumer, **kwargs):
        return self[consumer].get_instance(consumer, **kwargs)

    def get_provider_instances(self, consumer=None, **kwargs):
        if consumer is not None:
            return self[consumer].get_instances(consumer=consumer, **kwargs)
        elif GENERIC_CONSUMER in self.__providers:
            return self[GENERIC_CONSUMER].get_instances(**kwargs)

    def get_or_create_provider_instance(self, consumer, **kwargs):
        return self[consumer].get_or_create(consumer, **kwargs)

    def get_provider(self, consumer):
        print_error(
            'Deprecated method Sevice.get_provider(consumer). Use '
            'service[consumer] to get prvoder instance.'
            'For common provider methods use (get|create|get_or_create)'
            '_provider_instance|s call.'
            'Will be deleted in version Spicy-1.6')
        return self[consumer]

    def urls(self, is_public=False):
        return [
            [url for url, is_pub in prv.urls if is_pub == is_public]
            for prv in self.__providers.itervalues()]

    @cached_property
    def content_templates(self):
        return find_templates(self.PROVIDER_TEMPLATES_DIR)

    # TODO default documentation view for the service
    # register it like a request controller
    # service:admin:service_name
    # or use for public
    def __call__(self, request):
        """Default service view.

        dashboard ????

        Use it for something...
        """
        raise NotImplementedError()

    # TODO admin app
    def create_url(self):
        return

    def dashboard(self, request):
        return NotImplementedError()


class ServiceList(object):
    def __init__(self, services):
        self.services = set(services)

    def free(self):
        return set(filter(lambda srv: srv.is_free(), self.services))

    def prepaid(self):
        return self.services ^ self.free()

    def with_statistic(self):
        return set(
            [srv for srv in self.services if hasattr(srv, 'statistic_types')])

    def __iter__(self):
        return iter(self.services)


class Register(dict):
    _instance = None
    _is_loaded = False
    # TODO. singleton for dict

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Register, cls).__new__(cls, *args, **kwargs)
        return cls._instance

    def add(self, path_to_srv_interface):
        service = load_module(path_to_srv_interface)

        if not issubclass(service, Interface):
            raise WrongServiceAPI(
                'You must inherit services.Interface at first.')

        # Tested - works.
        if service.name in self:
            return

        try:
            service_instance = service()
            service_instance[
                'GENERIC_CONSUMER'].model.service = service_instance
            self[service.name] = service_instance
            if settings.DEBUG:
                print_success(
                    'Initialize [%s] service %s compatible with '
                    'consumer_types: %s' % (
                        service.name, service_instance,
                        service_instance.print_schema()))
        except Exception:
            print_error('Error while initialize service %s\n' % service.name)
            print_text(traceback.format_exc())

    def urls(self, is_public=False):
        # What follows below is not LISP :-P
        return patterns(
            '',
            *chain(*(chain(*(srv.urls(is_public=is_public)
                             for srv in self.get_list())))))

    def remove(self, service_name):
        try:
            del self[service_name]
            self.get_list()

        except KeyError:
            raise ServiceDoesNotExist(service_name)

    def get_list(self, consumer=None, stype=None):
        if consumer is not None:
            # TODO
            # filter services there specific consumer provider are defined
            pass

        return ServiceList(
            self.values() if stype is None else
            [srv for srv in self.itervalues() if srv.stype == stype])

    def __getitem__(self, name):
        if not self._is_loaded:
            self.load_services()
        try:
            return dict.__getitem__(self, name)
        except KeyError, e:
            raise ServiceDoesNotExist(name)

    def load_services(self):
        for service in settings.SERVICES:
            self.add(service)
        self._is_loaded = True
        return self


register = Register()