From 9b87ef5e0d348cedae7c202f98b85f9844be1f50 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Tue, 18 Nov 2014 13:59:21 +0000 Subject: [PATCH] Split services into plans --- TODO.md | 10 ++- orchestra/admin/menu.py | 7 +- orchestra/api/root.py | 2 +- orchestra/apps/accounts/models.py | 3 +- orchestra/apps/databases/backends.py | 35 +++++--- orchestra/apps/lists/admin.py | 1 + orchestra/apps/mailboxes/backends.py | 17 +++- orchestra/apps/orders/settings.py | 2 +- orchestra/apps/plans/__init__.py | 0 orchestra/apps/plans/admin.py | 37 ++++++++ orchestra/apps/plans/models.py | 90 +++++++++++++++++++ orchestra/apps/{services => plans}/rating.py | 0 .../apps/resources/migrations/0001_initial.py | 80 +++++++++++++++++ .../migrations/0002_auto_20141117_1415.py | 20 +++++ .../apps/resources/migrations/__init__.py | 0 orchestra/apps/services/admin.py | 28 +----- orchestra/apps/services/models.py | 88 ++---------------- orchestra/apps/systemusers/serializers.py | 2 +- orchestra/conf/base_settings.py | 13 ++- orchestra/core/__init__.py | 2 +- 20 files changed, 296 insertions(+), 141 deletions(-) create mode 100644 orchestra/apps/plans/__init__.py create mode 100644 orchestra/apps/plans/admin.py create mode 100644 orchestra/apps/plans/models.py rename orchestra/apps/{services => plans}/rating.py (100%) create mode 100644 orchestra/apps/resources/migrations/0001_initial.py create mode 100644 orchestra/apps/resources/migrations/0002_auto_20141117_1415.py create mode 100644 orchestra/apps/resources/migrations/__init__.py diff --git a/TODO.md b/TODO.md index 495c1e35..02e008fa 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ -TODO ==== +==== TODO ==== * scape strings before executing scripts in order to prevent exploits: django templates automatically scapes things. Most important is to ensuer that all escape ' to " * Don't store passwords and other service parameters that can be changed by the services i.e. mailman, vps etc. Find an execution mechanism that trigger `change_password()` @@ -152,7 +152,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * Secondary user home in /home/secondaryuser and simlink to /home/main/webapps/app so it can have private storage? * Grant permissions to systemusers, the problem of creating a related permission model is out of sync with the server-side. evaluate tradeoff -* Secondaryusers home should be under mainuser home. i.e. /home/mainuser/webapps/seconduser_webapp/ * Make one dedicated CGI user for each account only for CGI execution (fpm/fcgid). Different from the files owner, and without W permissions, so attackers can not inject backdors and malware. * In most cases we can prevent the creation of files for the CGI users, preventing attackers to upload and executing PHPShells. * Make main systemuser able to write/read everything on its home, including stuff created by the CGI user and secondary users @@ -169,8 +168,13 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * Directory Protection on webapp and use webapp path as base path (validate) * User [Group] webapp/website option (validation) which overrides default mainsystemuser -* validate systemuser.home +* validate systemuser.home on server-side * webapp backend option compatibility check? * admin systemuser home/directory, add default home and empty directory with has_shell on admin + + +* Backendlog doesn't show during execution, transaction isolation or what? + +* Resource used_list_display=True, allocated_list_displat=True, allow resources to show up on list_display diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 47776fc3..1bacdf64 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -43,10 +43,7 @@ def get_services(): def get_accounts(): - childrens = [ - items.MenuItem(_("Accounts"), - reverse('admin:accounts_account_changelist')) - ] + childrens=[] if isinstalled('orchestra.apps.payments'): url = reverse('admin:payments_transactionprocess_changelist') childrens.append(items.MenuItem(_("Transaction processes"), url)) @@ -68,7 +65,7 @@ def get_administration_items(): if isinstalled('orchestra.apps.services'): url = reverse('admin:services_service_changelist') childrens.append(items.MenuItem(_("Services"), url)) - url = reverse('admin:services_plan_changelist') + url = reverse('admin:plans_plan_changelist') childrens.append(items.MenuItem(_("Plans"), url)) if isinstalled('orchestra.apps.orchestration'): route = reverse('admin:orchestration_route_changelist') diff --git a/orchestra/api/root.py b/orchestra/api/root.py index 7e48cefe..977d4f08 100644 --- a/orchestra/api/root.py +++ b/orchestra/api/root.py @@ -40,7 +40,7 @@ class APIRoot(views.APIView): if model in services: group = 'services' menu = services[model].menu - elif model in accounts: + if model in accounts: group = 'accountancy' menu = accounts[model].menu if group and menu: diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index dfbbba1f..6bdbc799 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -6,7 +6,7 @@ from django.db.models.loading import get_model from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from orchestra.core import services +from orchestra.core import services, accounts from orchestra.utils import send_email_template from . import settings @@ -154,3 +154,4 @@ class Account(auth.AbstractBaseUser): services.register(Account, menu=False) +accounts.register(Account) diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index 5a78d2b1..b6316ef6 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -95,7 +95,7 @@ class MysqlDisk(ServiceMonitor): return context = self.get_context(db) self.append(textwrap.dedent("""\ - mysql -e 'UPDATE db SET Insert_priv="N", Create_priv="N" WHERE Db="%(db_name)s";' \ + mysql -e 'UPDATE db SET Insert_priv="N", Create_priv="N" WHERE Db="%(db_name)s";'\ """ % context )) @@ -104,22 +104,37 @@ class MysqlDisk(ServiceMonitor): return context = self.get_context(db) self.append(textwrap.dedent("""\ - mysql -e 'UPDATE db SET Insert_priv="Y", Create_priv="Y" WHERE Db="%(db_name)s";' \ + mysql -e 'UPDATE db SET Insert_priv="Y", Create_priv="Y" WHERE Db="%(db_name)s";'\ """ % context )) + + def prepare(self): + """ slower """ + self.append(textwrap.dedent("""\ + function monitor () { + { du -bs "/var/lib/mysql/$1" || echo 0; } | awk {'print $1'} + }""")) + # Slower way + #self.append(textwrap.dedent("""\ + # function monitor () { + # mysql -B -e " + # SELECT IFNULL(sum(data_length + index_length), 0) 'Size' + # FROM information_schema.TABLES + # WHERE table_schema = '$1'; + # " | tail -n 1 + # }""")) def monitor(self, db): if db.type != db.MYSQL: return context = self.get_context(db) - self.append(textwrap.dedent("""\ - echo %(db_id)s $(mysql -B -e '" - SELECT sum( data_length + index_length ) "Size" - FROM information_schema.TABLES - WHERE table_schema = "gisp" - GROUP BY table_schema;' | tail -n 1) \ - """ % context - )) + self.append("echo %(db_id)s $(monitor %(db_name)s)" % context) + + def monitor(self, db): + if db.type != db.MYSQL: + return + context = self.get_context(db) + self.append('echo %(db_id)s $(monitor "%(db_name)s")' % context) def get_context(self, db): return { diff --git a/orchestra/apps/lists/admin.py b/orchestra/apps/lists/admin.py index 89b3e2eb..a4778eb4 100644 --- a/orchestra/apps/lists/admin.py +++ b/orchestra/apps/lists/admin.py @@ -41,6 +41,7 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel 'fields': ('password',), }), ) + search_fields = ('name', 'address_name', 'address_domain__name', 'account__username') readonly_fields = ('account_link',) change_readonly_fields = ('name',) form = ListChangeForm diff --git a/orchestra/apps/mailboxes/backends.py b/orchestra/apps/mailboxes/backends.py index 86bfddb7..f11d6508 100644 --- a/orchestra/apps/mailboxes/backends.py +++ b/orchestra/apps/mailboxes/backends.py @@ -206,16 +206,25 @@ class AutoresponseBackend(ServiceController): class MaildirDisk(ServiceMonitor): + """ + Maildir disk usage based on Dovecot maildirsize file + + http://wiki2.dovecot.org/Quota/Maildir + """ model = 'mailboxes.Mailbox' resource = ServiceMonitor.DISK verbose_name = _("Maildir disk usage") + def prepare(self): + current_date = self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z") + self.append(textwrap.dedent("""\ + function monitor () { + awk 'NR>1 {s+=$1} END {print s}' $1 || echo 0 + }""")) + def monitor(self, mailbox): context = self.get_context(mailbox) - self.append( - "SIZE=$(awk 'NR>1 {s+=$1} END {print s}' %(maildir_path)s)\n" - "echo %(object_id)s ${SIZE:-0}" % context - ) + self.append("echo %(object_id)s $(monitor %(maildir_path)s)" % context) def get_context(self, mailbox): context = { diff --git a/orchestra/apps/orders/settings.py b/orchestra/apps/orders/settings.py index 60f031c5..3d0bf74b 100644 --- a/orchestra/apps/orders/settings.py +++ b/orchestra/apps/orders/settings.py @@ -17,5 +17,5 @@ ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', ( 'sessions', 'orchestration', 'bills', - # Do not put services here (plans) + 'services', )) diff --git a/orchestra/apps/plans/__init__.py b/orchestra/apps/plans/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/plans/admin.py b/orchestra/apps/plans/admin.py new file mode 100644 index 00000000..ed5d7644 --- /dev/null +++ b/orchestra/apps/plans/admin.py @@ -0,0 +1,37 @@ +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +from orchestra.admin import ExtendedModelAdmin +from orchestra.admin.filters import UsedContentTypeFilter +from orchestra.admin.utils import insertattr +from orchestra.apps.accounts.admin import AccountAdminMixin +from orchestra.apps.services.models import Service + +from .models import Plan, ContractedPlan, Rate + + +class RateInline(admin.TabularInline): + model = Rate + ordering = ('plan', 'quantity') + + +class PlanAdmin(ExtendedModelAdmin): + list_display = ('name', 'is_default', 'is_combinable', 'allow_multiple') + list_filter = ('is_default', 'is_combinable', 'allow_multiple') + fields = ('verbose_name', 'name', 'is_default', 'is_combinable', 'allow_multiple') + prepopulated_fields = { + 'name': ('verbose_name',) + } + change_readonly_fields = ('name',) + inlines = [RateInline] + + +class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin): + list_display = ('plan', 'account_link') + list_filter = ('plan__name',) + + +admin.site.register(Plan, PlanAdmin) +admin.site.register(ContractedPlan, ContractedPlanAdmin) + +insertattr(Service, 'inlines', RateInline) diff --git a/orchestra/apps/plans/models.py b/orchestra/apps/plans/models.py new file mode 100644 index 00000000..f0d1a57e --- /dev/null +++ b/orchestra/apps/plans/models.py @@ -0,0 +1,90 @@ +import decimal + +from django.core.validators import ValidationError +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orchestra.core import services, accounts +from orchestra.core.validators import validate_name +from orchestra.models import queryset + +from . import rating + + +class Plan(models.Model): + name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name]) + verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True) + is_default = models.BooleanField(_("default"), default=False, + help_text=_("Designates whether this plan is used by default or not.")) + is_combinable = models.BooleanField(_("combinable"), default=True, + help_text=_("Designates whether this plan can be combined with other plans or not.")) + allow_multiple = models.BooleanField(_("allow multiple"), default=False, + help_text=_("Designates whether this plan allow for multiple contractions.")) + + def __unicode__(self): + return self.name + + def clean(self): + self.verbose_name = self.verbose_name.strip() + + def get_verbose_name(self): + return self.verbose_name or self.name + + +class ContractedPlan(models.Model): + plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='contracts') + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='plans') + + class Meta: + verbose_name_plural = _("plans") + + def __unicode__(self): + return str(self.plan) + + def clean(self): + if not self.pk and not self.plan.allow_multiples: + if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists(): + raise ValidationError("A contracted plan for this account already exists.") + + +class RateQuerySet(models.QuerySet): + group_by = queryset.group_by + + def by_account(self, account): + # Default allways selected + return self.filter( + Q(plan__is_default=True) | + Q(plan__contracts__account=account) + ).order_by('plan', 'quantity').select_related('plan') + + +class Rate(models.Model): + STEP_PRICE = 'STEP_PRICE' + MATCH_PRICE = 'MATCH_PRICE' + RATE_METHODS = { + STEP_PRICE: rating.step_price, + MATCH_PRICE: rating.match_price, + } + + service = models.ForeignKey('services.Service', verbose_name=_("service"), + related_name='rates') + plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates') + quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True) + price = models.DecimalField(_("price"), max_digits=12, decimal_places=2) + + objects = RateQuerySet.as_manager() + + class Meta: + unique_together = ('service', 'plan', 'quantity') + + def __unicode__(self): + return "{}-{}".format(str(self.price), self.quantity) + + @classmethod + def get_methods(self): + return self.RATE_METHODS + + +accounts.register(ContractedPlan) +services.register(ContractedPlan, menu=False) diff --git a/orchestra/apps/services/rating.py b/orchestra/apps/plans/rating.py similarity index 100% rename from orchestra/apps/services/rating.py rename to orchestra/apps/plans/rating.py diff --git a/orchestra/apps/resources/migrations/0001_initial.py b/orchestra/apps/resources/migrations/0001_initial.py new file mode 100644 index 00000000..6c0451a6 --- /dev/null +++ b/orchestra/apps/resources/migrations/0001_initial.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import orchestra.core.validators +import orchestra.apps.resources.validators +import django.utils.timezone +import orchestra.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('djcelery', '__first__'), + ('contenttypes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='MonitorData', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('monitor', models.CharField(max_length=256, verbose_name='monitor', choices=[(b'Apache2Traffic', '[M] Apache 2 Traffic'), (b'MaildirDisk', '[M] Maildir disk usage'), (b'MailmanSubscribers', '[M] Mailman subscribers'), (b'MailmanTraffic', '[M] Mailman traffic'), (b'FTPTraffic', '[M] Main FTP traffic'), (b'SystemUserDisk', '[M] Main user disk'), (b'MysqlDisk', '[M] MySQL disk'), (b'OpenVZTraffic', '[M] OpenVZTraffic')])), + ('object_id', models.PositiveIntegerField(verbose_name='object id')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')), + ('value', models.DecimalField(verbose_name='value', max_digits=16, decimal_places=2)), + ('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType')), + ], + options={ + 'get_latest_by': 'id', + 'verbose_name_plural': 'monitor data', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(help_text='Required. 32 characters or fewer. Lowercase letters, digits and hyphen only.', max_length=32, verbose_name='name', validators=[orchestra.core.validators.validate_name])), + ('verbose_name', models.CharField(max_length=256, verbose_name='verbose name')), + ('period', models.CharField(default=b'LAST', help_text='Operation used for aggregating this resource monitored data.', max_length=16, verbose_name='period', choices=[(b'LAST', 'Last'), (b'MONTHLY_SUM', 'Monthly Sum'), (b'MONTHLY_AVG', 'Monthly Average')])), + ('on_demand', models.BooleanField(default=False, help_text='If enabled the resource will not be pre-allocated, but allocated under the application demand', verbose_name='on demand')), + ('default_allocation', models.PositiveIntegerField(help_text='Default allocation value used when this is not an on demand resource', null=True, verbose_name='default allocation', blank=True)), + ('unit', models.CharField(help_text='The unit in which this resource is represented. For example GB, KB or subscribers', max_length=16, verbose_name='unit')), + ('scale', models.CharField(help_text='Scale in which this resource monitoring resoults should be prorcessed to match with unit. e.g. 10**9', max_length=32, verbose_name='scale', validators=[orchestra.apps.resources.validators.validate_scale])), + ('disable_trigger', models.BooleanField(default=False, help_text='Disables monitors exeeded and recovery triggers', verbose_name='disable trigger')), + ('monitors', orchestra.models.fields.MultiSelectField(blank=True, help_text='Monitor backends used for monitoring this resource.', max_length=256, verbose_name='monitors', choices=[(b'Apache2Traffic', '[M] Apache 2 Traffic'), (b'MaildirDisk', '[M] Maildir disk usage'), (b'MailmanSubscribers', '[M] Mailman subscribers'), (b'MailmanTraffic', '[M] Mailman traffic'), (b'FTPTraffic', '[M] Main FTP traffic'), (b'SystemUserDisk', '[M] Main user disk'), (b'MysqlDisk', '[M] MySQL disk'), (b'OpenVZTraffic', '[M] OpenVZTraffic')])), + ('is_active', models.BooleanField(default=True, verbose_name='active')), + ('content_type', models.ForeignKey(help_text='Model where this resource will be hooked.', to='contenttypes.ContentType')), + ('crontab', models.ForeignKey(blank=True, to='djcelery.CrontabSchedule', help_text='Crontab for periodic execution. Leave it empty to disable periodic monitoring', null=True, verbose_name='crontab')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ResourceData', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('object_id', models.PositiveIntegerField(verbose_name='object id')), + ('used', models.DecimalField(verbose_name='used', null=True, editable=False, max_digits=16, decimal_places=2)), + ('updated_at', models.DateTimeField(verbose_name='updated', null=True, editable=False)), + ('allocated', models.DecimalField(null=True, verbose_name='allocated', max_digits=8, decimal_places=2, blank=True)), + ('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType')), + ('resource', models.ForeignKey(related_name='dataset', verbose_name='resource', to='resources.Resource')), + ], + options={ + 'verbose_name_plural': 'resource data', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='resourcedata', + unique_together=set([('resource', 'content_type', 'object_id')]), + ), + migrations.AlterUniqueTogether( + name='resource', + unique_together=set([('name', 'content_type'), ('verbose_name', 'content_type')]), + ), + ] diff --git a/orchestra/apps/resources/migrations/0002_auto_20141117_1415.py b/orchestra/apps/resources/migrations/0002_auto_20141117_1415.py new file mode 100644 index 00000000..4168ee24 --- /dev/null +++ b/orchestra/apps/resources/migrations/0002_auto_20141117_1415.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='resourcedata', + name='used', + field=models.DecimalField(verbose_name='used', null=True, editable=False, max_digits=16, decimal_places=3), + preserve_default=True, + ), + ] diff --git a/orchestra/apps/resources/migrations/__init__.py b/orchestra/apps/resources/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/services/admin.py b/orchestra/apps/services/admin.py index 15d878bc..16db2c73 100644 --- a/orchestra/apps/services/admin.py +++ b/orchestra/apps/services/admin.py @@ -4,34 +4,13 @@ from django.core.urlresolvers import reverse from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from orchestra.admin import ChangeViewActionsMixin, ExtendedModelAdmin +from orchestra.admin import ChangeViewActionsMixin from orchestra.admin.filters import UsedContentTypeFilter from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.core import services from .actions import update_orders, view_help, clone -from .models import Plan, ContractedPlan, Rate, Service - - -class RateInline(admin.TabularInline): - model = Rate - ordering = ('plan', 'quantity') - - -class PlanAdmin(ExtendedModelAdmin): - list_display = ('name', 'is_default', 'is_combinable', 'allow_multiple') - list_filter = ('is_default', 'is_combinable', 'allow_multiple') - fields = ('verbose_name', 'name', 'is_default', 'is_combinable', 'allow_multiple') - prepopulated_fields = { - 'name': ('verbose_name',) - } - change_readonly_fields = ('name',) - inlines = [RateInline] - - -class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin): - list_display = ('plan', 'account_link') - list_filter = ('plan__name',) +from .models import Service class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): @@ -56,7 +35,6 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): 'on_cancel', 'payment_style', 'tax', 'nominal_price') }), ) - inlines = [RateInline] actions = [update_orders, clone] change_view_actions = actions + [view_help] @@ -95,6 +73,4 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): return qs -admin.site.register(Plan, PlanAdmin) -admin.site.register(ContractedPlan, ContractedPlanAdmin) admin.site.register(Service, ServiceAdmin) diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index c91f588f..53fd99d1 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -9,78 +9,14 @@ from django.utils.functional import cached_property from django.utils.module_loading import autodiscover_modules from django.utils.translation import ugettext_lazy as _ -from orchestra.core import caches, services, accounts, validators +from orchestra.core import caches, validators from orchestra.core.validators import validate_name from orchestra.models import queryset -from . import settings, rating +from . import settings from .handlers import ServiceHandler -class Plan(models.Model): - name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name]) - verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True) - is_default = models.BooleanField(_("default"), default=False, - help_text=_("Designates whether this plan is used by default or not.")) - is_combinable = models.BooleanField(_("combinable"), default=True, - help_text=_("Designates whether this plan can be combined with other plans or not.")) - allow_multiple = models.BooleanField(_("allow multiple"), default=False, - help_text=_("Designates whether this plan allow for multiple contractions.")) - - def __unicode__(self): - return self.name - - def clean(self): - self.verbose_name = self.verbose_name.strip() - - def get_verbose_name(self): - return self.verbose_name or self.name - - -class ContractedPlan(models.Model): - plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='contracts') - account = models.ForeignKey('accounts.Account', verbose_name=_("account"), - related_name='plans') - - class Meta: - verbose_name_plural = _("plans") - - def __unicode__(self): - return str(self.plan) - - def clean(self): - if not self.pk and not self.plan.allow_multiples: - if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists(): - raise ValidationError("A contracted plan for this account already exists.") - - -class RateQuerySet(models.QuerySet): - group_by = queryset.group_by - - def by_account(self, account): - # Default allways selected - return self.filter( - Q(plan__is_default=True) | - Q(plan__contracts__account=account) - ).order_by('plan', 'quantity').select_related('plan') - - -class Rate(models.Model): - service = models.ForeignKey('services.Service', verbose_name=_("service"), - related_name='rates') - plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates') - quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True) - price = models.DecimalField(_("price"), max_digits=12, decimal_places=2) - - objects = RateQuerySet.as_manager() - - class Meta: - unique_together = ('service', 'plan', 'quantity') - - def __unicode__(self): - return "{}-{}".format(str(self.price), self.quantity) - - autodiscover_modules('handlers') @@ -105,12 +41,6 @@ class Service(models.Model): REFUND = 'REFUND' PREPAY = 'PREPAY' POSTPAY = 'POSTPAY' - STEP_PRICE = 'STEP_PRICE' - MATCH_PRICE = 'MATCH_PRICE' - RATE_METHODS = { - STEP_PRICE: rating.step_price, - MATCH_PRICE: rating.match_price, - } description = models.CharField(_("description"), max_length=256, unique=True) content_type = models.ForeignKey(ContentType, verbose_name=_("content type"), @@ -197,11 +127,12 @@ class Service(models.Model): default=BILLING_PERIOD) rate_algorithm = models.CharField(_("rate algorithm"), max_length=16, help_text=_("Algorithm used to interprete the rating table."), + # TODO this should be dynamic, retrieved from rate (plans) app choices=( - (STEP_PRICE, _("Step price")), - (MATCH_PRICE, _("Match price")), + ('STEP_PRICE', _("Step price")), + ('MATCH_PRICE', _("Match price")), ), - default=STEP_PRICE) + default='STEP_PRICE') on_cancel = models.CharField(_("on cancel"), max_length=16, help_text=_("Defines the cancellation behaviour of this service."), choices=( @@ -297,7 +228,8 @@ class Service(models.Model): @property def rate_method(self): - return self.RATE_METHODS[self.rate_algorithm] + rate_model = type(self).rates.related.model + return rate_model.get_methods()[self.rate_algorithm] def update_orders(self, commit=True): order_model = get_model(settings.SERVICES_ORDER_MODEL) @@ -306,7 +238,3 @@ class Service(models.Model): for instance in related_model.objects.all().select_related('account'): updates += order_model.update_orders(instance, service=self, commit=commit) return updates - - -accounts.register(ContractedPlan) -services.register(ContractedPlan, menu=False) diff --git a/orchestra/apps/systemusers/serializers.py b/orchestra/apps/systemusers/serializers.py index a7e76b1e..ab7b05a9 100644 --- a/orchestra/apps/systemusers/serializers.py +++ b/orchestra/apps/systemusers/serializers.py @@ -31,7 +31,7 @@ class SystemUserSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): class Meta: model = SystemUser fields = ( - 'url', 'username', 'password', 'home', 'shell', 'groups', 'is_active', + 'url', 'username', 'password', 'home', 'directory', 'shell', 'groups', 'is_active', ) postonly_fields = ('username',) diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index e74de7c1..9a115637 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -72,10 +72,6 @@ INSTALLED_APPS = ( 'orchestra.apps.orchestration', 'orchestra.apps.domains', 'orchestra.apps.systemusers', -# 'orchestra.apps.users', -# 'orchestra.apps.users.roles.mail', -# 'orchestra.apps.users.roles.jabber', -# 'orchestra.apps.users.roles.posix', 'orchestra.apps.mailboxes', 'orchestra.apps.lists', 'orchestra.apps.webapps', @@ -85,6 +81,7 @@ INSTALLED_APPS = ( 'orchestra.apps.saas', 'orchestra.apps.issues', 'orchestra.apps.services', + 'orchestra.apps.plans', 'orchestra.apps.orders', 'orchestra.apps.miscellaneous', 'orchestra.apps.bills', @@ -149,7 +146,7 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.accounts.models.Account', 'orchestra.apps.contacts.models.Contact', 'orchestra.apps.orders.models.Order', - 'orchestra.apps.services.models.ContractedPlan', + 'orchestra.apps.plans.models.ContractedPlan', 'orchestra.apps.bills.models.Bill', # 'orchestra.apps.payments.models.PaymentSource', 'orchestra.apps.payments.models.Transaction', @@ -167,7 +164,7 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.resources.models.Resource', 'orchestra.apps.resources.models.Monitor', 'orchestra.apps.services.models.Service', - 'orchestra.apps.services.models.Plan', + 'orchestra.apps.plans.models.Plan', 'orchestra.apps.miscellaneous.models.MiscService', ), 'collapsible': True, @@ -195,7 +192,7 @@ FLUENT_DASHBOARD_APP_ICONS = { 'accounts/account': 'Face-monkey.png', 'contacts/contact': 'contact_book.png', 'orders/order': 'basket.png', - 'services/contractedplan': 'ContractedPack.png', + 'plans/contractedplan': 'ContractedPack.png', 'services/service': 'price.png', 'bills/bill': 'invoice.png', 'payments/paymentsource': 'card_in_use.png', @@ -210,7 +207,7 @@ FLUENT_DASHBOARD_APP_ICONS = { 'orchestration/backendlog': 'scriptlog.png', 'resources/resource': "gauge.png", 'resources/monitor': "Utilities-system-monitor.png", - 'services/plan': 'Pack.png', + 'plans/plan': 'Pack.png', } # Django-celery diff --git a/orchestra/core/__init__.py b/orchestra/core/__init__.py index bdd068f1..3e9c6172 100644 --- a/orchestra/core/__init__.py +++ b/orchestra/core/__init__.py @@ -18,7 +18,7 @@ class Register(object): self._registry[model] = AttrDict(**{ 'verbose_name': kwargs.get('verbose_name', model._meta.verbose_name), 'verbose_name_plural': plural, - 'menu': kwargs.get('menu', True) + 'menu': kwargs.get('menu', True), }) def get(self, *args):