Improvements on orders app

This commit is contained in:
Marc 2014-07-17 16:09:24 +00:00
parent d15d5dc249
commit 9c5af583dc
14 changed files with 375 additions and 46 deletions

View File

@ -50,7 +50,9 @@ class DomainInline(admin.TabularInline):
class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin): class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin):
fields = ('name', 'account') fields = ('name', 'account')
list_display = ('structured_name', 'is_top', 'websites', 'account_link') list_display = (
'structured_name', 'display_is_top', 'websites', 'account_link'
)
inlines = [RecordInline, DomainInline] inlines = [RecordInline, DomainInline]
list_filter = [TopDomainListFilter] list_filter = [TopDomainListFilter]
change_readonly_fields = ('name',) change_readonly_fields = ('name',)
@ -59,17 +61,17 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
form = DomainAdminForm form = DomainAdminForm
def structured_name(self, domain): def structured_name(self, domain):
if not self.is_top(domain): if not domain.is_top:
return ' '*4 + domain.name return ' '*4 + domain.name
return domain.name return domain.name
structured_name.short_description = _("name") structured_name.short_description = _("name")
structured_name.allow_tags = True structured_name.allow_tags = True
structured_name.admin_order_field = 'structured_name' structured_name.admin_order_field = 'structured_name'
def is_top(self, domain): def display_is_top(self, domain):
return not bool(domain.top) return domain.is_top
is_top.boolean = True display_is_top.boolean = True
is_top.admin_order_field = 'top' display_is_top.admin_order_field = 'top'
def websites(self, domain): def websites(self, domain):
if apps.isinstalled('orchestra.apps.websites'): if apps.isinstalled('orchestra.apps.websites'):

View File

@ -1,11 +1,11 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
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 services from orchestra.core import services
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address, from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address,
validate_hostname, validate_ascii) validate_hostname, validate_ascii)
from orchestra.utils.functional import cached
from . import settings, validators, utils from . import settings, validators, utils
@ -22,11 +22,14 @@ class Domain(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@property @cached_property
@cached
def origin(self): def origin(self):
return self.top or self return self.top or self
@cached_property
def is_top(self):
return not bool(self.top)
def get_records(self): def get_records(self):
""" proxy method, needed for input validation """ """ proxy method, needed for input validation """
return self.records.all() return self.records.all()

View File

@ -35,8 +35,10 @@ def execute(operations):
router = import_class(settings.ORCHESTRATION_ROUTER) router = import_class(settings.ORCHESTRATION_ROUTER)
# Generate scripts per server+backend # Generate scripts per server+backend
scripts = {} scripts = {}
cache = {}
for operation in operations: for operation in operations:
servers = router.get_servers(operation) servers = router.get_servers(operation, cache=cache)
print cache
for server in servers: for server in servers:
key = (server, operation.backend) key = (server, operation.backend)
if key not in scripts: if key not in scripts:

View File

@ -4,6 +4,7 @@ from threading import local
from django.db.models.signals import pre_delete, post_save from django.db.models.signals import pre_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.http.response import HttpResponseServerError from django.http.response import HttpResponseServerError
from orchestra.utils.python import OrderedSet from orchestra.utils.python import OrderedSet
from .backends import ServiceBackend from .backends import ServiceBackend
@ -12,12 +13,12 @@ from .models import BackendLog
from .models import BackendOperation as Operation from .models import BackendOperation as Operation
@receiver(post_save) @receiver(post_save, dispatch_uid='orchestration.post_save_collector')
def post_save_collector(sender, *args, **kwargs): def post_save_collector(sender, *args, **kwargs):
if sender != BackendLog: if sender != BackendLog:
OperationsMiddleware.collect(Operation.SAVE, **kwargs) OperationsMiddleware.collect(Operation.SAVE, **kwargs)
@receiver(pre_delete) @receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector')
def pre_delete_collector(sender, *args, **kwargs): def pre_delete_collector(sender, *args, **kwargs):
if sender != BackendLog: if sender != BackendLog:
OperationsMiddleware.collect(Operation.DELETE, **kwargs) OperationsMiddleware.collect(Operation.DELETE, **kwargs)

View File

@ -6,7 +6,6 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.models.fields import NullableCharField from orchestra.models.fields import NullableCharField
from orchestra.utils.apps import autodiscover from orchestra.utils.apps import autodiscover
from orchestra.utils.functional import cached
from . import settings, manager from . import settings, manager
from .backends import ServiceBackend from .backends import ServiceBackend
@ -56,8 +55,8 @@ class BackendLog(models.Model):
server = models.ForeignKey(Server, verbose_name=_("server"), server = models.ForeignKey(Server, verbose_name=_("server"),
related_name='execution_logs') related_name='execution_logs')
script = models.TextField(_("script")) script = models.TextField(_("script"))
stdout = models.TextField() stdout = models.TextField(_("stdout"))
stderr = models.TextField() stderr = models.TextField(_("stdin"))
traceback = models.TextField(_("traceback")) traceback = models.TextField(_("traceback"))
exit_code = models.IntegerField(_("exit code"), null=True) exit_code = models.IntegerField(_("exit code"), null=True)
task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True, task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True,
@ -149,35 +148,31 @@ class Route(models.Model):
# raise ValidationError(msg % (self.backend, self.method) # raise ValidationError(msg % (self.backend, self.method)
@classmethod @classmethod
@cached def get_servers(cls, operation, **kwargs):
def get_routing_table(cls): cache = kwargs.get('cache', {})
table = {}
for route in cls.objects.filter(is_active=True):
for action in route.backend_class().get_actions():
key = (route.backend, action)
try:
table[key].append(route)
except KeyError:
table[key] = [route]
return table
@classmethod
def get_servers(cls, operation):
table = cls.get_routing_table()
servers = [] servers = []
key = (operation.backend.get_name(), operation.action) backend = operation.backend
key = (backend.get_name(), operation.action)
try: try:
routes = table[key] routes = cache[key]
except KeyError: except KeyError:
return servers cache[key] = []
safe_locals = { for route in cls.objects.filter(is_active=True, backend=backend.get_name()):
'instance': operation.instance for action in backend.get_actions():
} _key = (route.backend, action)
cache[_key] = [route]
routes = cache[key]
for route in routes: for route in routes:
if eval(route.match, safe_locals): if route.matches(operation.instance):
servers.append(route.host) servers.append(route.host)
return servers return servers
def matches(self, instance):
safe_locals = {
'instance': instance
}
return eval(self.match, safe_locals)
def backend_class(self): def backend_class(self):
return ServiceBackend.get_backend(self.backend) return ServiceBackend.get_backend(self.backend)

View File

@ -0,0 +1,79 @@
from threading import local
from django.db.models.signals import pre_delete, pre_save
from django.dispatch import receiver
from django.http.response import HttpResponseServerError
from orchestra.core import services
from orchestra.utils.python import OrderedSet
from .models import Order
@receiver(pre_save, dispatch_uid='orders.ppre_save_collector')
def pre_save_collector(sender, *args, **kwargs):
if sender in services:
OrderMiddleware.collect(Order.SAVE, **kwargs)
@receiver(pre_delete, dispatch_uid='orders.pre_delete_collector')
def pre_delete_collector(sender, *args, **kwargs):
if sender in services:
OrderMiddleware.collect(Order.DELETE, **kwargs)
class OrderCandidate(object):
def __unicode__(self):
return "{}.{}()".format(str(self.instance), self.action)
def __init__(self, instance, action):
self.instance = instance
self.action = action
def __hash__(self):
""" set() """
opts = self.instance._meta
model = opts.app_label + opts.model_name
return hash(model + str(self.instance.pk) + self.action)
def __eq__(self, candidate):
""" set() """
return hash(self) == hash(candidate)
class OrderMiddleware(object):
"""
Stores all the operations derived from save and delete signals and executes them
at the end of the request/response cycle
"""
# Thread local is used because request object is not available on model signals
thread_locals = local()
@classmethod
def get_order_candidates(cls):
# Check if an error poped up before OrdersMiddleware.process_request()
if hasattr(cls.thread_locals, 'request'):
request = cls.thread_locals.request
if not hasattr(request, 'order_candidates'):
request.order_candidates = OrderedSet()
return request.order_candidates
return set()
@classmethod
def collect(cls, action, **kwargs):
""" Collects all pending operations derived from model signals """
request = getattr(cls.thread_locals, 'request', None)
if request is None:
return
order_candidates = cls.get_order_candidates()
instance = kwargs['instance']
order_candidates.add(OrderCandidate(instance, action))
def process_request(self, request):
""" Store request on a thread local variable """
type(self).thread_locals.request = request
def process_response(self, request, response):
if not isinstance(response, HttpResponseServerError):
candidates = type(self).get_order_candidates()
Order.process_candidates(candidates)
return response

View File

@ -143,8 +143,27 @@ class Service(models.Model):
def __unicode__(self): def __unicode__(self):
return self.description return self.description
@classmethod
def get_services(cls, instance, **kwargs):
cache = kwargs.get('cache', {})
ct = ContentType.objects.get_for_model(type(instance))
try:
return cache[ct]
except KeyError:
cache[ct] = cls.objects.filter(model=ct, is_active=True)
return cache[ct]
def matches(self, instance):
safe_locals = {
'instance': instance
}
return eval(self.match, safe_locals)
class Order(models.Model): class Order(models.Model):
SAVE = 'SAVE'
DELETE = 'DELETE'
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='orders') related_name='orders')
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType)
@ -161,7 +180,46 @@ class Order(models.Model):
content_object = generic.GenericForeignKey() content_object = generic.GenericForeignKey()
def __unicode__(self): def __unicode__(self):
return self.service return str(self.service)
def update(self):
instance = self.content_object
if self.service.metric:
metric = self.service.get_metric(instance)
self.store_metric(instance, metric)
description = "{}: {}".format(self.service.description, str(instance))
if self.description != description:
self.description = description
self.save()
@classmethod
def process_candidates(cls, candidates):
cache = {}
for candidate in candidates:
instance = candidate.instance
if candidate.action == cls.DELETE:
cls.objects.filter_for_object(instance).cancel()
else:
for service in Service.get_services(instance, cache=cache):
print cache
if not instance.pk:
if service.matches(instance):
order = cls.objects.create(content_object=instance,
account_id=instance.account_id, service=service)
order.update()
else:
ct = ContentType.objects.get_for_model(instance)
orders = cls.objects.filter(content_type=ct, service=service,
object_id=instance.pk)
if service.matches(instance):
if not orders:
order = cls.objects.create(content_object=instance,
service=service, account_id=instance.account_id)
else:
order = orders.get()
order.update()
elif orders:
orders.get().cancel()
class MetricStorage(models.Model): class MetricStorage(models.Model):

