Added support for service handlers
This commit is contained in:
parent
f4732c9f8e
commit
c731a73889
|
@ -61,7 +61,7 @@ class AccountAdmin(ExtendedModelAdmin):
|
||||||
messages.warning(request, 'This account is disabled.')
|
messages.warning(request, 'This account is disabled.')
|
||||||
context = {
|
context = {
|
||||||
'services': sorted(
|
'services': sorted(
|
||||||
[ model._meta for model in services.get().keys() ],
|
[ model._meta for model in services.get() ],
|
||||||
key=lambda i: i.verbose_name_plural.lower()
|
key=lambda i: i.verbose_name_plural.lower()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ from orchestra.utils import plugins
|
||||||
from . import methods
|
from . import methods
|
||||||
|
|
||||||
|
|
||||||
class ServiceBackend(object):
|
class ServiceBackend(plugins.Plugin):
|
||||||
"""
|
"""
|
||||||
Service management backend base class
|
Service management backend base class
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ class ServiceBackend(object):
|
||||||
be conviniently supported. Each backend generates the configuration for all
|
be conviniently supported. Each backend generates the configuration for all
|
||||||
the changes of all modified objects, reloading the daemon just once.
|
the changes of all modified objects, reloading the daemon just once.
|
||||||
"""
|
"""
|
||||||
verbose_name = None
|
|
||||||
model = None
|
model = None
|
||||||
related_models = () # ((model, accessor__attribute),)
|
related_models = () # ((model, accessor__attribute),)
|
||||||
script_method = methods.BashSSH
|
script_method = methods.BashSSH
|
||||||
|
@ -65,28 +64,11 @@ class ServiceBackend(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backends(cls):
|
def get_backends(cls):
|
||||||
return cls.plugins
|
return cls.get_plugins()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backend(cls, name):
|
def get_backend(cls, name):
|
||||||
for backend in ServiceBackend.get_backends():
|
return cls.get_plugin(name)
|
||||||
if backend.get_name() == name:
|
|
||||||
return backend
|
|
||||||
raise KeyError('This backend is not registered')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_choices(cls):
|
|
||||||
backends = cls.get_backends()
|
|
||||||
choices = []
|
|
||||||
for b in backends:
|
|
||||||
# don't evaluate b.verbose_name ugettext_lazy
|
|
||||||
verbose = getattr(b.verbose_name, '_proxy____args', [b.verbose_name])
|
|
||||||
if verbose[0]:
|
|
||||||
verbose = b.verbose_name
|
|
||||||
else:
|
|
||||||
verbose = b.get_name()
|
|
||||||
choices.append((b.get_name(), verbose))
|
|
||||||
return sorted(choices, key=lambda e: e[0])
|
|
||||||
|
|
||||||
def get_banner(self):
|
def get_banner(self):
|
||||||
time = timezone.now().strftime("%h %d, %Y %I:%M:%S")
|
time = timezone.now().strftime("%h %d, %Y %I:%M:%S")
|
||||||
|
|
|
@ -125,7 +125,7 @@ class Route(models.Model):
|
||||||
Defines the routing that determine in which server a backend is executed
|
Defines the routing that determine in which server a backend is executed
|
||||||
"""
|
"""
|
||||||
backend = models.CharField(_("backend"), max_length=256,
|
backend = models.CharField(_("backend"), max_length=256,
|
||||||
choices=ServiceBackend.get_choices())
|
choices=ServiceBackend.get_plugin_choices())
|
||||||
host = models.ForeignKey(Server, verbose_name=_("host"))
|
host = models.ForeignKey(Server, verbose_name=_("host"))
|
||||||
match = models.CharField(_("match"), max_length=256, blank=True, default='True',
|
match = models.CharField(_("match"), max_length=256, blank=True, default='True',
|
||||||
help_text=_("Python expression used for selecting the targe host, "
|
help_text=_("Python expression used for selecting the targe host, "
|
||||||
|
|
|
@ -12,7 +12,7 @@ class ServiceAdmin(admin.ModelAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('description', 'model', 'match', 'is_active')
|
'fields': ('description', 'content_type', 'match', 'handler', 'is_active')
|
||||||
}),
|
}),
|
||||||
(_("Billing options"), {
|
(_("Billing options"), {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
|
@ -22,16 +22,17 @@ class ServiceAdmin(admin.ModelAdmin):
|
||||||
(_("Pricing options"), {
|
(_("Pricing options"), {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('metric', 'pricing_period', 'rate_algorithm',
|
'fields': ('metric', 'pricing_period', 'rate_algorithm',
|
||||||
'orders_effect', ('on_cancel', 'on_disable', 'on_register'),
|
'orders_effect', 'on_cancel', 'payment_style',
|
||||||
'payment_style', 'trial_period', 'refound_period', 'tax',)
|
'trial_period', 'refound_period', 'tax')
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Improve performance of account field and filter by account """
|
""" Improve performance of account field and filter by account """
|
||||||
if db_field.name == 'model':
|
if db_field.name == 'content_type':
|
||||||
models = [model._meta.model_name for model in services.get().keys()]
|
models = [model._meta.model_name for model in services.get()]
|
||||||
kwargs['queryset'] = db_field.rel.to.objects.filter(model__in=models)
|
queryset = db_field.rel.to.objects
|
||||||
|
kwargs['queryset'] = queryset.filter(model__in=models)
|
||||||
if db_field.name in ['match', 'metric']:
|
if db_field.name in ['match', 'metric']:
|
||||||
kwargs['widget'] = forms.TextInput(attrs={'size':'160'})
|
kwargs['widget'] = forms.TextInput(attrs={'size':'160'})
|
||||||
return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
@ -43,7 +44,7 @@ class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class MetricStorageAdmin(admin.ModelAdmin):
|
class MetricStorageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('order', 'value', 'date')
|
list_display = ('order', 'value', 'created_on', 'updated_on')
|
||||||
list_filter = ('order__service',)
|
list_filter = ('order__service',)
|
||||||
|
|
||||||
|
|
||||||
|
|
39
orchestra/apps/orders/handlers.py
Normal file
39
orchestra/apps/orders/handlers.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from orchestra.utils import plugins
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceHandler(plugins.Plugin):
|
||||||
|
model = None
|
||||||
|
|
||||||
|
__metaclass__ = plugins.PluginMount
|
||||||
|
|
||||||
|
def __init__(self, service):
|
||||||
|
self.service = service
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_plugin_choices(cls):
|
||||||
|
choices = super(ServiceHandler, cls).get_plugin_choices()
|
||||||
|
return [('', _("Default"))] + choices
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.service, attr)
|
||||||
|
|
||||||
|
def matches(self, instance):
|
||||||
|
safe_locals = {
|
||||||
|
instance._meta.model_name: instance
|
||||||
|
}
|
||||||
|
return eval(self.match, safe_locals)
|
||||||
|
|
||||||
|
def get_metric(self, instance):
|
||||||
|
safe_locals = {
|
||||||
|
instance._meta.model_name: instance
|
||||||
|
}
|
||||||
|
return eval(self.metric, safe_locals)
|
||||||
|
|
||||||
|
def get_content_type(self):
|
||||||
|
if not self.model:
|
||||||
|
return self.content_type
|
||||||
|
app_label, model = self.model.split('.')
|
||||||
|
return ContentType.objects.get_by_natural_key(app_label, model.lower())
|
|
@ -5,13 +5,22 @@ from django.dispatch import receiver
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
from django.contrib.contenttypes import generic
|
from django.contrib.contenttypes import generic
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.validators import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from orchestra.core import caches
|
||||||
|
from orchestra.utils.apps import autodiscover
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
from .handlers import ServiceHandler
|
||||||
from .helpers import search_for_related
|
from .helpers import search_for_related
|
||||||
|
|
||||||
|
|
||||||
|
autodiscover('handlers')
|
||||||
|
|
||||||
|
|
||||||
class Service(models.Model):
|
class Service(models.Model):
|
||||||
NEVER = 'NEVER'
|
NEVER = 'NEVER'
|
||||||
MONTHLY = 'MONTHLY'
|
MONTHLY = 'MONTHLY'
|
||||||
|
@ -34,10 +43,12 @@ class Service(models.Model):
|
||||||
MATCH_PRICE = 'MATCH_PRICE'
|
MATCH_PRICE = 'MATCH_PRICE'
|
||||||
|
|
||||||
description = models.CharField(_("description"), max_length=256, unique=True)
|
description = models.CharField(_("description"), max_length=256, unique=True)
|
||||||
model = models.ForeignKey(ContentType, verbose_name=_("model"))
|
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
|
||||||
match = models.CharField(_("match"), max_length=256)
|
match = models.CharField(_("match"), max_length=256, blank=True)
|
||||||
|
handler = models.CharField(_("handler"), max_length=256, blank=True,
|
||||||
|
help_text=_("Handler used to process this Service."),
|
||||||
|
choices=ServiceHandler.get_plugin_choices())
|
||||||
is_active = models.BooleanField(_("is active"), default=True)
|
is_active = models.BooleanField(_("is active"), default=True)
|
||||||
# TODO class based Service definition (like ServiceBackend)
|
|
||||||
# Billing
|
# Billing
|
||||||
billing_period = models.CharField(_("billing period"), max_length=16,
|
billing_period = models.CharField(_("billing period"), max_length=16,
|
||||||
help_text=_("Renewal period for recurring invoicing"),
|
help_text=_("Renewal period for recurring invoicing"),
|
||||||
|
@ -105,21 +116,22 @@ class Service(models.Model):
|
||||||
(REFOUND, _("Refound")),
|
(REFOUND, _("Refound")),
|
||||||
),
|
),
|
||||||
default=DISCOUNT)
|
default=DISCOUNT)
|
||||||
on_disable = models.CharField(_("on disable"), max_length=16,
|
# TODO remove, orders are not disabled (they are cancelled user.is_active)
|
||||||
help_text=_("Defines the behaviour of this service when disabled"),
|
# on_disable = models.CharField(_("on disable"), max_length=16,
|
||||||
choices=(
|
# help_text=_("Defines the behaviour of this service when disabled"),
|
||||||
(NOTHING, _("Nothing")),
|
# choices=(
|
||||||
(DISCOUNT, _("Discount")),
|
# (NOTHING, _("Nothing")),
|
||||||
(REFOUND, _("Refound")),
|
# (DISCOUNT, _("Discount")),
|
||||||
),
|
# (REFOUND, _("Refound")),
|
||||||
default=DISCOUNT)
|
# ),
|
||||||
on_register = models.CharField(_("on register"), max_length=16,
|
# default=DISCOUNT)
|
||||||
help_text=_("Defines the behaviour of this service on registration"),
|
# on_register = models.CharField(_("on register"), max_length=16,
|
||||||
choices=(
|
# help_text=_("Defines the behaviour of this service on registration"),
|
||||||
(NOTHING, _("Nothing")),
|
# choices=(
|
||||||
(DISCOUNT, _("Discount (fixed BP)")),
|
# (NOTHING, _("Nothing")),
|
||||||
),
|
# (DISCOUNT, _("Discount (fixed BP)")),
|
||||||
default=DISCOUNT)
|
# ),
|
||||||
|
# default=DISCOUNT)
|
||||||
payment_style = models.CharField(_("payment style"), max_length=16,
|
payment_style = models.CharField(_("payment style"), max_length=16,
|
||||||
help_text=_("Designates whether this service should be paid after "
|
help_text=_("Designates whether this service should be paid after "
|
||||||
"consumtion (postpay/on demand) or prepaid"),
|
"consumtion (postpay/on demand) or prepaid"),
|
||||||
|
@ -151,27 +163,38 @@ class Service(models.Model):
|
||||||
return self.description
|
return self.description
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_services(cls, instance, **kwargs):
|
def get_services(cls, instance):
|
||||||
# TODO get per-request cache from thread local
|
cache = caches.get_request_cache()
|
||||||
cache = kwargs.get('cache', {})
|
|
||||||
ct = ContentType.objects.get_for_model(instance)
|
ct = ContentType.objects.get_for_model(instance)
|
||||||
|
services = cache.get(ct)
|
||||||
|
if services is None:
|
||||||
|
services = cls.objects.filter(content_type=ct, is_active=True)
|
||||||
|
cache.set(ct, services)
|
||||||
|
return services
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def proxy(self):
|
||||||
|
if self.handler:
|
||||||
|
return ServiceHandler.get_plugin(self.handler)(self)
|
||||||
|
return ServiceHandler(self)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
content_type = self.proxy.get_content_type()
|
||||||
|
if self.content_type != content_type:
|
||||||
|
msg =_("Content type must be equal to '%s'." % str(content_type))
|
||||||
|
raise ValidationError(msg)
|
||||||
|
if not self.match:
|
||||||
|
msg =_("Match should be provided")
|
||||||
|
raise ValidationError(msg)
|
||||||
try:
|
try:
|
||||||
return cache[ct]
|
obj = content_type.model_class().objects.all()[0]
|
||||||
except KeyError:
|
except IndexError:
|
||||||
cache[ct] = cls.objects.filter(model=ct, is_active=True)
|
pass
|
||||||
return cache[ct]
|
else:
|
||||||
|
try:
|
||||||
def matches(self, instance):
|
self.proxy.matches(obj)
|
||||||
safe_locals = {
|
except Exception as e:
|
||||||
instance._meta.model_name: instance
|
raise ValidationError(_(str(e)))
|
||||||
}
|
|
||||||
return eval(self.match, safe_locals)
|
|
||||||
|
|
||||||
def get_metric(self, instance):
|
|
||||||
safe_locals = {
|
|
||||||
instance._meta.model_name: instance
|
|
||||||
}
|
|
||||||
return eval(self.metric, safe_locals)
|
|
||||||
|
|
||||||
|
|
||||||
class OrderQuerySet(models.QuerySet):
|
class OrderQuerySet(models.QuerySet):
|
||||||
|
@ -242,10 +265,11 @@ class Order(models.Model):
|
||||||
class MetricStorage(models.Model):
|
class MetricStorage(models.Model):
|
||||||
order = models.ForeignKey(Order, verbose_name=_("order"))
|
order = models.ForeignKey(Order, verbose_name=_("order"))
|
||||||
value = models.BigIntegerField(_("value"))
|
value = models.BigIntegerField(_("value"))
|
||||||
date = models.DateTimeField(_("date"), auto_now_add=True)
|
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
||||||
|
updated_on = models.DateTimeField(_("updated on"), auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = 'date'
|
get_latest_by = 'created_on'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return unicode(self.order)
|
return unicode(self.order)
|
||||||
|
@ -259,11 +283,14 @@ class MetricStorage(models.Model):
|
||||||
else:
|
else:
|
||||||
if metric.value != value:
|
if metric.value != value:
|
||||||
cls.objects.create(order=order, value=value)
|
cls.objects.create(order=order, value=value)
|
||||||
|
else:
|
||||||
|
metric.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
|
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
|
||||||
def cancel_orders(sender, **kwargs):
|
def cancel_orders(sender, **kwargs):
|
||||||
if not sender in [MetricStorage, LogEntry, Order, Service]:
|
if (not sender in [MetricStorage, LogEntry, Order, Service] and
|
||||||
|
not Service in sender.__mro__):
|
||||||
instance = kwargs['instance']
|
instance = kwargs['instance']
|
||||||
for order in Order.objects.by_object(instance).active():
|
for order in Order.objects.by_object(instance).active():
|
||||||
order.cancel()
|
order.cancel()
|
||||||
|
@ -272,8 +299,10 @@ def cancel_orders(sender, **kwargs):
|
||||||
@receiver(post_save, dispatch_uid="orders.update_orders")
|
@receiver(post_save, dispatch_uid="orders.update_orders")
|
||||||
@receiver(post_delete, dispatch_uid="orders.update_orders")
|
@receiver(post_delete, dispatch_uid="orders.update_orders")
|
||||||
def update_orders(sender, **kwargs):
|
def update_orders(sender, **kwargs):
|
||||||
if not sender in [MetricStorage, LogEntry, Order, Service]:
|
if (not sender in [MetricStorage, LogEntry, Order, Service] and
|
||||||
|
not Service in sender.__mro__):
|
||||||
instance = kwargs['instance']
|
instance = kwargs['instance']
|
||||||
|
print kwargs
|
||||||
if instance.pk:
|
if instance.pk:
|
||||||
# post_save
|
# post_save
|
||||||
Order.update_orders(instance)
|
Order.update_orders(instance)
|
||||||
|
|
|
@ -59,7 +59,7 @@ class ResourceAdmin(ExtendedModelAdmin):
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" filter service content_types """
|
""" filter service content_types """
|
||||||
if db_field.name == 'content_type':
|
if db_field.name == 'content_type':
|
||||||
models = [ model._meta.model_name for model in services.get().keys() ]
|
models = [ model._meta.model_name for model in services.get() ]
|
||||||
kwargs['queryset'] = db_field.rel.to.objects.filter(model__in=models)
|
kwargs['queryset'] = db_field.rel.to.objects.filter(model__in=models)
|
||||||
return super(ResourceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(ResourceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ class ServiceMonitor(ServiceBackend):
|
||||||
def content_type(self):
|
def content_type(self):
|
||||||
app_label, model = self.model.split('.')
|
app_label, model = self.model.split('.')
|
||||||
model = model.lower()
|
model = model.lower()
|
||||||
return ContentType.objects.get(app_label=app_label, model=model)
|
return ContentType.objects.get_by_natural_key(app_label, model)
|
||||||
|
|
||||||
def get_last_data(self, object_id):
|
def get_last_data(self, object_id):
|
||||||
from .models import MonitorData
|
from .models import MonitorData
|
||||||
|
@ -56,7 +56,7 @@ class ServiceMonitor(ServiceBackend):
|
||||||
from .models import MonitorData
|
from .models import MonitorData
|
||||||
name = self.get_name()
|
name = self.get_name()
|
||||||
app_label, model_name = self.model.split('.')
|
app_label, model_name = self.model.split('.')
|
||||||
ct = ContentType.objects.get(app_label=app_label, model=model_name.lower())
|
ct = ContentType.objects.get_by_natural_key(app_label, model_name.lower())
|
||||||
for line in log.stdout.splitlines():
|
for line in log.stdout.splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
object_id, value = self.process(line)
|
object_id, value = self.process(line)
|
||||||
|
|
|
@ -58,7 +58,7 @@ class Resource(models.Model):
|
||||||
help_text=_("Crontab for periodic execution. "
|
help_text=_("Crontab for periodic execution. "
|
||||||
"Leave it empty to disable periodic monitoring"))
|
"Leave it empty to disable periodic monitoring"))
|
||||||
monitors = MultiSelectField(_("monitors"), max_length=256, blank=True,
|
monitors = MultiSelectField(_("monitors"), max_length=256, blank=True,
|
||||||
choices=ServiceMonitor.get_choices(),
|
choices=ServiceMonitor.get_plugin_choices(),
|
||||||
help_text=_("Monitor backends used for monitoring this resource."))
|
help_text=_("Monitor backends used for monitoring this resource."))
|
||||||
is_active = models.BooleanField(_("is active"), default=True)
|
is_active = models.BooleanField(_("is active"), default=True)
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ class ResourceData(models.Model):
|
||||||
class MonitorData(models.Model):
|
class MonitorData(models.Model):
|
||||||
""" Stores monitored data """
|
""" Stores monitored data """
|
||||||
monitor = models.CharField(_("monitor"), max_length=256,
|
monitor = models.CharField(_("monitor"), max_length=256,
|
||||||
choices=ServiceMonitor.get_choices())
|
choices=ServiceMonitor.get_plugin_choices())
|
||||||
content_type = models.ForeignKey(ContentType)
|
content_type = models.ForeignKey(ContentType)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
date = models.DateTimeField(_("date"), auto_now_add=True)
|
date = models.DateTimeField(_("date"), auto_now_add=True)
|
||||||
|
|
|
@ -43,6 +43,7 @@ MIDDLEWARE_CLASSES = (
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.transaction.TransactionMiddleware',
|
'django.middleware.transaction.TransactionMiddleware',
|
||||||
|
'orchestra.core.cache.RequestCacheMiddleware',
|
||||||
'orchestra.apps.orchestration.middlewares.OperationsMiddleware',
|
'orchestra.apps.orchestration.middlewares.OperationsMiddleware',
|
||||||
# Uncomment the next line for simple clickjacking protection:
|
# Uncomment the next line for simple clickjacking protection:
|
||||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
|
39
orchestra/core/caches.py
Normal file
39
orchestra/core/caches.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from threading import currentThread
|
||||||
|
|
||||||
|
from django.core.cache.backends.locmem import LocMemCache
|
||||||
|
|
||||||
|
|
||||||
|
_request_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
class RequestCache(LocMemCache):
|
||||||
|
""" LocMemCache is a threadsafe local memory cache """
|
||||||
|
def __init__(self):
|
||||||
|
name = 'locmemcache@%i' % hash(currentThread())
|
||||||
|
super(RequestCache, self).__init__(name, {})
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_cache():
|
||||||
|
"""
|
||||||
|
Returns per-request cache when running RequestCacheMiddleware otherwise a
|
||||||
|
new LocMemCache instance (when running periodic tasks or shell)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return _request_cache[currentThread()]
|
||||||
|
except KeyError:
|
||||||
|
cache = RequestCache()
|
||||||
|
_request_cache[currentThread()] = cache
|
||||||
|
return cache
|
||||||
|
|
||||||
|
|
||||||
|
class RequestCacheMiddleware(object):
|
||||||
|
def process_request(self, request):
|
||||||
|
cache = _request_cache.get(currentThread(), RequestCache())
|
||||||
|
_request_cache[currentThread()] = cache
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
def process_response(self, request, response):
|
||||||
|
# TODO not sure if this actually saves memory, remove otherwise
|
||||||
|
if currentThread() in _request_cache:
|
||||||
|
_request_cache[currentThread()].clear()
|
||||||
|
return response
|
|
@ -2,8 +2,15 @@ def cached(func):
|
||||||
""" caches func return value """
|
""" caches func return value """
|
||||||
def cached_func(self, *args, **kwargs):
|
def cached_func(self, *args, **kwargs):
|
||||||
attr = '_cached_' + func.__name__
|
attr = '_cached_' + func.__name__
|
||||||
if not hasattr(self, attr):
|
key = (args, tuple(kwargs.items()))
|
||||||
setattr(self, attr, func(self, *args, **kwargs))
|
try:
|
||||||
return getattr(self, attr)
|
return getattr(self, attr)[key]
|
||||||
|
except KeyError:
|
||||||
|
value = func(self, *args, **kwargs)
|
||||||
|
getattr(self, attr)[key] = value
|
||||||
|
except AttributeError:
|
||||||
|
value = func(self, *args, **kwargs)
|
||||||
|
setattr(self, attr, {key: value})
|
||||||
|
return value
|
||||||
return cached_func
|
return cached_func
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,40 @@
|
||||||
|
from .functional import cached
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(object):
|
||||||
|
verbose_name = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_plugin_name(cls):
|
||||||
|
return cls.__name__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_plugins(cls):
|
||||||
|
return cls.plugins
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@cached
|
||||||
|
def get_plugin(cls, name):
|
||||||
|
for plugin in cls.get_plugins():
|
||||||
|
if plugin.get_plugin_name() == name:
|
||||||
|
return plugin
|
||||||
|
raise KeyError('This plugin is not registered')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_plugin_choices(cls):
|
||||||
|
plugins = cls.get_plugins()
|
||||||
|
choices = []
|
||||||
|
for p in plugins:
|
||||||
|
# don't evaluate p.verbose_name ugettext_lazy
|
||||||
|
verbose = getattr(p.verbose_name, '_proxy____args', [p.verbose_name])
|
||||||
|
if verbose[0]:
|
||||||
|
verbose = p.verbose_name
|
||||||
|
else:
|
||||||
|
verbose = p.get_plugin_name()
|
||||||
|
choices.append((p.get_plugin_name(), verbose))
|
||||||
|
return sorted(choices, key=lambda e: e[0])
|
||||||
|
|
||||||
|
|
||||||
class PluginMount(type):
|
class PluginMount(type):
|
||||||
def __init__(cls, name, bases, attrs):
|
def __init__(cls, name, bases, attrs):
|
||||||
if not hasattr(cls, 'plugins'):
|
if not hasattr(cls, 'plugins'):
|
||||||
|
|
Loading…
Reference in a new issue