Improvements on orders app
This commit is contained in:
parent
d15d5dc249
commit
9c5af583dc
|
@ -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'):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 |
|
@ -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 |
Loading…
Reference in New Issue