From d15d5dc24984210fd8196f02cc28735446f0f8c2 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 16 Jul 2014 15:20:16 +0000 Subject: [PATCH] Improved resources and orders apps --- orchestra/admin/menu.py | 8 +- orchestra/apps/accounts/settings.py | 1 + orchestra/apps/databases/backends.py | 31 +- orchestra/apps/miscellaneous/__init__.py | 0 orchestra/apps/miscellaneous/admin.py | 34 + orchestra/apps/miscellaneous/models.py | 36 + orchestra/apps/orchestration/backends.py | 4 +- orchestra/apps/orders/admin.py | 40 +- orchestra/apps/orders/models.py | 144 ++- orchestra/apps/orders/settings.py | 8 +- orchestra/apps/prices/admin.py | 25 +- orchestra/apps/prices/models.py | 25 +- orchestra/apps/prices/settings.py | 9 - orchestra/apps/resources/admin.py | 37 +- orchestra/apps/resources/backends.py | 10 +- orchestra/apps/resources/forms.py | 4 +- orchestra/apps/resources/helpers.py | 4 +- orchestra/apps/resources/models.py | 28 +- orchestra/apps/vps/backends.py | 2 +- orchestra/conf/base_settings.py | 8 +- .../orchestra/icons/Applications-office.svg | 614 ++++++++++++ .../orchestra/icons/Misc-Misc-Box-icon.png | Bin 0 -> 3795 bytes .../orchestra/icons/Misc-Misc-Box-icon.svg | 894 ++++++++++++++++++ .../orchestra/icons/Package-x-generic.png | Bin 0 -> 2221 bytes .../orchestra/icons/Package-x-generic.svg | 418 ++++++++ 25 files changed, 2305 insertions(+), 79 deletions(-) create mode 100644 orchestra/apps/miscellaneous/__init__.py create mode 100644 orchestra/apps/miscellaneous/admin.py create mode 100644 orchestra/apps/miscellaneous/models.py create mode 100644 orchestra/static/orchestra/icons/Applications-office.svg create mode 100644 orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png create mode 100644 orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg create mode 100644 orchestra/static/orchestra/icons/Package-x-generic.png create mode 100644 orchestra/static/orchestra/icons/Package-x-generic.svg diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 4d95731b..04b7a7a5 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -53,11 +53,13 @@ def get_accounts(): users.append(items.MenuItem(_("Tokens"), tokens)) accounts.append(items.MenuItem(_("Users"), url, children=users)) if isinstalled('orchestra.apps.prices'): - url = reverse('admin:prices_price_changelist') - accounts.append(items.MenuItem(_("Prices"), url)) + url = reverse('admin:prices_pack_changelist') + accounts.append(items.MenuItem(_("Packs"), url)) if isinstalled('orchestra.apps.orders'): url = reverse('admin:orders_order_changelist') accounts.append(items.MenuItem(_("Orders"), url)) + url = reverse('admin:orders_service_changelist') + accounts.append(items.MenuItem(_("Services"), url)) return accounts @@ -76,6 +78,8 @@ def get_administration_models(): administration_models.append('orchestra.apps.issues.*') if isinstalled('orchestra.apps.resources'): administration_models.append('orchestra.apps.resources.*') + if isinstalled('orchestra.apps.miscellaneous'): + administration_models.append('orchestra.apps.miscellaneous.models.MiscService') return administration_models diff --git a/orchestra/apps/accounts/settings.py b/orchestra/apps/accounts/settings.py index 5d82ed2e..a6d2d95b 100644 --- a/orchestra/apps/accounts/settings.py +++ b/orchestra/apps/accounts/settings.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ ACCOUNTS_TYPES = getattr(settings, 'ACCOUNTS_TYPES', ( ('INDIVIDUAL', _("Individual")), ('ASSOCIATION', _("Association")), + ('CUSTOMER', _("Customer")), ('COMPANY', _("Company")), ('PUBLICBODY', _("Public body")), )) diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index 1511d62b..5606e657 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -74,5 +74,34 @@ class MySQLPermissionBackend(ServiceController): class MysqlDisk(ServiceMonitor): model = 'database.Database' - resource = ServiceMonitor.DISK verbose_name = _("MySQL disk") + + def exceeded(self, db): + context = self.get_context(obj) + self.append("mysql -e '" + "UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\"" + " WHERE Db=\"%(db_name)s\";'" % context + ) + + def recovery(self, db): + context = self.get_context(obj) + self.append("mysql -e '" + "UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\"" + " WHERE Db=\"%(db_name)s\";'" % context + ) + + def monitor(self, db): + context = self.get_context(obj) + self.append( + "echo %(db_id)s $(mysql -B -e '" + " SELECT sum( data_length + index_length ) \"Size\"\n" + " FROM information_schema.TABLES\n" + " WHERE table_schema=\"gisp\"\n" + " GROUP BY table_schema;' | tail -n 1)" % context + ) + + def get_context(self, db): + return { + 'db_name': db.name, + 'db_id': db.pk, + } diff --git a/orchestra/apps/miscellaneous/__init__.py b/orchestra/apps/miscellaneous/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py new file mode 100644 index 00000000..d8f9eccd --- /dev/null +++ b/orchestra/apps/miscellaneous/admin.py @@ -0,0 +1,34 @@ +from django.contrib import admin +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.accounts.admin import AccountAdminMixin + +from .models import MiscService, Miscellaneous + + +class MiscServiceAdmin(admin.ModelAdmin): + list_display = ('name', 'num_instances') + + def num_instances(self, misc): + """ return num slivers as a link to slivers changelist view """ + num = misc.instances.count() + url = reverse('admin:miscellaneous_miscellaneous_changelist') + url += '?service={}'.format(misc.pk) + return mark_safe('{1}'.format(url, num)) + num_instances.short_description = _("Instances") + num_instances.admin_order_field = 'instances__count' + + def get_queryset(self, request): + qs = super(MiscServiceAdmin, self).queryset(request) + return qs.annotate(models.Count('instances', distinct=True)) + + +class MiscellaneousAdmin(AccountAdminMixin, admin.ModelAdmin): + list_display = ('service', 'amount', 'account_link') + + +admin.site.register(MiscService, MiscServiceAdmin) +admin.site.register(Miscellaneous, MiscellaneousAdmin) diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py new file mode 100644 index 00000000..6bb4c83d --- /dev/null +++ b/orchestra/apps/miscellaneous/models.py @@ -0,0 +1,36 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orchestra.core import services + + +class MiscService(models.Model): + name = models.CharField(_("name"), max_length=256) + description = models.TextField(blank=True) + is_active = models.BooleanField(default=True, + help_text=_("Whether new instances of this service can be created " + "or not. Unselect this instead of deleting services.")) + + def __unicode__(self): + return self.name + + +class Miscellaneous(models.Model): + service = models.ForeignKey(MiscService, verbose_name=_("service"), + related_name='instances') + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='miscellaneous') + description = models.TextField(_("description"), blank=True) + amount = models.PositiveIntegerField(_("amount"), default=1) + is_active = models.BooleanField(default=True, + help_text=_("Designates whether this service should be treated as " + "active. Unselect this instead of deleting services.")) + + class Meta: + verbose_name_plural = _("miscellaneous") + + def __unicode__(self): + return "{0}-{1}".format(str(self.service), str(self.account)) + + +services.register(Miscellaneous) diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index b1f6de6a..a3626b7d 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -130,4 +130,6 @@ class ServiceController(ServiceBackend): @classmethod def get_backends(cls): """ filter controller classes """ - return [ plugin for plugin in cls.plugins if ServiceController in plugin.__mro__ ] + return [ + plugin for plugin in cls.plugins if ServiceController in plugin.__mro__ + ] diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py index 54f9ec06..99f83e3f 100644 --- a/orchestra/apps/orders/admin.py +++ b/orchestra/apps/orders/admin.py @@ -1,15 +1,49 @@ +from django import forms from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ -from .models import Order, QuotaStorage +from orchestra.core import services + +from .models import Service, Order, MetricStorage + + +class ServiceAdmin(admin.ModelAdmin): + fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('description', 'model', 'match', 'is_active') + }), + (_("Billing options"), { + 'classes': ('wide',), + 'fields': ('billing_period', 'billing_point', 'delayed_billing', + 'is_fee') + }), + (_("Pricing options"), { + 'classes': ('wide',), + 'fields': ('metric', 'pricing_period', 'rate_algorithm', + 'orders_effect', ('on_cancel', 'on_disable', 'on_register'), + 'payment_style', 'trial_period', 'refound_period', 'tax',) + }), + ) + + def formfield_for_dbfield(self, db_field, **kwargs): + """ Improve performance of account field and filter by account """ + if db_field.name == 'model': + models = [model._meta.model_name for model in services.get().keys()] + kwargs['queryset'] = db_field.rel.to.objects.filter(model__in=models) + if db_field.name in ['match', 'metric']: + kwargs['widget'] = forms.TextInput(attrs={'size':'160'}) + return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs) class OrderAdmin(admin.ModelAdmin): pass -class QuotaStorageAdmin(admin.ModelAdmin): +class MetricStorageAdmin(admin.ModelAdmin): pass +admin.site.register(Service, ServiceAdmin) admin.site.register(Order, OrderAdmin) -admin.site.register(QuotaStorage, QuotaStorageAdmin) +admin.site.register(MetricStorage, MetricStorageAdmin) diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 780a352c..bdd7d9e3 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -6,13 +6,151 @@ from django.utils.translation import ugettext_lazy as _ from . import settings +class Service(models.Model): + NEVER = 'NEVER' + MONTHLY = 'MONTHLY' + ANUAL = 'ANUAL' + TEN_DAYS = 'TEN_DAYS' + ONE_MONTH = 'ONE_MONTH' + ALWAYS = 'ALWAYS' + ON_REGISTER = 'ON_REGISTER' + FIXED_DATE = 'ON_FIXED_DATE' + BILLING_PERIOD = 'BILLING_PERIOD' + REGISTER_OR_RENEW = 'REGISTER_OR_RENEW' + CONCURRENT = 'CONCURRENT' + NOTHING = 'NOTHING' + DISCOUNT = 'DISCOUNT' + REFOUND = 'REFOUND' + PREPAY = 'PREPAY' + POSTPAY = 'POSTPAY' + BEST_PRICE = 'BEST_PRICE' + PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE' + MATCH_PRICE = 'MATCH_PRICE' + + description = models.CharField(_("description"), max_length=256, unique=True) + model = models.ForeignKey(ContentType, verbose_name=_("model")) + match = models.CharField(_("match"), max_length=256) + is_active = models.BooleanField(_("is active"), default=True) + # Billing + billing_period = models.CharField(_("billing period"), max_length=16, + help_text=_("Renewal period for recurring invoicing"), + choices=( + (NEVER, _("One time service")), + (MONTHLY, _("Monthly billing")), + (ANUAL, _("Anual billing")), + ), + default=ANUAL) + billing_point = models.CharField(_("billing point"), max_length=16, + help_text=_("Reference point for calculating the renewal date " + "on recurring invoices"), + choices=( + (ON_REGISTER, _("Registration date")), + (FIXED_DATE, _("Fixed billing date")), + ), + default=FIXED_DATE) + delayed_billing = models.CharField(_("delayed billing"), max_length=16, + help_text=_("Period in which this service will be ignored for billing"), + choices=( + (NEVER, _("No delay (inmediate billing)")), + (TEN_DAYS, _("Ten days")), + (ONE_MONTH, _("One month")), + ), + default=ONE_MONTH) + is_fee = models.BooleanField(_("is fee"), default=False, + help_text=_("Designates whether this service should be billed as " + " membership fee or not")) + # Pricing + metric = models.CharField(_("metric"), max_length=256, blank=True, + help_text=_("Metric used to compute the pricing rate. " + "Number of orders is used when left blank.")) + tax = models.IntegerField(_("tax"), choices=settings.ORDERS_SERVICE_TAXES, + default=settings.ORDERS_SERVICE_DEFAUL_TAX) + pricing_period = models.CharField(_("pricing period"), max_length=16, + help_text=_("Period used for calculating the metric used on the " + "pricing rate"), + choices=( + (BILLING_PERIOD, _("Same as billing period")), + (MONTHLY, _("Monthly data")), + (ANUAL, _("Anual data")), + ), + default=BILLING_PERIOD) + rate_algorithm = models.CharField(_("rate algorithm"), max_length=16, + help_text=_("Algorithm used to interprete the rating table"), + choices=( + (BEST_PRICE, _("Best price")), + (PROGRESSIVE_PRICE, _("Progressive price")), + (MATCH_PRICE, _("Match price")), + ), + default=BEST_PRICE) + orders_effect = models.CharField(_("orders effect"), max_length=16, + help_text=_("Defines the lookup behaviour when using orders for " + "the pricing rate computation of this service."), + choices=( + (REGISTER_OR_RENEW, _("Register or renew events")), + (CONCURRENT, _("Active at every given time")), + ), + default=CONCURRENT) + on_cancel = models.CharField(_("on cancel"), max_length=16, + help_text=_("Defines the cancellation behaviour of this service"), + choices=( + (NOTHING, _("Nothing")), + (DISCOUNT, _("Discount")), + (REFOUND, _("Refound")), + ), + default=DISCOUNT) + on_disable = models.CharField(_("on disable"), max_length=16, + help_text=_("Defines the behaviour of this service when disabled"), + choices=( + (NOTHING, _("Nothing")), + (DISCOUNT, _("Discount")), + (REFOUND, _("Refound")), + ), + default=DISCOUNT) + on_register = models.CharField(_("on register"), max_length=16, + help_text=_("Defines the behaviour of this service on registration"), + choices=( + (NOTHING, _("Nothing")), + (DISCOUNT, _("Discount (fixed BP)")), + ), + default=DISCOUNT) + payment_style = models.CharField(_("payment style"), max_length=16, + help_text=_("Designates whether this service should be paid after " + "consumtion (postpay/on demand) or prepaid"), + choices=( + (PREPAY, _("Prepay")), + (POSTPAY, _("Postpay (on demand)")), + ), + default=PREPAY) + trial_period = models.CharField(_("trial period"), max_length=16, + help_text=_("Period in which no charge will be issued"), + choices=( + (NEVER, _("No trial")), + (TEN_DAYS, _("Ten days")), + (ONE_MONTH, _("One month")), + ), + default=NEVER) + refound_period = models.CharField(_("refound period"), max_length=16, + help_text=_("Period in which automatic refound will be performed on " + "service cancellation"), + choices=( + (NEVER, _("Never refound")), + (TEN_DAYS, _("Ten days")), + (ONE_MONTH, _("One month")), + (ALWAYS, _("Always refound")), + ), + default=ONE_MONTH) + + def __unicode__(self): + return self.description + + class Order(models.Model): account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='orders') content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField(null=True) - price = models.ForeignKey(settings.ORDERS_PRICE_MODEL, - verbose_name=_("price"), related_name='orders') + service = models.ForeignKey(Service, verbose_name=_("price"), + related_name='orders') registered_on = models.DateTimeField(_("registered on"), auto_now_add=True) cancelled_on = models.DateTimeField(_("cancelled on"), null=True, blank=True) billed_on = models.DateTimeField(_("billed on"), null=True, blank=True) @@ -26,7 +164,7 @@ class Order(models.Model): return self.service -class QuotaStorage(models.Model): +class MetricStorage(models.Model): order = models.ForeignKey(Order, verbose_name=_("order")) value = models.BigIntegerField(_("value")) date = models.DateTimeField(_("date")) diff --git a/orchestra/apps/orders/settings.py b/orchestra/apps/orders/settings.py index d8de9e98..e8b41f7e 100644 --- a/orchestra/apps/orders/settings.py +++ b/orchestra/apps/orders/settings.py @@ -2,4 +2,10 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ -ORDERS_PRICE_MODEL = getattr(settings, 'ORDERS_PRICE_MODEL', 'prices.Price') +ORDERS_SERVICE_TAXES = getattr(settings, 'ORDERS_SERVICE_TAXES', ( + (0, _("Duty free")), + (7, _("7%")), + (21, _("21%")), +)) + +ORDERS_SERVICE_DEFAUL_TAX = getattr(settings, 'ORDERS_SERVICE_DFAULT_TAX', 0) diff --git a/orchestra/apps/prices/admin.py b/orchestra/apps/prices/admin.py index 3a507e43..8a6749db 100644 --- a/orchestra/apps/prices/admin.py +++ b/orchestra/apps/prices/admin.py @@ -1,23 +1,20 @@ from django.contrib import admin -from orchestra.core import services +from orchestra.admin.utils import insertattr +from orchestra.apps.orders.models import Service -from .models import Pack, Price, Rate +from .models import Pack, Rate + + +class PackAdmin(admin.ModelAdmin): + pass + +admin.site.register(Pack, PackAdmin) class RateInline(admin.TabularInline): model = Rate + ordering = ('pack', 'quantity') -class PriceAdmin(admin.ModelAdmin): - inlines = [RateInline] - - def formfield_for_dbfield(self, db_field, **kwargs): - """ Improve performance of account field and filter by account """ - if db_field.name == 'service': - models = [model._meta.model_name for model in services.get().keys()] - kwargs['queryset'] = db_field.rel.to.objects.filter(model__in=models) - return super(PriceAdmin, self).formfield_for_dbfield(db_field, **kwargs) - - -admin.site.register(Price, PriceAdmin) +insertattr(Service, 'inlines', RateInline) diff --git a/orchestra/apps/prices/models.py b/orchestra/apps/prices/models.py index 7e550a2a..c43021a4 100644 --- a/orchestra/apps/prices/models.py +++ b/orchestra/apps/prices/models.py @@ -2,6 +2,8 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ +from orchestra.core import services + from . import settings @@ -16,27 +18,18 @@ class Pack(models.Model): return self.pack -class Price(models.Model): - description = models.CharField(_("description"), max_length=256, unique=True) - service = models.ForeignKey(ContentType, verbose_name=_("service")) - expression = models.CharField(_("match"), max_length=256) - tax = models.IntegerField(_("tax"), choices=settings.PRICES_TAXES, - default=settings.PRICES_DEFAUL_TAX) - active = models.BooleanField(_("is active"), default=True) - - def __unicode__(self): - return self.description - - class Rate(models.Model): - price = models.ForeignKey('prices.Price', verbose_name=_("price")) + service = models.ForeignKey('orders.Service', verbose_name=_("service")) pack = models.CharField(_("pack"), max_length=128, blank=True, choices=(('', _("default")),) + settings.PRICES_PACKS) quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True) - value = models.DecimalField(_("price"), max_digits=12, decimal_places=2) + value = models.DecimalField(_("value"), max_digits=12, decimal_places=2) class Meta: - unique_together = ('price', 'pack', 'quantity') + unique_together = ('service', 'pack', 'quantity') def __unicode__(self): - return self.price + return "{}-{}".format(str(self.value), self.quantity) + + +services.register(Pack, menu=False) diff --git a/orchestra/apps/prices/settings.py b/orchestra/apps/prices/settings.py index 5ef9920a..885657b5 100644 --- a/orchestra/apps/prices/settings.py +++ b/orchestra/apps/prices/settings.py @@ -8,12 +8,3 @@ PRICES_PACKS = getattr(settings, 'PRICES_PACKS', ( )) PRICES_DEFAULT_PACK = getattr(settings, 'PRICES_DEFAULT_PACK', 'basic') - - -PRICES_TAXES = getattr(settings, 'PRICES_TAXES', ( - (0, _("Duty free")), - (7, _("7%")), - (21, _("21%")), -)) - -PRICES_DEFAUL_TAX = getattr(settings, 'PRICES_DFAULT_TAX', 0) diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index ea4aa9b8..bb0956c9 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -7,7 +7,7 @@ from djcelery.humanize import naturaldate from orchestra.admin import ExtendedModelAdmin from orchestra.admin.filters import UsedContentTypeFilter -from orchestra.admin.utils import insertattr, get_modeladmin +from orchestra.admin.utils import insertattr, get_modeladmin, link from orchestra.core import services from orchestra.utils import running_syncdb @@ -17,7 +17,7 @@ from .models import Resource, ResourceData, MonitorData class ResourceAdmin(ExtendedModelAdmin): list_display = ( - 'name', 'verbose_name', 'content_type', 'period', 'ondemand', + 'id', 'name', 'verbose_name', 'content_type', 'period', 'ondemand', 'default_allocation', 'disable_trigger', 'crontab', ) list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger') @@ -26,8 +26,8 @@ class ResourceAdmin(ExtendedModelAdmin): 'fields': ('name', 'content_type', 'period'), }), (_("Configuration"), { - 'fields': ('verbose_name', 'default_allocation', 'ondemand', - 'disable_trigger', 'is_active'), + 'fields': ('verbose_name', 'unit', 'scale', 'ondemand', + 'default_allocation', 'disable_trigger', 'is_active'), }), (_("Monitoring"), { 'fields': ('monitors', 'crontab'), @@ -65,16 +65,27 @@ class ResourceAdmin(ExtendedModelAdmin): class ResourceDataAdmin(admin.ModelAdmin): - list_display = ('id', 'resource', 'used', 'allocated', 'last_update', 'content_type') # TODO content_object + list_display = ( + 'id', 'resource', 'used', 'allocated', 'last_update', 'content_object_link' + ) list_filter = ('resource',) + readonly_fields = ('content_object_link',) + + def content_object_link(self, data): + return link('content_object')(self, data) + content_object_link.allow_tags = True + content_object_link.short_description = _("Content object") class MonitorDataAdmin(admin.ModelAdmin): - list_display = ('id', 'monitor', 'date', 'value', 'ct', 'object_id') # TODO content_object + list_display = ('id', 'monitor', 'date', 'value', 'content_object_link') list_filter = ('monitor',) + readonly_fields = ('content_object_link',) - def ct(self, i): - return i.content_type_id + def content_object_link(self, data): + return link('content_object')(self, data) + content_object_link.allow_tags = True + content_object_link.short_description = _("Content object") admin.site.register(Resource, ResourceAdmin) @@ -102,8 +113,10 @@ def resource_inline_factory(resources): form = ResourceForm formset = ResourceInlineFormSet can_delete = False - fields = ('verbose_name', 'used', 'display_last_update', 'allocated',) - readonly_fields = ('used', 'display_last_update',) + fields = ( + 'verbose_name', 'used', 'display_last_update', 'allocated', 'unit' + ) + readonly_fields = ('used', 'display_last_update') class Media: css = { @@ -114,9 +127,9 @@ def resource_inline_factory(resources): """ Hidde add another """ return False - def display_last_update(self, log): + def display_last_update(self, data): return '
{1}
'.format( - escape(str(log.last_update)), escape(naturaldate(log.last_update)), + escape(str(data.last_update)), escape(naturaldate(data.last_update)), ) display_last_update.short_description = _("last update") display_last_update.allow_tags = True diff --git a/orchestra/apps/resources/backends.py b/orchestra/apps/resources/backends.py index 686289dd..9d52210e 100644 --- a/orchestra/apps/resources/backends.py +++ b/orchestra/apps/resources/backends.py @@ -14,7 +14,7 @@ class ServiceMonitor(ServiceBackend): CPU = 'cpu' # TODO UNITS - actions = ('monitor', 'resource_exceeded', 'resource_recovery') + actions = ('monitor', 'exceeded', 'recovery') @classmethod def get_backends(cls): @@ -47,15 +47,19 @@ class ServiceMonitor(ServiceBackend): return self.current_date - datetime.timedelta(days=1) return data.date + def process(self, line): + """ line -> object_id, value """ + return line.split() + def store(self, log): - """ object_id value """ + """ stores montirod values from stdout """ from .models import MonitorData name = self.get_name() app_label, model_name = self.model.split('.') ct = ContentType.objects.get(app_label=app_label, model=model_name.lower()) for line in log.stdout.splitlines(): line = line.strip() - object_id, value = line.split() + object_id, value = self.process(line) MonitorData.objects.create(monitor=name, object_id=object_id, content_type=ct, value=value, date=self.current_date) diff --git a/orchestra/apps/resources/forms.py b/orchestra/apps/resources/forms.py index 44cf9adf..05145808 100644 --- a/orchestra/apps/resources/forms.py +++ b/orchestra/apps/resources/forms.py @@ -12,15 +12,17 @@ class ResourceForm(forms.ModelForm): used = forms.IntegerField(label=_("Used"), widget=ShowTextWidget(), required=False) allocated = forms.IntegerField(label=_("Allocated")) + unit = forms.CharField(label=_("Unit"), widget=ShowTextWidget(), required=False) class Meta: - fields = ('verbose_name', 'used', 'last_update', 'allocated',) + fields = ('verbose_name', 'used', 'last_update', 'allocated', 'unit') def __init__(self, *args, **kwargs): self.resource = kwargs.pop('resource', None) super(ResourceForm, self).__init__(*args, **kwargs) if self.resource: self.fields['verbose_name'].initial = self.resource.verbose_name + self.fields['unit'].initial = self.resource.unit if self.resource.ondemand: self.fields['allocated'].required = False self.fields['allocated'].widget = ReadOnlyWidget(None, '') diff --git a/orchestra/apps/resources/helpers.py b/orchestra/apps/resources/helpers.py index 11a218f2..3baacb3e 100644 --- a/orchestra/apps/resources/helpers.py +++ b/orchestra/apps/resources/helpers.py @@ -9,7 +9,7 @@ from orchestra.models.utils import get_model_field_path from .backends import ServiceMonitor -def get_used_resource(data): +def compute_resource_usage(data): """ Computes MonitorData.used based on related monitors """ MonitorData = type(data) resource = data.resource @@ -65,4 +65,4 @@ def get_used_resource(data): msg = "%s support not implemented" % data.period raise NotImplementedError(msg) - return result if has_result else None + return result/resource.scale if has_result else None diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index 80d43e21..99b999c9 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -26,14 +26,14 @@ class Resource(models.Model): (MONTHLY_AVG, _("Monthly Average")), ) - name = models.CharField(_("name"), max_length=32, unique=True, + name = models.CharField(_("name"), max_length=32, help_text=_('Required. 32 characters or fewer. Lowercase letters, ' 'digits and hyphen only.'), validators=[validators.RegexValidator(r'^[a-z0-9_\-]+$', _('Enter a valid name.'), 'invalid')]) - verbose_name = models.CharField(_("verbose name"), max_length=256, unique=True) + verbose_name = models.CharField(_("verbose name"), max_length=256) content_type = models.ForeignKey(ContentType, - help_text=_("Model where this resource will be hooked")) + help_text=_("Model where this resource will be hooked.")) period = models.CharField(_("period"), max_length=16, choices=PERIODS, default=LAST, help_text=_("Operation used for aggregating this resource monitored" @@ -45,7 +45,12 @@ class Resource(models.Model): null=True, blank=True, help_text=_("Default allocation value used when this is not an " "on demand resource")) - is_active = models.BooleanField(_("is active"), default=True) + unit = models.CharField(_("unit"), max_length=16, + help_text=_("The unit in which this resource is measured. " + "For example GB, KB or subscribers")) + scale = models.PositiveIntegerField(_("scale"), + help_text=_("Scale in which this resource monitoring resoults should " + "be prorcessed to match with unit.")) disable_trigger = models.BooleanField(_("disable trigger"), default=False, help_text=_("Disables monitors exeeded and recovery triggers")) crontab = models.ForeignKey(CrontabSchedule, verbose_name=_("crontab"), @@ -55,9 +60,16 @@ class Resource(models.Model): monitors = MultiSelectField(_("monitors"), max_length=256, blank=True, choices=ServiceMonitor.get_choices(), help_text=_("Monitor backends used for monitoring this resource.")) + is_active = models.BooleanField(_("is active"), default=True) + + class Meta: + unique_together = ( + ('name', 'content_type'), + ('verbose_name', 'content_type') + ) def __unicode__(self): - return self.name + return "{}-{}".format(str(self.content_type), self.name) def save(self, *args, **kwargs): super(Resource, self).save(*args, **kwargs) @@ -126,7 +138,7 @@ class ResourceData(models.Model): allocated=resource.default_allocation) def get_used(self): - return helpers.get_used(self) + return helpers.compute_resource_usage(self) class MonitorData(models.Model): @@ -135,8 +147,8 @@ class MonitorData(models.Model): choices=ServiceMonitor.get_choices()) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() - date = models.DateTimeField(auto_now_add=True) - value = models.PositiveIntegerField() + date = models.DateTimeField(_("date"), auto_now_add=True) + value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) content_object = GenericForeignKey() diff --git a/orchestra/apps/vps/backends.py b/orchestra/apps/vps/backends.py index 4b077bda..76f72611 100644 --- a/orchestra/apps/vps/backends.py +++ b/orchestra/apps/vps/backends.py @@ -7,7 +7,7 @@ class OpenVZTraffic(ServiceMonitor): model = 'vps.VPS' resource = ServiceMonitor.TRAFFIC - def process(self, line, obj): + def process(self, line): """ diff with last stored value """ object_id, value = line.split() last = self.get_last_data(object_id) diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index 3300c612..0ca433f7 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -78,6 +78,7 @@ INSTALLED_APPS = ( 'orchestra.apps.issues', 'orchestra.apps.prices', 'orchestra.apps.orders', + 'orchestra.apps.miscellaneous', # Third-party apps 'django_extensions', @@ -139,7 +140,8 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.contacts.models.Contact', 'orchestra.apps.users.models.User', 'orchestra.apps.orders.models.Order', - 'orchestra.apps.prices.models.Price', + 'orchestra.apps.orders.models.Service', + 'orchestra.apps.prices.models.Pack', ), 'collapsible': True, }), @@ -170,11 +172,13 @@ FLUENT_DASHBOARD_APP_ICONS = { 'databases/database': 'database.png', 'databases/databaseuser': 'postgresql.png', 'vps/vps': 'TuxBox.png', + 'miscellaneous/miscellaneous': 'Misc-Misc-Box-icon.png', # Accounts 'accounts/account': 'Face-monkey.png', 'contacts/contact': 'contact.png', 'orders/order': 'basket.png', - 'prices/price': 'price.png', + 'orders/service': 'price.png', + 'prices/pack': 'pack.png', # Administration 'users/user': 'Mr-potato.png', 'djcelery/taskstate': 'taskstate.png', diff --git a/orchestra/static/orchestra/icons/Applications-office.svg b/orchestra/static/orchestra/icons/Applications-office.svg new file mode 100644 index 00000000..a58268d0 --- /dev/null +++ b/orchestra/static/orchestra/icons/Applications-office.svg @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Office Applications + + + office + applications + category + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png b/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7d64b79fcef00dd24d0a2aa82191d3d70ff483a9 GIT binary patch literal 3795 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 JPDHLkV1lk8ElB_X literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg b/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg new file mode 100644 index 00000000..644f1826 --- /dev/null +++ b/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg @@ -0,0 +1,894 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/Package-x-generic.png b/orchestra/static/orchestra/icons/Package-x-generic.png new file mode 100644 index 0000000000000000000000000000000000000000..fafa218fc6c3ad34a9783ae16719d787cd6f2cff GIT binary patch literal 2221 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 literal 0 HcmV?d00001 diff --git a/orchestra/static/orchestra/icons/Package-x-generic.svg b/orchestra/static/orchestra/icons/Package-x-generic.svg new file mode 100644 index 00000000..8d04cd36 --- /dev/null +++ b/orchestra/static/orchestra/icons/Package-x-generic.svg @@ -0,0 +1,418 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Package + + + Jakub Steiner + + + http://jimmac.musichall.cz/ + + + package + archive + tarball + tar + bzip + gzip + zip + arj + tar + jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +