Split services into plans

This commit is contained in:
Marc Aymerich 2014-11-18 13:59:21 +00:00
parent ae7c5b7969
commit 9b87ef5e0d
20 changed files with 296 additions and 141 deletions

10
TODO.md
View File

@ -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 &quot * scape strings before executing scripts in order to prevent exploits: django templates automatically scapes things. Most important is to ensuer that all escape ' to &quot
* 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()` * 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? * 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 * 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. * 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. * 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 * 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) * Directory Protection on webapp and use webapp path as base path (validate)
* User [Group] webapp/website option (validation) which overrides default mainsystemuser * User [Group] webapp/website option (validation) which overrides default mainsystemuser
* validate systemuser.home * validate systemuser.home on server-side
* webapp backend option compatibility check? * webapp backend option compatibility check?
* admin systemuser home/directory, add default home and empty directory with has_shell on admin * 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

View File

@ -43,10 +43,7 @@ def get_services():
def get_accounts(): def get_accounts():
childrens = [ childrens=[]
items.MenuItem(_("Accounts"),
reverse('admin:accounts_account_changelist'))
]
if isinstalled('orchestra.apps.payments'): if isinstalled('orchestra.apps.payments'):
url = reverse('admin:payments_transactionprocess_changelist') url = reverse('admin:payments_transactionprocess_changelist')
childrens.append(items.MenuItem(_("Transaction processes"), url)) childrens.append(items.MenuItem(_("Transaction processes"), url))
@ -68,7 +65,7 @@ def get_administration_items():
if isinstalled('orchestra.apps.services'): if isinstalled('orchestra.apps.services'):
url = reverse('admin:services_service_changelist') url = reverse('admin:services_service_changelist')
childrens.append(items.MenuItem(_("Services"), url)) childrens.append(items.MenuItem(_("Services"), url))
url = reverse('admin:services_plan_changelist') url = reverse('admin:plans_plan_changelist')
childrens.append(items.MenuItem(_("Plans"), url)) childrens.append(items.MenuItem(_("Plans"), url))
if isinstalled('orchestra.apps.orchestration'): if isinstalled('orchestra.apps.orchestration'):
route = reverse('admin:orchestration_route_changelist') route = reverse('admin:orchestration_route_changelist')

View File

@ -40,7 +40,7 @@ class APIRoot(views.APIView):
if model in services: if model in services:
group = 'services' group = 'services'
menu = services[model].menu menu = services[model].menu
elif model in accounts: if model in accounts:
group = 'accountancy' group = 'accountancy'
menu = accounts[model].menu menu = accounts[model].menu
if group and menu: if group and menu:

View File

@ -6,7 +6,7 @@ from django.db.models.loading import get_model
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services from orchestra.core import services, accounts
from orchestra.utils import send_email_template from orchestra.utils import send_email_template
from . import settings from . import settings
@ -154,3 +154,4 @@ class Account(auth.AbstractBaseUser):
services.register(Account, menu=False) services.register(Account, menu=False)
accounts.register(Account)

View File

@ -108,18 +108,33 @@ class MysqlDisk(ServiceMonitor):
""" % context """ % 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): def monitor(self, db):
if db.type != db.MYSQL: if db.type != db.MYSQL:
return return
context = self.get_context(db) context = self.get_context(db)
self.append(textwrap.dedent("""\ self.append("echo %(db_id)s $(monitor %(db_name)s)" % context)
echo %(db_id)s $(mysql -B -e '"
SELECT sum( data_length + index_length ) "Size" def monitor(self, db):
FROM information_schema.TABLES if db.type != db.MYSQL:
WHERE table_schema = "gisp" return
GROUP BY table_schema;' | tail -n 1) \ context = self.get_context(db)
""" % context self.append('echo %(db_id)s $(monitor "%(db_name)s")' % context)
))
def get_context(self, db): def get_context(self, db):
return { return {

View File

@ -41,6 +41,7 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel
'fields': ('password',), 'fields': ('password',),
}), }),
) )
search_fields = ('name', 'address_name', 'address_domain__name', 'account__username')
readonly_fields = ('account_link',) readonly_fields = ('account_link',)
change_readonly_fields = ('name',) change_readonly_fields = ('name',)
form = ListChangeForm form = ListChangeForm

View File

@ -206,16 +206,25 @@ class AutoresponseBackend(ServiceController):
class MaildirDisk(ServiceMonitor): class MaildirDisk(ServiceMonitor):
"""
Maildir disk usage based on Dovecot maildirsize file
http://wiki2.dovecot.org/Quota/Maildir
"""
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
resource = ServiceMonitor.DISK resource = ServiceMonitor.DISK
verbose_name = _("Maildir disk usage") 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): def monitor(self, mailbox):
context = self.get_context(mailbox) context = self.get_context(mailbox)
self.append( self.append("echo %(object_id)s $(monitor %(maildir_path)s)" % context)
"SIZE=$(awk 'NR>1 {s+=$1} END {print s}' %(maildir_path)s)\n"
"echo %(object_id)s ${SIZE:-0}" % context
)
def get_context(self, mailbox): def get_context(self, mailbox):
context = { context = {

View File

@ -17,5 +17,5 @@ ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', (
'sessions', 'sessions',
'orchestration', 'orchestration',
'bills', 'bills',
# Do not put services here (plans) 'services',
)) ))

View File

View File

@ -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)

View File

@ -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)

View File

@ -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. <tt>10**9</tt>', 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')]),
),
]

View File

@ -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,
),
]

View File

@ -4,34 +4,13 @@ from django.core.urlresolvers import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ 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.admin.filters import UsedContentTypeFilter
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services from orchestra.core import services
from .actions import update_orders, view_help, clone from .actions import update_orders, view_help, clone
from .models import Plan, ContractedPlan, Rate, Service from .models import 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',)
class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
@ -56,7 +35,6 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
'on_cancel', 'payment_style', 'tax', 'nominal_price') 'on_cancel', 'payment_style', 'tax', 'nominal_price')
}), }),
) )
inlines = [RateInline]
actions = [update_orders, clone] actions = [update_orders, clone]
change_view_actions = actions + [view_help] change_view_actions = actions + [view_help]
@ -95,6 +73,4 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
return qs return qs
admin.site.register(Plan, PlanAdmin)
admin.site.register(ContractedPlan, ContractedPlanAdmin)
admin.site.register(Service, ServiceAdmin) admin.site.register(Service, ServiceAdmin)

View File

@ -9,78 +9,14 @@ from django.utils.functional import cached_property
from django.utils.module_loading import autodiscover_modules from django.utils.module_loading import autodiscover_modules
from django.utils.translation import ugettext_lazy as _ 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.core.validators import validate_name
from orchestra.models import queryset from orchestra.models import queryset
from . import settings, rating from . import settings
from .handlers import ServiceHandler 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') autodiscover_modules('handlers')
@ -105,12 +41,6 @@ class Service(models.Model):
REFUND = 'REFUND' REFUND = 'REFUND'
PREPAY = 'PREPAY' PREPAY = 'PREPAY'
POSTPAY = 'POSTPAY' 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) description = models.CharField(_("description"), max_length=256, unique=True)
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"), content_type = models.ForeignKey(ContentType, verbose_name=_("content type"),
@ -197,11 +127,12 @@ class Service(models.Model):
default=BILLING_PERIOD) default=BILLING_PERIOD)
rate_algorithm = models.CharField(_("rate algorithm"), max_length=16, rate_algorithm = models.CharField(_("rate algorithm"), max_length=16,
help_text=_("Algorithm used to interprete the rating table."), help_text=_("Algorithm used to interprete the rating table."),
# TODO this should be dynamic, retrieved from rate (plans) app
choices=( choices=(
(STEP_PRICE, _("Step price")), ('STEP_PRICE', _("Step price")),
(MATCH_PRICE, _("Match price")), ('MATCH_PRICE', _("Match price")),
), ),
default=STEP_PRICE) default='STEP_PRICE')
on_cancel = models.CharField(_("on cancel"), max_length=16, on_cancel = models.CharField(_("on cancel"), max_length=16,
help_text=_("Defines the cancellation behaviour of this service."), help_text=_("Defines the cancellation behaviour of this service."),
choices=( choices=(
@ -297,7 +228,8 @@ class Service(models.Model):
@property @property
def rate_method(self): 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): def update_orders(self, commit=True):
order_model = get_model(settings.SERVICES_ORDER_MODEL) 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'): for instance in related_model.objects.all().select_related('account'):
updates += order_model.update_orders(instance, service=self, commit=commit) updates += order_model.update_orders(instance, service=self, commit=commit)
return updates return updates
accounts.register(ContractedPlan)
services.register(ContractedPlan, menu=False)

View File

@ -31,7 +31,7 @@ class SystemUserSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
class Meta: class Meta:
model = SystemUser model = SystemUser
fields = ( fields = (
'url', 'username', 'password', 'home', 'shell', 'groups', 'is_active', 'url', 'username', 'password', 'home', 'directory', 'shell', 'groups', 'is_active',
) )
postonly_fields = ('username',) postonly_fields = ('username',)

View File

@ -72,10 +72,6 @@ INSTALLED_APPS = (
'orchestra.apps.orchestration', 'orchestra.apps.orchestration',
'orchestra.apps.domains', 'orchestra.apps.domains',
'orchestra.apps.systemusers', '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.mailboxes',
'orchestra.apps.lists', 'orchestra.apps.lists',
'orchestra.apps.webapps', 'orchestra.apps.webapps',
@ -85,6 +81,7 @@ INSTALLED_APPS = (
'orchestra.apps.saas', 'orchestra.apps.saas',
'orchestra.apps.issues', 'orchestra.apps.issues',
'orchestra.apps.services', 'orchestra.apps.services',
'orchestra.apps.plans',
'orchestra.apps.orders', 'orchestra.apps.orders',
'orchestra.apps.miscellaneous', 'orchestra.apps.miscellaneous',
'orchestra.apps.bills', 'orchestra.apps.bills',
@ -149,7 +146,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
'orchestra.apps.accounts.models.Account', 'orchestra.apps.accounts.models.Account',
'orchestra.apps.contacts.models.Contact', 'orchestra.apps.contacts.models.Contact',
'orchestra.apps.orders.models.Order', 'orchestra.apps.orders.models.Order',
'orchestra.apps.services.models.ContractedPlan', 'orchestra.apps.plans.models.ContractedPlan',
'orchestra.apps.bills.models.Bill', 'orchestra.apps.bills.models.Bill',
# 'orchestra.apps.payments.models.PaymentSource', # 'orchestra.apps.payments.models.PaymentSource',
'orchestra.apps.payments.models.Transaction', 'orchestra.apps.payments.models.Transaction',
@ -167,7 +164,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
'orchestra.apps.resources.models.Resource', 'orchestra.apps.resources.models.Resource',
'orchestra.apps.resources.models.Monitor', 'orchestra.apps.resources.models.Monitor',
'orchestra.apps.services.models.Service', 'orchestra.apps.services.models.Service',
'orchestra.apps.services.models.Plan', 'orchestra.apps.plans.models.Plan',
'orchestra.apps.miscellaneous.models.MiscService', 'orchestra.apps.miscellaneous.models.MiscService',
), ),
'collapsible': True, 'collapsible': True,
@ -195,7 +192,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
'accounts/account': 'Face-monkey.png', 'accounts/account': 'Face-monkey.png',
'contacts/contact': 'contact_book.png', 'contacts/contact': 'contact_book.png',
'orders/order': 'basket.png', 'orders/order': 'basket.png',
'services/contractedplan': 'ContractedPack.png', 'plans/contractedplan': 'ContractedPack.png',
'services/service': 'price.png', 'services/service': 'price.png',
'bills/bill': 'invoice.png', 'bills/bill': 'invoice.png',
'payments/paymentsource': 'card_in_use.png', 'payments/paymentsource': 'card_in_use.png',
@ -210,7 +207,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
'orchestration/backendlog': 'scriptlog.png', 'orchestration/backendlog': 'scriptlog.png',
'resources/resource': "gauge.png", 'resources/resource': "gauge.png",
'resources/monitor': "Utilities-system-monitor.png", 'resources/monitor': "Utilities-system-monitor.png",
'services/plan': 'Pack.png', 'plans/plan': 'Pack.png',
} }
# Django-celery # Django-celery

View File

@ -18,7 +18,7 @@ class Register(object):
self._registry[model] = AttrDict(**{ self._registry[model] = AttrDict(**{
'verbose_name': kwargs.get('verbose_name', model._meta.verbose_name), 'verbose_name': kwargs.get('verbose_name', model._meta.verbose_name),
'verbose_name_plural': plural, 'verbose_name_plural': plural,
'menu': kwargs.get('menu', True) 'menu': kwargs.get('menu', True),
}) })
def get(self, *args): def get(self, *args):