View File

@ -1,13 +1,16 @@
from django.contrib import admin from django.contrib import admin
from orchestra.admin.utils import insertattr from orchestra.admin.utils import insertattr
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.apps.orders.models import Service from orchestra.apps.orders.models import Service
from .models import Pack, Rate from .models import Pack, Rate
class PackAdmin(admin.ModelAdmin): class PackAdmin(AccountAdminMixin, admin.ModelAdmin):
pass list_display = ('name', 'account_link')
list_filter = ('name',)
admin.site.register(Pack, PackAdmin) admin.site.register(Pack, PackAdmin)

View File

@ -15,7 +15,7 @@ class Pack(models.Model):
default=settings.PRICES_DEFAULT_PACK) default=settings.PRICES_DEFAULT_PACK)
def __unicode__(self): def __unicode__(self):
return self.pack return self.name
class Rate(models.Model): class Rate(models.Model):

View File

@ -37,10 +37,10 @@ class ResourceAdmin(ExtendedModelAdmin):
def add_view(self, request, **kwargs): def add_view(self, request, **kwargs):
""" Warning user if the node is not fully configured """ """ Warning user if the node is not fully configured """
if request.method == 'GET': if request.method == 'POST':
messages.warning(request, _( messages.warning(request, _(
"Restarting orchestra and celery is required to fully apply changes. " "Restarting orchestra and celerybeat is required to fully apply changes. "
"Remember that allocated values will be applied when objects are saved" "Remember that new allocated values will be applied when objects are saved."
)) ))
return super(ResourceAdmin, self).add_view(request, **kwargs) return super(ResourceAdmin, self).add_view(request, **kwargs)

View File

@ -43,7 +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.apps.contacts.middlewares.ContractMiddleware', 'orchestra.apps.orders.middlewares.OrderMiddleware',
'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',
@ -178,7 +178,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
'contacts/contact': 'contact.png', 'contacts/contact': 'contact.png',
'orders/order': 'basket.png', 'orders/order': 'basket.png',
'orders/service': 'price.png', 'orders/service': 'price.png',
'prices/pack': 'pack.png', 'prices/pack': 'Dialog-accept.png',
# Administration # Administration
'users/user': 'Mr-potato.png', 'users/user': 'Mr-potato.png',
'djcelery/taskstate': 'taskstate.png', 'djcelery/taskstate': 'taskstate.png',

View File

@ -1,6 +1,9 @@
class Service(object): class Service(object):
_registry = {} _registry = {}
def __contains__(self, key):
return key in self._registry
def register(self, model, **kwargs): def register(self, model, **kwargs):
if model in self._registry: if model in self._registry:
raise KeyError("%s already registered" % str(model)) raise KeyError("%s already registered" % str(model))

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg1306"
height="48px"
width="48px"
version="1.1"
inkscape:version="0.48.3.1 r9886"
sodipodi:docname="Dialog-accept.svg"
inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/icons/Dialog-accept.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1024"
id="namedview25"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer4" />
<defs
id="defs1308">
<radialGradient
id="radialGradient3976"
gradientUnits="userSpaceOnUse"
cy="40"
cx="23.857"
gradientTransform="matrix(1,0,0,0.5,0,20)"
r="17.143">
<stop
id="stop4128"
offset="0" />
<stop
id="stop4130"
stop-opacity="0"
offset="1" />
</radialGradient>
<linearGradient
id="linearGradient3980"
y2="-8.5627"
gradientUnits="userSpaceOnUse"
x2="20.065"
y1="53.836"
x1="43.936">
<stop
id="stop2481"
stop-color="#ffe69b"
offset="0" />
<stop
id="stop2483"
stop-color="#fff"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3982"
y2="15.815"
gradientUnits="userSpaceOnUse"
x2="20.917"
gradientTransform="matrix(0.9176372,0,0,0.9176372,1.9768375,2.1031664)"
y1="33.955"
x1="21.994">
<stop
id="stop3959"
stop-color="#fffeff"
stop-opacity=".33333"
offset="0" />
<stop
id="stop3961"
stop-color="#fffeff"
stop-opacity=".21569"
offset="1" />
</linearGradient>
</defs>
<metadata
id="metadata1311">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Rodney Dawes</dc:title>
</cc:Agent>
</dc:creator>
<dc:contributor>
<cc:Agent>
<dc:title>Jakub Steiner, Garrett LeSage</dc:title>
</cc:Agent>
</dc:contributor>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" />
<dc:title></dc:title>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/2.0/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Attribution" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer2"
transform="matrix(0.91489252,0,0,0.91489252,2.0425808,2.0851578)">
<path
id="path6548"
style="opacity:0.6;color:#000000;fill:url(#radialGradient3976);display:block"
d="M 41,40 A 17.143,8.5714 0 1 1 6.714,40 17.143,8.5714 0 1 1 41,40 z"
transform="matrix(1.0706,0,0,0.525,-0.89276,22.5)"
display="block"
inkscape:connector-curvature="0" />
</g>
<g
id="layer1"
transform="matrix(0.91489252,0,0,0.91489252,2.0425808,2.0851578)">
<g
id="g4006">
<path
id="path1314"
d="M 46.857,23.929 C 46.857,36.829 36.4,47.286 23.5,47.286 10.6,47.286 0.143,36.829 0.143,23.929 0.143,11.029 10.6,0.572 23.5,0.572 c 12.9,0 23.357,10.457 23.357,23.357 z"
transform="matrix(0.92049,0,0,0.92049,2.3685,0.97408)"
inkscape:connector-curvature="0"
style="fill:#73d216;stroke:#4e9a06;stroke-width:1.08640003" />
<path
id="path3560"
d="m 49.902,26.635 c 0,13.25 -10.741,23.991 -23.991,23.991 -13.25,0 -23.991,-10.741 -23.991,-23.991 0,-13.25 10.741,-23.991 23.991,-23.991 13.25,0 23.991,10.741 23.991,23.991 z"
transform="matrix(0.85609,0,0,0.85609,1.8183,0.19777)"
style="opacity:0.34659005;fill-opacity:0;stroke:url(#linearGradient3980);stroke-width:1.1681"
inkscape:connector-curvature="0" />
</g>
</g>
<g
id="layer3"
transform="matrix(0.91489252,0,0,0.91489252,2.0425808,2.0851578)">
<g
id="text4967"
style="fill:#eeeeec;stroke:#eeeeec">
<path
id="path4984"
d="m 14.707,25.178 c 0.62921,1.5e-5 1.1052,0.5163 1.4278,1.5489 0.64534,1.9361 1.1052,2.9041 1.3794,2.9041 0.20973,1.2e-5 0.42753,-0.16133 0.65342,-0.48402 4.5336,-7.2602 8.7284,-13.133 12.584,-17.618 1.0003,-1.1616 2.5895,-1.7424 4.7676,-1.7425 0.51625,3.13e-5 0.86313,0.048433 1.0406,0.1452 0.17744,0.096834 0.26618,0.21784 0.26621,0.36301 -3.3e-5,0.2259 -0.26624,0.66959 -0.79863,1.331 -6.2277,7.4861 -12.004,15.392 -17.328,23.717 -0.37109,0.58082 -1.1294,0.87124 -2.2749,0.87123 -1.1617,5e-6 -1.8473,-0.0484 -2.0571,-0.1452 -0.54856,-0.242 -1.1939,-1.4762 -1.9361,-3.7027 -0.83897,-2.4685 -1.2585,-4.0173 -1.2584,-4.6466 -8e-6,-0.67761 0.56468,-1.331 1.6941,-1.9603 0.69375,-0.3872 1.3068,-0.5808 1.8393,-0.58082"
inkscape:connector-curvature="0" />
</g>
</g>
<g
id="layer4">
<path
id="path3955"
d="m 41.775448,22.029814 c 0,9.938478 -9.502074,-5.750098 -17.136852,0.354979 C 17.181308,28.347148 5.7378316,33.571184 5.7378316,23.632706 5.7381976,13.460932 13.717524,4.0256448 23.656002,4.0256448 33.59448,4.0254618 41.775448,12.091338 41.775448,22.029814 z"
style="fill:url(#linearGradient3982)"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB