Split services into plans
This commit is contained in:
parent
ae7c5b7969
commit
9b87ef5e0d
10
TODO.md
10
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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -17,5 +17,5 @@ ORDERS_EXCLUDED_APPS = getattr(settings, 'ORDERS_EXCLUDED_APPS', (
|
|||
'sessions',
|
||||
'orchestration',
|
||||
'bills',
|
||||
# Do not put services here (plans)
|
||||
'services',
|
||||
))
|
||||
|
|
0
orchestra/apps/plans/__init__.py
Normal file
0
orchestra/apps/plans/__init__.py
Normal file
37
orchestra/apps/plans/admin.py
Normal file
37
orchestra/apps/plans/admin.py
Normal 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)
|
90
orchestra/apps/plans/models.py
Normal file
90
orchestra/apps/plans/models.py
Normal 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)
|
80
orchestra/apps/resources/migrations/0001_initial.py
Normal file
80
orchestra/apps/resources/migrations/0001_initial.py
Normal 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')]),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
0
orchestra/apps/resources/migrations/__init__.py
Normal file
0
orchestra/apps/resources/migrations/__init__.py
Normal file
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue