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):
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]
list_filter = [TopDomainListFilter]
change_readonly_fields = ('name',)
@ -59,17 +61,17 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
form = DomainAdminForm
def structured_name(self, domain):
if not self.is_top(domain):
if not domain.is_top:
return ' '*4 + domain.name
return domain.name
structured_name.short_description = _("name")
structured_name.allow_tags = True
structured_name.admin_order_field = 'structured_name'
def is_top(self, domain):
return not bool(domain.top)
is_top.boolean = True
is_top.admin_order_field = 'top'
def display_is_top(self, domain):
return domain.is_top
display_is_top.boolean = True
display_is_top.admin_order_field = 'top'
def websites(self, domain):
if apps.isinstalled('orchestra.apps.websites'):

View file

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

View file

@ -35,8 +35,10 @@ def execute(operations):
router = import_class(settings.ORCHESTRATION_ROUTER)
# Generate scripts per server+backend
scripts = {}
cache = {}
for operation in operations:
servers = router.get_servers(operation)
servers = router.get_servers(operation, cache=cache)
print cache
for server in servers:
key = (server, operation.backend)
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.dispatch import receiver
from django.http.response import HttpResponseServerError
from orchestra.utils.python import OrderedSet
from .backends import ServiceBackend
@ -12,12 +13,12 @@ from .models import BackendLog
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):
if sender != BackendLog:
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
@receiver(pre_delete)
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector')
def pre_delete_collector(sender, *args, **kwargs):
if sender != BackendLog:
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.utils.apps import autodiscover
from orchestra.utils.functional import cached
from . import settings, manager
from .backends import ServiceBackend
@ -56,8 +55,8 @@ class BackendLog(models.Model):
server = models.ForeignKey(Server, verbose_name=_("server"),
related_name='execution_logs')
script = models.TextField(_("script"))
stdout = models.TextField()
stderr = models.TextField()
stdout = models.TextField(_("stdout"))
stderr = models.TextField(_("stdin"))
traceback = models.TextField(_("traceback"))
exit_code = models.IntegerField(_("exit code"), 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)
@classmethod
@cached
def get_routing_table(cls):
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()
def get_servers(cls, operation, **kwargs):
cache = kwargs.get('cache', {})
servers = []
key = (operation.backend.get_name(), operation.action)
backend = operation.backend
key = (backend.get_name(), operation.action)
try:
routes = table[key]
routes = cache[key]
except KeyError:
return servers
safe_locals = {
'instance': operation.instance
}
cache[key] = []
for route in cls.objects.filter(is_active=True, backend=backend.get_name()):
for action in backend.get_actions():
_key = (route.backend, action)
cache[_key] = [route]
routes = cache[key]
for route in routes:
if eval(route.match, safe_locals):
if route.matches(operation.instance):
servers.append(route.host)
return servers
def matches(self, instance):
safe_locals = {
'instance': instance
}
return eval(self.match, safe_locals)
def backend_class(self):
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):
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):
SAVE = 'SAVE'
DELETE = 'DELETE'
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='orders')
content_type = models.ForeignKey(ContentType)
@ -161,7 +180,46 @@ class Order(models.Model):
content_object = generic.GenericForeignKey()
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):

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.transaction.TransactionMiddleware',
# 'orchestra.apps.contacts.middlewares.ContractMiddleware',
'orchestra.apps.orders.middlewares.OrderMiddleware',
'orchestra.apps.orchestration.middlewares.OperationsMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
@ -178,7 +178,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
'contacts/contact': 'contact.png',
'orders/order': 'basket.png',
'orders/service': 'price.png',
'prices/pack': 'pack.png',
'prices/pack': 'Dialog-accept.png',
# Administration
'users/user': 'Mr-potato.png',
'djcelery/taskstate': 'taskstate.png',

View file

@ -1,6 +1,9 @@
class Service(object):
_registry = {}
def __contains__(self, key):
return key in self._registry
def register(self, model, **kwargs):
if model in self._registry:
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