Major cleanup
This commit is contained in:
parent
af323ebe25
commit
99b14f9c9f
19
TODO.md
19
TODO.md
|
@ -59,12 +59,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
|||
dependency collector with max_recursion that matches the number of dots on service.match and service.metric
|
||||
|
||||
|
||||
* Be consistent with dates:
|
||||
* created_on date
|
||||
* created_at datetime
|
||||
|
||||
at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
||||
|
||||
|
||||
* backend logs with hal logo
|
||||
* Use logs for storing monitored values
|
||||
|
@ -82,13 +76,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
|||
|
||||
* help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
|
||||
|
||||
* Create ProForma from orders orders.bill(proforma=True)
|
||||
|
||||
* generic confirmation breadcrumbs for single objects
|
||||
|
||||
* DirectDebit due date = bill.due_date
|
||||
|
||||
* settings.ENABLED_PLUGINS = ('path.module.ClassPlugin',)
|
||||
|
||||
* Transaction states: CREATED, PROCESSED, EXECUTED, COMMITED, ABORTED (SECURED, REJECTED?)
|
||||
* bill.send() -> transacction.EXECUTED when source=None
|
||||
|
@ -108,5 +95,7 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
|||
return order.register_at.date()
|
||||
|
||||
|
||||
* latest by 'id' *always*
|
||||
* replace add_now by default=lambda: timezone.now()
|
||||
* mail backend related_models = ('resources__content_type') ??
|
||||
* ignore orders
|
||||
|
||||
* Redmine, BSCW and other applications management
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from functools import wraps, partial
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin import helpers
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import available_attrs
|
||||
|
@ -61,8 +60,10 @@ def action_with_confirmation(action_name=None, extra_context={},
|
|||
|
||||
if len(queryset) == 1:
|
||||
objects_name = force_text(opts.verbose_name)
|
||||
obj = queryset.get()
|
||||
else:
|
||||
objects_name = force_text(opts.verbose_name_plural)
|
||||
obj = None
|
||||
if not action_name:
|
||||
action_name = func.__name__
|
||||
context = {
|
||||
|
@ -73,6 +74,7 @@ def action_with_confirmation(action_name=None, extra_context={},
|
|||
'action_value': action_value,
|
||||
'queryset': queryset,
|
||||
'opts': opts,
|
||||
'obj': obj,
|
||||
'app_label': app_label,
|
||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ from django.shortcuts import redirect
|
|||
from django.utils import importlib
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.models.utils import get_field_value
|
||||
from orchestra.utils import humanize
|
||||
|
|
|
@ -6,7 +6,6 @@ from orchestra.utils.apps import autodiscover as module_autodiscover
|
|||
from orchestra.utils.python import import_class
|
||||
|
||||
from .helpers import insert_links, replace_collectionmethodname
|
||||
from .root import APIRoot
|
||||
|
||||
|
||||
def collectionlink(**kwargs):
|
||||
|
|
|
@ -2,7 +2,6 @@ from django import forms
|
|||
from django.conf.urls import patterns, url
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.admin.util import unquote
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.six.moves.urllib.parse import parse_qsl
|
||||
|
@ -115,7 +114,6 @@ class AccountListAdmin(AccountAdmin):
|
|||
select_account.order_admin_field = 'user__username'
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
opts = self.model._meta
|
||||
original_app_label = request.META['PATH_INFO'].split('/')[-5]
|
||||
original_model = request.META['PATH_INFO'].split('/')[-4]
|
||||
context = {
|
||||
|
@ -182,7 +180,6 @@ class AccountAdminMixin(object):
|
|||
|
||||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||
account_id = self.get_account_from_preserve_filters(request)
|
||||
verb = 'change' if object_id else 'add'
|
||||
if not object_id:
|
||||
if account_id:
|
||||
# Preselect account
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.contrib.admin import SimpleListFilter
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from optparse import make_option
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
from orchestra.apps.users.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -29,5 +28,4 @@ class Command(BaseCommand):
|
|||
username = options.get('username')
|
||||
password = options.get('password')
|
||||
account = Account.objects.create()
|
||||
user = User.objects.create_superuser(username, email, password,
|
||||
account=account, is_main=True)
|
||||
account.users.create_superuser(username, email, password, is_main=True)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import datetime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_auto_20140909_1850'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='account',
|
||||
name='register_date',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='registered_on',
|
||||
field=models.DateField(default=datetime.datetime(2014, 9, 26, 13, 25, 49, 42008), verbose_name='registered', auto_now_add=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -1,6 +1,5 @@
|
|||
from django.conf import settings as djsettings
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services
|
||||
|
@ -18,7 +17,7 @@ class Account(models.Model):
|
|||
language = models.CharField(_("language"), max_length=2,
|
||||
choices=settings.ACCOUNTS_LANGUAGES,
|
||||
default=settings.ACCOUNTS_DEFAULT_LANGUAGE)
|
||||
register_date = models.DateTimeField(_("register date"), auto_now_add=True)
|
||||
registered_on = models.DateField(_("registered"), auto_now_add=True)
|
||||
comments = models.TextField(_("comments"), max_length=256, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import admin_link, admin_date
|
||||
from orchestra.admin.utils import admin_date
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
|
||||
from . import settings
|
||||
|
@ -53,8 +53,8 @@ class BillLineInline(admin.TabularInline):
|
|||
|
||||
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'number', 'is_open', 'type_link', 'account_link', 'created_on_display',
|
||||
'num_lines', 'display_total', 'display_payment_state'
|
||||
'number', 'type_link', 'account_link', 'created_on_display',
|
||||
'num_lines', 'display_total', 'display_payment_state', 'is_open'
|
||||
)
|
||||
list_filter = (BillTypeListFilter, 'is_open',)
|
||||
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
|
||||
|
|
18
orchestra/apps/bills/migrations/0008_auto_20140926_1218.py
Normal file
18
orchestra/apps/bills/migrations/0008_auto_20140926_1218.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bills', '0007_auto_20140918_1454'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='bill',
|
||||
options={'get_latest_by': 'id'},
|
||||
),
|
||||
]
|
46
orchestra/apps/bills/migrations/0009_auto_20140926_1220.py
Normal file
46
orchestra/apps/bills/migrations/0009_auto_20140926_1220.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import datetime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '__first__'),
|
||||
('bills', '0008_auto_20140926_1218'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='billline',
|
||||
name='created_on',
|
||||
field=models.DateField(default=datetime.datetime(2014, 9, 26, 12, 20, 24, 908200), verbose_name='created', auto_now_add=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='billline',
|
||||
name='order_billed_on',
|
||||
field=models.DateField(null=True, verbose_name='order billed', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='billline',
|
||||
name='order_billed_until',
|
||||
field=models.DateField(null=True, verbose_name='order billed until', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='billline',
|
||||
name='order_id',
|
||||
field=models.ForeignKey(blank=True, to='orders.Order', help_text='Informative link back to the order', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='billsubline',
|
||||
name='type',
|
||||
field=models.CharField(default=b'OTHER', max_length=16, verbose_name='type', choices=[(b'VOLUME', 'Volume'), (b'COMPENSATION', 'Compensation'), (b'OTHER', 'Other')]),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
29
orchestra/apps/bills/migrations/0010_auto_20140926_1326.py
Normal file
29
orchestra/apps/bills/migrations/0010_auto_20140926_1326.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bills', '0009_auto_20140926_1220'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bill',
|
||||
name='closed_on',
|
||||
field=models.DateField(null=True, verbose_name='closed on', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bill',
|
||||
name='created_on',
|
||||
field=models.DateField(auto_now_add=True, verbose_name='created on'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bill',
|
||||
name='last_modified_on',
|
||||
field=models.DateField(auto_now=True, verbose_name='last modified on'),
|
||||
),
|
||||
]
|
25
orchestra/apps/bills/migrations/0011_auto_20140926_1334.py
Normal file
25
orchestra/apps/bills/migrations/0011_auto_20140926_1334.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import datetime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bills', '0010_auto_20140926_1326'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='bill',
|
||||
name='last_modified_on',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bill',
|
||||
name='updated_on',
|
||||
field=models.DateField(default=datetime.date(2014, 9, 26), verbose_name='updated on', auto_now=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
19
orchestra/apps/bills/migrations/0012_auto_20140926_1458.py
Normal file
19
orchestra/apps/bills/migrations/0012_auto_20140926_1458.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bills', '0011_auto_20140926_1334'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='billline',
|
||||
old_name='order_id',
|
||||
new_name='order',
|
||||
),
|
||||
]
|
|
@ -1,6 +1,6 @@
|
|||
import inspect
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.template import loader, Context
|
||||
from django.utils import timezone
|
||||
|
@ -9,8 +9,8 @@ from django.utils.functional import cached_property
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
from orchestra.apps.contacts.models import Contact
|
||||
from orchestra.core import accounts
|
||||
from orchestra.utils.functional import cached
|
||||
from orchestra.utils.html import html_to_pdf
|
||||
|
||||
from . import settings
|
||||
|
@ -53,13 +53,12 @@ class Bill(models.Model):
|
|||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||
related_name='%(class)s')
|
||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
||||
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
|
||||
# TODO rename to is_closed
|
||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||
closed_on = models.DateField(_("closed on"), blank=True, null=True)
|
||||
is_open = models.BooleanField(_("is open"), default=True)
|
||||
is_sent = models.BooleanField(_("is sent"), default=False)
|
||||
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
||||
updated_on = models.DateField(_("updated on"), auto_now=True)
|
||||
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
comments = models.TextField(_("comments"), blank=True)
|
||||
html = models.TextField(_("HTML"), blank=True)
|
||||
|
@ -145,7 +144,6 @@ class Bill(models.Model):
|
|||
self.save()
|
||||
|
||||
def send(self):
|
||||
from orchestra.apps.contacts.models import Contact
|
||||
self.account.send_email(
|
||||
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
|
||||
context={
|
||||
|
@ -241,9 +239,13 @@ class BillLine(models.Model):
|
|||
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
||||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||
tax = models.PositiveIntegerField(_("tax"))
|
||||
# TODO
|
||||
# order_id = models.ForeignKey('orders.Order', null=True, blank=True,
|
||||
# help_text=_("Informative link back to the order"))
|
||||
# Undo
|
||||
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
|
||||
help_text=_("Informative link back to the order"))
|
||||
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
|
||||
order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
|
||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||
# Amendment
|
||||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||
related_name='amendment_lines', null=True, blank=True)
|
||||
|
||||
|
@ -262,6 +264,17 @@ class BillLine(models.Model):
|
|||
total += subline.total
|
||||
return total
|
||||
|
||||
def undo(self):
|
||||
# TODO warn user that undoing bills with compensations lead to compensation lost
|
||||
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
|
||||
if not getattr(self, attr):
|
||||
raise ValidationError(_("Not enough information stored for undoing"))
|
||||
if self.created_on != self.order.billed_on:
|
||||
raise ValidationError(_("Dates don't match"))
|
||||
self.order.billed_until = self.order_billed_until
|
||||
self.order.billed_on = self.order_billed_on
|
||||
self.delete()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# TODO cost and consistency of this shit
|
||||
super(BillLine, self).save(*args, **kwargs)
|
||||
|
@ -272,10 +285,20 @@ class BillLine(models.Model):
|
|||
|
||||
class BillSubline(models.Model):
|
||||
""" Subline used for describing an item discount """
|
||||
VOLUME = 'VOLUME'
|
||||
COMPENSATION = 'COMPENSATION'
|
||||
OTHER = 'OTHER'
|
||||
TYPES = (
|
||||
(VOLUME, _("Volume")),
|
||||
(COMPENSATION, _("Compensation")),
|
||||
(OTHER, _("Other")),
|
||||
)
|
||||
|
||||
# TODO: order info for undoing
|
||||
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
|
||||
description = models.CharField(_("description"), max_length=256)
|
||||
total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
# TODO type ? Volume and Compensation
|
||||
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# TODO cost of this shit
|
||||
|
|
|
@ -36,3 +36,6 @@ BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT', '0000
|
|||
|
||||
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
|
||||
'bills/bill-notification.email')
|
||||
|
||||
|
||||
BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')
|
||||
|
|
|
@ -78,9 +78,9 @@
|
|||
{% with sublines=line.sublines.all %}
|
||||
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.amount|default:" " }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.quantity|default:" " }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.total }} &{{ currency.lower }};</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
||||
<br>
|
||||
{% for subline in sublines %}
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </span>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from django.db import models
|
||||
from django.conf.urls import patterns
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
from rest_framework import viewsets
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from orchestra.api import router, SetPasswordApiMixin
|
||||
from orchestra.apps.accounts.api import AccountApiMixin
|
||||
|
|
|
@ -77,21 +77,21 @@ class MysqlDisk(ServiceMonitor):
|
|||
verbose_name = _("MySQL disk")
|
||||
|
||||
def exceeded(self, db):
|
||||
context = self.get_context(obj)
|
||||
context = self.get_context(db)
|
||||
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)
|
||||
context = self.get_context(db)
|
||||
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)
|
||||
context = self.get_context(db)
|
||||
self.append(
|
||||
"echo %(db_id)s $(mysql -B -e '"
|
||||
" SELECT sum( data_length + index_length ) \"Size\"\n"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm, ReadOnlyPasswordHashField
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -109,7 +109,6 @@ class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
|
|||
if 'Invalid' not in original:
|
||||
return original
|
||||
encoded = value
|
||||
final_attrs = self.build_attrs(attrs)
|
||||
if not encoded:
|
||||
summary = mark_safe("<strong>%s</strong>" % _("No password set."))
|
||||
else:
|
||||
|
|
14
orchestra/apps/domains/actions.py
Normal file
14
orchestra/apps/domains/actions.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
def view_zone(modeladmin, request, queryset):
|
||||
zone = queryset.get()
|
||||
context = {
|
||||
'opts': modeladmin.model._meta,
|
||||
'object': zone,
|
||||
'title': _("%s zone content") % zone.origin.name
|
||||
}
|
||||
return TemplateResponse(request, 'admin/domains/domain/view_zone.html', context)
|
||||
view_zone.url_name = 'view-zone'
|
||||
view_zone.verbose_name = _("View zone")
|
|
@ -1,17 +1,13 @@
|
|||
from django import forms
|
||||
from django.conf.urls import patterns, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.util import unquote
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import F
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
|
||||
from orchestra.admin.utils import wrap_admin_view, admin_link, change_url
|
||||
from orchestra.admin.utils import admin_link, change_url
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
from orchestra.utils import apps
|
||||
|
||||
from .actions import view_zone
|
||||
from .forms import RecordInlineFormSet, DomainAdminForm
|
||||
from .filters import TopDomainListFilter
|
||||
from .models import Domain, Record
|
||||
|
@ -61,6 +57,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
|
|||
('top_domain', 'True'),
|
||||
)
|
||||
form = DomainAdminForm
|
||||
change_view_actions = [view_zone]
|
||||
|
||||
def structured_name(self, domain):
|
||||
if not domain.is_top:
|
||||
|
@ -89,35 +86,12 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
|
|||
websites.short_description = _("Websites")
|
||||
websites.allow_tags = True
|
||||
|
||||
def get_urls(self):
|
||||
""" Returns the additional urls for the change view links """
|
||||
urls = super(DomainAdmin, self).get_urls()
|
||||
admin_site = self.admin_site
|
||||
opts = self.model._meta
|
||||
urls = patterns("",
|
||||
url('^(\d+)/view-zone/$',
|
||||
wrap_admin_view(self, self.view_zone_view),
|
||||
name='domains_domain_view_zone')
|
||||
) + urls
|
||||
return urls
|
||||
|
||||
def view_zone_view(self, request, object_id):
|
||||
zone = self.get_object(request, unquote(object_id))
|
||||
context = {
|
||||
'opts': self.model._meta,
|
||||
'object': zone,
|
||||
'title': _("%s zone content") % zone.origin.name
|
||||
}
|
||||
return TemplateResponse(request, 'admin/domains/domain/view_zone.html',
|
||||
context)
|
||||
|
||||
def get_queryset(self, request):
|
||||
""" Order by structured name and imporve performance """
|
||||
qs = super(DomainAdmin, self).get_queryset(request)
|
||||
qs = qs.select_related('top')
|
||||
# qs = qs.select_related('top')
|
||||
# For some reason if we do this we know for sure that join table will be called T4
|
||||
__ = str(qs.query)
|
||||
str(qs.query)
|
||||
qs = qs.extra(
|
||||
select={'structured_name': 'CONCAT(T4.name, domains_domain.name)'},
|
||||
).order_by('structured_name')
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import os
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import settings
|
||||
|
|
42
orchestra/apps/domains/migrations/0001_initial.py
Normal file
42
orchestra/apps/domains/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import orchestra.core.validators
|
||||
import orchestra.apps.domains.validators
|
||||
import orchestra.apps.domains.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_auto_20140909_1850'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Domain',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=256, verbose_name='name', validators=[orchestra.core.validators.validate_hostname, orchestra.apps.domains.validators.validate_allowed_domain])),
|
||||
('serial', models.IntegerField(default=orchestra.apps.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')),
|
||||
('account', models.ForeignKey(related_name=b'domains', verbose_name='Account', blank=True, to='accounts.Account')),
|
||||
('top', models.ForeignKey(related_name=b'subdomains', to='domains.Domain', null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Record',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('type', models.CharField(max_length=32, verbose_name='type', choices=[(b'MX', b'MX'), (b'NS', b'NS'), (b'CNAME', b'CNAME'), (b'A', 'A (IPv4 address)'), (b'AAAA', 'AAAA (IPv6 address)'), (b'SRV', b'SRV'), (b'TXT', b'TXT'), (b'SOA', b'SOA')])),
|
||||
('value', models.CharField(max_length=256, verbose_name='value')),
|
||||
('domain', models.ForeignKey(related_name=b'records', verbose_name='domain', to='domains.Domain')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
21
orchestra/apps/domains/migrations/0002_record_ttl.py
Normal file
21
orchestra/apps/domains/migrations/0002_record_ttl.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import orchestra.apps.domains.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('domains', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='record',
|
||||
name='ttl',
|
||||
field=models.CharField(default='', validators=[orchestra.apps.domains.validators.validate_zone_interval], max_length=8, blank=True, help_text='Record TTL, defaults to 1h', verbose_name='TTL'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
0
orchestra/apps/domains/migrations/__init__.py
Normal file
0
orchestra/apps/domains/migrations/__init__.py
Normal file
|
@ -1,4 +1,3 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -6,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from orchestra.core import services
|
||||
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address,
|
||||
validate_hostname, validate_ascii)
|
||||
from orchestra.utils.python import AttrDict
|
||||
|
||||
from . import settings, validators, utils
|
||||
|
||||
|
@ -69,13 +69,17 @@ class Domain(models.Model):
|
|||
# Update serial and insert at 0
|
||||
value = record.value.split()
|
||||
value[2] = str(self.serial)
|
||||
records.insert(0, (record.SOA, ' '.join(value)))
|
||||
records.insert(0,
|
||||
AttrDict(type=record.SOA, ttl=record.get_ttl(), value=' '.join(value))
|
||||
)
|
||||
else:
|
||||
records.append((record.type, record.value))
|
||||
records.append(
|
||||
AttrDict(type=record.type, ttl=record.get_ttl(), value=record.value)
|
||||
)
|
||||
if not self.top:
|
||||
if Record.NS not in types:
|
||||
for ns in settings.DOMAINS_DEFAULT_NS:
|
||||
records.append((Record.NS, ns))
|
||||
records.append(AttrDict(type=Record.NS, value=ns))
|
||||
if Record.SOA not in types:
|
||||
soa = [
|
||||
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
|
||||
|
@ -86,18 +90,28 @@ class Domain(models.Model):
|
|||
settings.DOMAINS_DEFAULT_EXPIRATION,
|
||||
settings.DOMAINS_DEFAULT_MIN_CACHING_TIME
|
||||
]
|
||||
records.insert(0, (Record.SOA, ' '.join(soa)))
|
||||
records.insert(0, AttrDict(type=Record.SOA, value=' '.join(soa)))
|
||||
no_cname = Record.CNAME not in types
|
||||
if Record.MX not in types and no_cname:
|
||||
for mx in settings.DOMAINS_DEFAULT_MX:
|
||||
records.append((Record.MX, mx))
|
||||
records.append(AttrDict(type=Record.MX, value=mx))
|
||||
if (Record.A not in types and Record.AAAA not in types) and no_cname:
|
||||
records.append((Record.A, settings.DOMAINS_DEFAULT_A))
|
||||
records.append(AttrDict(type=Record.A, value=settings.DOMAINS_DEFAULT_A))
|
||||
result = ''
|
||||
for type, value in records:
|
||||
name = '%s.%s' % (self.name, ' '*(37-len(self.name)))
|
||||
type = '%s %s' % (type, ' '*(7-len(type)))
|
||||
result += '%s IN %s %s\n' % (name, type, value)
|
||||
for record in records:
|
||||
name = '{name}.{spaces}'.format(
|
||||
name=self.name, spaces=' ' * (37-len(self.name))
|
||||
)
|
||||
ttl = record.get('ttl', settings.DOMAINS_DEFAULT_TTL)
|
||||
ttl = '{spaces}{ttl}'.format(
|
||||
spaces=' ' * (7-len(ttl)), ttl=ttl
|
||||
)
|
||||
type = '{type} {spaces}'.format(
|
||||
type=record.type, spaces=' ' * (7-len(record.type))
|
||||
)
|
||||
result += '{name} {ttl} IN {type} {value}\n'.format(
|
||||
name=name, ttl=ttl, type=type, value=record.value
|
||||
)
|
||||
return result
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
@ -150,13 +164,15 @@ class Record(models.Model):
|
|||
(SOA, "SOA"),
|
||||
)
|
||||
|
||||
# TODO TTL
|
||||
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
|
||||
type = models.CharField(max_length=32, choices=TYPE_CHOICES)
|
||||
value = models.CharField(max_length=256)
|
||||
ttl = models.CharField(_("TTL"), max_length=8, blank=True,
|
||||
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
|
||||
validators=[validators.validate_zone_interval])
|
||||
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
|
||||
value = models.CharField(_("value"), max_length=256)
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s IN %s %s" % (self.domain, self.type, self.value)
|
||||
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
|
||||
|
||||
def clean(self):
|
||||
""" validates record value based on its type """
|
||||
|
@ -173,5 +189,7 @@ class Record(models.Model):
|
|||
}
|
||||
mapp[self.type](self.value)
|
||||
|
||||
def get_ttl(self):
|
||||
return self.ttl or settings.DOMAINS_DEFAULT_TTL
|
||||
|
||||
services.register(Domain)
|
||||
|
|
|
@ -124,7 +124,7 @@ class DomainTestMixin(object):
|
|||
self.assertNotEqual(hostmaster, soa[5])
|
||||
|
||||
def validate_update(self, server_addr, domain_name):
|
||||
domain = Domain.objects.get(name=domain_name)
|
||||
Domain.objects.get(name=domain_name)
|
||||
context = {
|
||||
'domain_name': domain_name,
|
||||
'server_addr': server_addr
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from django.db import IntegrityError, transaction
|
||||
from django.test import TestCase
|
||||
|
||||
from ..models import Domain
|
||||
|
|
|
@ -56,7 +56,7 @@ class MessageReadOnlyInline(admin.TabularInline):
|
|||
def content_html(self, msg):
|
||||
context = {
|
||||
'number': msg.number,
|
||||
'time': admin_date('created_on')(msg),
|
||||
'time': admin_date('created_at')(msg),
|
||||
'author': admin_link('author')(msg) if msg.author else msg.author_name,
|
||||
}
|
||||
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
|
||||
|
@ -98,11 +98,11 @@ class MessageInline(admin.TabularInline):
|
|||
class TicketInline(admin.TabularInline):
|
||||
fields = [
|
||||
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
|
||||
'colored_priority', 'created', 'last_modified'
|
||||
'colored_priority', 'created', 'updated'
|
||||
]
|
||||
readonly_fields = [
|
||||
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
|
||||
'colored_priority', 'created', 'last_modified'
|
||||
'colored_priority', 'created', 'updated'
|
||||
]
|
||||
model = Ticket
|
||||
extra = 0
|
||||
|
@ -110,8 +110,8 @@ class TicketInline(admin.TabularInline):
|
|||
|
||||
creator_link = admin_link('creator')
|
||||
owner_link = admin_link('owner')
|
||||
created = admin_link('created_on')
|
||||
last_modified = admin_link('last_modified_on')
|
||||
created = admin_link('created_at')
|
||||
updated = admin_link('updated_at')
|
||||
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
|
@ -121,10 +121,10 @@ class TicketInline(admin.TabularInline):
|
|||
ticket_id.allow_tags = True
|
||||
|
||||
|
||||
class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions,
|
||||
class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
|
||||
list_display = [
|
||||
'unbold_id', 'bold_subject', 'display_creator', 'display_owner',
|
||||
'display_queue', 'display_priority', 'display_state', 'last_modified'
|
||||
'display_queue', 'display_priority', 'display_state', 'updated'
|
||||
]
|
||||
list_display_links = ('unbold_id', 'bold_subject')
|
||||
list_filter = [
|
||||
|
@ -134,7 +134,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
|||
('my_tickets', lambda r: 'True' if not r.user.is_superuser else 'False'),
|
||||
('state', 'OPEN')
|
||||
)
|
||||
date_hierarchy = 'created_on'
|
||||
date_hierarchy = 'created_at'
|
||||
search_fields = [
|
||||
'id', 'subject', 'creator__username', 'creator__email', 'queue__name',
|
||||
'owner__username'
|
||||
|
@ -192,20 +192,20 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
|||
display_creator = admin_link('creator')
|
||||
display_queue = admin_link('queue')
|
||||
display_owner = admin_link('owner')
|
||||
last_modified = admin_date('last_modified_on')
|
||||
updated = admin_date('updated')
|
||||
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
|
||||
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
|
||||
|
||||
def display_summary(self, ticket):
|
||||
context = {
|
||||
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
|
||||
'created': admin_date('created_on')(ticket),
|
||||
'created': admin_date('created_at')(ticket),
|
||||
'updated': '',
|
||||
}
|
||||
msg = ticket.messages.last()
|
||||
if msg:
|
||||
context.update({
|
||||
'updated': admin_date('created_on')(msg),
|
||||
'updated': admin_date('created_at')(msg),
|
||||
'updater': admin_link('author')(self, msg) if msg.author else msg.author_name,
|
||||
})
|
||||
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
|
||||
|
@ -283,7 +283,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
|
|||
def message_preview_view(self, request):
|
||||
""" markdown preview render via ajax """
|
||||
data = request.POST.get("data")
|
||||
data_formated = markdowt_tn(strip_tags(data))
|
||||
data_formated = markdown(strip_tags(data))
|
||||
return HttpResponse(data_formated)
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
from django import forms
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from markdown import markdown
|
||||
|
||||
from orchestra.admin.utils import change_url
|
||||
from orchestra.apps.users.models import User
|
||||
from orchestra.forms.widgets import ReadOnlyWidget
|
||||
|
||||
|
@ -42,7 +40,6 @@ class MessageInlineForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
||||
admin_link = change_url(self.user)
|
||||
self.fields['created_on'].widget = ReadOnlyWidget('')
|
||||
|
||||
def clean_content(self):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.conf import settings as djsettings
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.contacts import settings as contacts_settings
|
||||
|
@ -68,13 +67,13 @@ class Ticket(models.Model):
|
|||
priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES,
|
||||
default=MEDIUM)
|
||||
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
|
||||
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
||||
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||
updated_at = models.DateTimeField(_("modified"), auto_now=True)
|
||||
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"),
|
||||
blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-last_modified_on"]
|
||||
ordering = ['-updated_at']
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.pk)
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import textwrap
|
||||
|
||||
from django.template import Template, Context
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.orchestration import ServiceController
|
||||
from orchestra.apps.resources import ServiceMonitor
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import insertattr, admin_link, change_url
|
||||
from orchestra.admin.utils import admin_link, change_url
|
||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
||||
from orchestra.apps.domains.forms import DomainIterator
|
||||
|
||||
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
|
||||
from .models import Mailbox, Address, Autoresponse
|
||||
|
|
|
@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController
|
|||
from orchestra.apps.resources import ServiceMonitor
|
||||
|
||||
from . import settings
|
||||
from .models import Address
|
||||
|
||||
|
||||
class MailSystemUserBackend(ServiceController):
|
||||
|
@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor):
|
|||
)
|
||||
|
||||
def get_context(self, mailbox):
|
||||
context = MailSystemUserBackend().get_context(site)
|
||||
context = MailSystemUserBackend().get_context(mailbox)
|
||||
context['home'] = settings.EMAILS_HOME % context
|
||||
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
||||
context['object_id'] = mailbox.pk
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import re
|
||||
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
|
@ -44,7 +44,6 @@ def validate_forward(value):
|
|||
|
||||
|
||||
def validate_sieve(value):
|
||||
from .models import Mailbox
|
||||
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
|
||||
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
|
||||
with open(path, 'wb') as f:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -89,18 +88,18 @@ class BackendLogAdmin(admin.ModelAdmin):
|
|||
)
|
||||
list_display_links = ('id', 'backend')
|
||||
list_filter = ('state', 'backend')
|
||||
date_hierarchy = 'last_update'
|
||||
date_hierarchy = 'updated_at'
|
||||
inlines = [BackendOperationInline]
|
||||
fields = [
|
||||
'backend', 'server_link', 'state', 'mono_script', 'mono_stdout',
|
||||
'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created',
|
||||
'display_last_update', 'execution_time'
|
||||
'display_updated', 'execution_time'
|
||||
]
|
||||
readonly_fields = fields
|
||||
|
||||
server_link = admin_link('server')
|
||||
display_last_update = admin_date('last_update')
|
||||
display_created = admin_date('created')
|
||||
display_updated = admin_date('updated_at')
|
||||
display_created = admin_date('created_at')
|
||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||
mono_script = display_mono('script')
|
||||
mono_stdout = display_mono('stdout')
|
||||
|
@ -112,6 +111,9 @@ class BackendLogAdmin(admin.ModelAdmin):
|
|||
qs = super(BackendLogAdmin, self).get_queryset(request)
|
||||
return qs.select_related('server').defer('script', 'stdout')
|
||||
|
||||
def has_add_permission(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
|
||||
class ServerAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'address', 'os')
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from functools import partial
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.utils import plugins
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -62,8 +60,8 @@ class BackendLog(models.Model):
|
|||
exit_code = models.IntegerField(_("exit code"), null=True)
|
||||
task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True,
|
||||
help_text="Celery task ID when used as execution backend")
|
||||
created = models.DateTimeField(_("created"), auto_now_add=True)
|
||||
last_update = models.DateTimeField(_("last update"), auto_now=True)
|
||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||
|
||||
class Meta:
|
||||
get_latest_by = 'id'
|
||||
|
@ -89,7 +87,7 @@ class BackendOperation(models.Model):
|
|||
action = models.CharField(_("action"), max_length=64)
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
# TODO rename to content_object
|
||||
|
||||
instance = generic.GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from django.db import IntegrityError, transaction
|
||||
|
||||
from orchestra.utils.tests import BaseTestCase
|
||||
|
||||
from .. import operations, backends
|
||||
|
|
|
@ -78,7 +78,7 @@ class BillSelectedOrders(object):
|
|||
if int(request.POST.get('step')) >= 3:
|
||||
bills = self.queryset.bill(commit=True, **self.options)
|
||||
for order in self.queryset:
|
||||
modeladmin.log_change(request, order, 'Billed')
|
||||
self.modeladmin.log_change(request, order, 'Billed')
|
||||
if not bills:
|
||||
msg = _("Selected orders do not have pending billing")
|
||||
self.modeladmin.message_user(request, msg, messages.WARNING)
|
||||
|
|
|
@ -3,7 +3,6 @@ from django.utils import timezone
|
|||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ChangeListDefaultFilter
|
||||
from orchestra.admin.utils import admin_link, admin_date
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
from orchestra.utils.humanize import naturaldate
|
||||
|
@ -13,7 +12,7 @@ from .filters import ActiveOrderListFilter, BilledOrderListFilter
|
|||
from .models import Order, MetricStorage
|
||||
|
||||
|
||||
class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
|
||||
class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'service', 'account_link', 'content_object_link',
|
||||
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
|
||||
|
@ -22,9 +21,6 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
|
|||
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',)
|
||||
actions = (BillSelectedOrders(),)
|
||||
date_hierarchy = 'registered_on'
|
||||
default_changelist_filters = (
|
||||
('is_active', 'True'),
|
||||
)
|
||||
|
||||
content_object_link = admin_link('content_object', order=False)
|
||||
display_registered_on = admin_date('registered_on')
|
||||
|
|
|
@ -2,7 +2,7 @@ import datetime
|
|||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.bills.models import Invoice, Fee, ProForma, BillLine, BillSubline
|
||||
from orchestra.apps.bills.models import Invoice, Fee, ProForma
|
||||
|
||||
|
||||
class BillsBackend(object):
|
||||
|
@ -39,6 +39,10 @@ class BillsBackend(object):
|
|||
subtotal=line.subtotal,
|
||||
tax=service.tax,
|
||||
description=self.get_line_description(line),
|
||||
|
||||
order=line.order,
|
||||
order_billed_on=line.order.old_billed_on,
|
||||
order_billed_until=line.order.old_billed_until
|
||||
)
|
||||
self.create_sublines(billine, line.discounts)
|
||||
return bills
|
||||
|
@ -46,7 +50,6 @@ class BillsBackend(object):
|
|||
def format_period(self, ini, end):
|
||||
ini = ini.strftime("%b, %Y")
|
||||
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
|
||||
# TODO if diff is less than a month: write the month only
|
||||
if ini == end:
|
||||
return ini
|
||||
return _("{ini} to {end}").format(ini=ini, end=end)
|
||||
|
@ -67,6 +70,7 @@ class BillsBackend(object):
|
|||
def create_sublines(self, line, discounts):
|
||||
for discount in discounts:
|
||||
line.sublines.create(
|
||||
description=_("Discount per %s") % discount.type,
|
||||
description=_("Discount per %s") % discount.type.lower(),
|
||||
total=discount.total,
|
||||
type=discount.type,
|
||||
)
|
||||
|
|
|
@ -13,7 +13,6 @@ class ActiveOrderListFilter(SimpleListFilter):
|
|||
return (
|
||||
('True', _("Active")),
|
||||
('False', _("Inactive")),
|
||||
('None', _("All")),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
|
@ -23,12 +22,6 @@ class ActiveOrderListFilter(SimpleListFilter):
|
|||
return queryset.inactive()
|
||||
return queryset
|
||||
|
||||
def choices(self, cl):
|
||||
""" Remove default All """
|
||||
choices = iter(super(ActiveOrderListFilter, self).choices(cl))
|
||||
choices.next()
|
||||
return choices
|
||||
|
||||
|
||||
class BilledOrderListFilter(SimpleListFilter):
|
||||
""" Filter tickets by created_by according to request.user """
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
import datetime
|
||||
import decimal
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from django.db import models
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.db.models import F, Q
|
||||
from django.db.models.loading import get_model
|
||||
from django.db.models.signals import pre_delete, post_delete, post_save
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import caches, services, accounts
|
||||
from orchestra.core import accounts
|
||||
from orchestra.models import queryset
|
||||
from orchestra.utils.apps import autodiscover
|
||||
from orchestra.utils.python import import_class
|
||||
|
||||
from . import helpers, settings
|
||||
from .handlers import ServiceHandler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -39,8 +35,13 @@ class OrderQuerySet(models.QuerySet):
|
|||
for account, services in qs.group_by('account', 'service').iteritems():
|
||||
bill_lines = []
|
||||
for service, orders in services.iteritems():
|
||||
for order in orders:
|
||||
# Saved for undoing support
|
||||
order.old_billed_on = order.billed_on
|
||||
order.old_billed_until = order.billed_until
|
||||
lines = service.handler.generate_bill_lines(orders, account, **options)
|
||||
bill_lines.extend(lines)
|
||||
# TODO make this consistent always returning the same fucking objects
|
||||
if commit:
|
||||
bills += bill_backend.create_bills(account, bill_lines, **options)
|
||||
else:
|
||||
|
@ -73,7 +74,7 @@ class OrderQuerySet(models.QuerySet):
|
|||
|
||||
def inactive(self, **kwargs):
|
||||
""" return inactive orders """
|
||||
return self.filter(cancelled_on__lt=timezone.now(), **kwargs)
|
||||
return self.filter(cancelled_on__lte=timezone.now(), **kwargs)
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
|
||||
|
|
|
@ -26,8 +26,8 @@ def process_transactions(modeladmin, request, queryset):
|
|||
method = PaymentMethod.get_plugin(method)
|
||||
procs = method.process(transactions)
|
||||
processes += procs
|
||||
for transaction in transactions:
|
||||
modeladmin.log_change(request, transaction, 'Processed')
|
||||
for trans in transactions:
|
||||
modeladmin.log_change(request, trans, 'Processed')
|
||||
if not processes:
|
||||
return
|
||||
opts = modeladmin.model._meta
|
||||
|
@ -44,9 +44,9 @@ def process_transactions(modeladmin, request, queryset):
|
|||
@transaction.atomic
|
||||
@action_with_confirmation()
|
||||
def mark_as_executed(modeladmin, request, queryset, extra_context={}):
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_executed()
|
||||
modeladmin.log_change(request, transaction, 'Executed')
|
||||
for trans in queryset:
|
||||
trans.mark_as_executed()
|
||||
modeladmin.log_change(request, trans, 'Executed')
|
||||
msg = _("%s selected transactions have been marked as executed.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
mark_as_executed.url_name = 'execute'
|
||||
|
@ -56,9 +56,9 @@ mark_as_executed.verbose_name = _("Mark as executed")
|
|||
@transaction.atomic
|
||||
@action_with_confirmation()
|
||||
def mark_as_secured(modeladmin, request, queryset):
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_secured()
|
||||
modeladmin.log_change(request, transaction, 'Secured')
|
||||
for trans in queryset:
|
||||
trans.mark_as_secured()
|
||||
modeladmin.log_change(request, trans, 'Secured')
|
||||
msg = _("%s selected transactions have been marked as secured.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
mark_as_secured.url_name = 'secure'
|
||||
|
@ -68,9 +68,9 @@ mark_as_secured.verbose_name = _("Mark as secured")
|
|||
@transaction.atomic
|
||||
@action_with_confirmation()
|
||||
def mark_as_rejected(modeladmin, request, queryset):
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_rejected()
|
||||
modeladmin.log_change(request, transaction, 'Rejected')
|
||||
for trans in queryset:
|
||||
trans.mark_as_rejected()
|
||||
modeladmin.log_change(request, trans, 'Rejected')
|
||||
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
mark_as_rejected.url_name = 'reject'
|
||||
|
@ -90,7 +90,7 @@ def _format_display_objects(modeladmin, request, queryset, related):
|
|||
for related in getattr(obj.transactions, attr)():
|
||||
subobjects.append(
|
||||
mark_safe('{0}: <a href="{1}">{2}</a> will be marked as {3}'.format(
|
||||
capfirst(subobj.get_type().lower()), change_url(subobj), subobj, verb))
|
||||
capfirst(related.get_type().lower()), change_url(related), related, verb))
|
||||
)
|
||||
objects.append(subobjects)
|
||||
return {'display_objects': objects}
|
||||
|
@ -127,9 +127,9 @@ abort.verbose_name = _("Abort")
|
|||
@transaction.atomic
|
||||
@action_with_confirmation(extra_context=_format_commit)
|
||||
def commit(modeladmin, request, queryset):
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_rejected()
|
||||
modeladmin.log_change(request, transaction, 'Rejected')
|
||||
for trans in queryset:
|
||||
trans.mark_as_rejected()
|
||||
modeladmin.log_change(request, trans, 'Rejected')
|
||||
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
commit.url_name = 'commit'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from django import forms
|
||||
from django.conf.urls import patterns, url
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
|
@ -37,7 +36,6 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
|||
def get_urls(self):
|
||||
""" Hooks select account url """
|
||||
urls = super(PaymentSourceAdmin, self).get_urls()
|
||||
admin_site = self.admin_site
|
||||
opts = self.model._meta
|
||||
info = opts.app_label, opts.model_name
|
||||
select_urls = patterns("",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
|
@ -99,8 +98,8 @@ class Transaction(models.Model):
|
|||
default=WAITTING_PROCESSING)
|
||||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
||||
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
|
||||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
modified_on = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||
modified_at = models.DateTimeField(_("modified"), auto_now=True)
|
||||
|
||||
objects = TransactionQuerySet.as_manager()
|
||||
|
||||
|
|
|
@ -15,16 +15,16 @@ from .models import Resource, ResourceData, MonitorData
|
|||
|
||||
class ResourceAdmin(ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'id', 'verbose_name', 'content_type', 'period', 'ondemand',
|
||||
'id', 'verbose_name', 'content_type', 'period', 'on_demand',
|
||||
'default_allocation', 'unit', 'disable_trigger', 'crontab',
|
||||
)
|
||||
list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger')
|
||||
list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name', 'content_type', 'period'),
|
||||
}),
|
||||
(_("Configuration"), {
|
||||
'fields': ('verbose_name', 'unit', 'scale', 'ondemand',
|
||||
'fields': ('verbose_name', 'unit', 'scale', 'on_demand',
|
||||
'default_allocation', 'disable_trigger', 'is_active'),
|
||||
}),
|
||||
(_("Monitoring"), {
|
||||
|
@ -64,7 +64,7 @@ class ResourceAdmin(ExtendedModelAdmin):
|
|||
|
||||
class ResourceDataAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'resource', 'used', 'allocated', 'last_update', 'content_object_link'
|
||||
'id', 'resource', 'used', 'allocated', 'updated_at', 'content_object_link'
|
||||
)
|
||||
list_filter = ('resource',)
|
||||
readonly_fields = ('content_object_link',)
|
||||
|
@ -77,7 +77,7 @@ class ResourceDataAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
class MonitorDataAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'monitor', 'date', 'value', 'content_object_link')
|
||||
list_display = ('id', 'monitor', 'created_at', 'value', 'content_object_link')
|
||||
list_filter = ('monitor',)
|
||||
readonly_fields = ('content_object_link',)
|
||||
|
||||
|
@ -118,16 +118,16 @@ def resource_inline_factory(resources):
|
|||
formset = ResourceInlineFormSet
|
||||
can_delete = False
|
||||
fields = (
|
||||
'verbose_name', 'used', 'display_last_update', 'allocated', 'unit'
|
||||
'verbose_name', 'used', 'display_updated', 'allocated', 'unit'
|
||||
)
|
||||
readonly_fields = ('used', 'display_last_update')
|
||||
readonly_fields = ('used', 'display_updated')
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('orchestra/css/hide-inline-id.css',)
|
||||
}
|
||||
|
||||
display_last_update = admin_date('last_update', default=_("Never"))
|
||||
display_updated = admin_date('updated_at', default=_("Never"))
|
||||
|
||||
def has_add_permission(self, *args, **kwargs):
|
||||
""" Hidde add another """
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.apps import AppConfig
|
||||
from django.contrib.contenttypes import generic
|
||||
|
||||
from orchestra.utils import running_syncdb
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
from django import forms
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from djcelery.humanize import naturaldate
|
||||
|
||||
from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
|
||||
|
||||
|
@ -21,7 +19,7 @@ class ResourceForm(forms.ModelForm):
|
|||
if self.resource:
|
||||
self.fields['verbose_name'].initial = self.resource.verbose_name
|
||||
self.fields['unit'].initial = self.resource.unit
|
||||
if self.resource.ondemand:
|
||||
if self.resource.on_demand:
|
||||
self.fields['allocated'].required = False
|
||||
self.fields['allocated'].widget = ReadOnlyWidget(None, '')
|
||||
else:
|
||||
|
|
|
@ -38,13 +38,15 @@ def compute_resource_usage(data):
|
|||
continue
|
||||
has_result = True
|
||||
epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc)
|
||||
total = (epoch-last.date).total_seconds()
|
||||
dataset = dataset.filter(date__year=today.year, date__month=today.month)
|
||||
total = (last.created_at-epoch).total_seconds()
|
||||
dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month)
|
||||
ini = epoch
|
||||
for data in dataset:
|
||||
slot = (previous-data.date).total_seconds()
|
||||
slot = (data.created_at-ini).total_seconds()
|
||||
result += data.value * slot/total
|
||||
ini = data.created_at
|
||||
elif resource.period == resource.MONTHLY_SUM:
|
||||
dataset = dataset.filter(date__year=today.year, date__month=today.month)
|
||||
dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month)
|
||||
# FIXME Aggregation of 0s returns None! django bug?
|
||||
# value = dataset.aggregate(models.Sum('value'))['value__sum']
|
||||
values = dataset.values_list('value', flat=True)
|
||||
|
|
78
orchestra/apps/resources/migrations/0001_initial.py
Normal file
78
orchestra/apps/resources/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import orchestra.models.fields
|
||||
import django.core.validators
|
||||
|
||||
|
||||
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'Apache2Backend', 'Apache 2'), (b'Apache2Traffic', 'Apache 2 Traffic'), (b'AutoresponseBackend', 'Mail autoresponse'), (b'AwstatsBackend', 'Awstats'), (b'Bind9MasterDomainBackend', 'Bind9 master domain'), (b'Bind9SlaveDomainBackend', 'Bind9 slave domain'), (b'DokuWikiMuBackend', 'DokuWiki multisite'), (b'DrupalMuBackend', 'Drupal multisite'), (b'FTPTraffic', 'FTP traffic'), (b'MailSystemUserBackend', 'Mail system user'), (b'MaildirDisk', 'Maildir disk usage'), (b'MailmanBackend', b'Mailman'), (b'MailmanTraffic', b'MailmanTraffic'), (b'MySQLDBBackend', b'MySQL database'), (b'MySQLPermissionBackend', b'MySQL permission'), (b'MySQLUserBackend', b'MySQL user'), (b'MysqlDisk', 'MySQL disk'), (b'OpenVZTraffic', b'OpenVZTraffic'), (b'PHPFPMBackend', 'PHP-FPM'), (b'PHPFcgidBackend', 'PHP-Fcgid'), (b'PostfixAddressBackend', 'Postfix address'), (b'ServiceController', b'ServiceController'), (b'ServiceMonitor', b'ServiceMonitor'), (b'StaticBackend', 'Static'), (b'SystemUserBackend', 'System User'), (b'SystemUserDisk', 'System user disk'), (b'WebalizerBackend', 'Webalizer'), (b'WordpressMuBackend', 'Wordpress multisite')])),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('date', models.DateTimeField(auto_now_add=True, verbose_name='date')),
|
||||
('value', models.DecimalField(verbose_name='value', max_digits=16, decimal_places=2)),
|
||||
('content_type', models.ForeignKey(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=[django.core.validators.RegexValidator(b'^[a-z0-9_\\-]+$', 'Enter a valid name.', b'invalid')])),
|
||||
('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 monitoreddata.', max_length=16, verbose_name='period', choices=[(b'LAST', 'Last'), (b'MONTHLY_SUM', 'Monthly Sum'), (b'MONTHLY_AVG', 'Monthly Average')])),
|
||||
('ondemand', 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 measured. For example GB, KB or subscribers', max_length=16, verbose_name='unit')),
|
||||
('scale', models.PositiveIntegerField(help_text='Scale in which this resource monitoring resoults should be prorcessed to match with unit.', verbose_name='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'Apache2Backend', 'Apache 2'), (b'Apache2Traffic', 'Apache 2 Traffic'), (b'AutoresponseBackend', 'Mail autoresponse'), (b'AwstatsBackend', 'Awstats'), (b'Bind9MasterDomainBackend', 'Bind9 master domain'), (b'Bind9SlaveDomainBackend', 'Bind9 slave domain'), (b'DokuWikiMuBackend', 'DokuWiki multisite'), (b'DrupalMuBackend', 'Drupal multisite'), (b'FTPTraffic', 'FTP traffic'), (b'MailSystemUserBackend', 'Mail system user'), (b'MaildirDisk', 'Maildir disk usage'), (b'MailmanBackend', b'Mailman'), (b'MailmanTraffic', b'MailmanTraffic'), (b'MySQLDBBackend', b'MySQL database'), (b'MySQLPermissionBackend', b'MySQL permission'), (b'MySQLUserBackend', b'MySQL user'), (b'MysqlDisk', 'MySQL disk'), (b'OpenVZTraffic', b'OpenVZTraffic'), (b'PHPFPMBackend', 'PHP-FPM'), (b'PHPFcgidBackend', 'PHP-Fcgid'), (b'PostfixAddressBackend', 'Postfix address'), (b'ServiceController', b'ServiceController'), (b'ServiceMonitor', b'ServiceMonitor'), (b'StaticBackend', 'Static'), (b'SystemUserBackend', 'System User'), (b'SystemUserDisk', 'System user disk'), (b'WebalizerBackend', 'Webalizer'), (b'WordpressMuBackend', 'Wordpress multisite')])),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='is 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()),
|
||||
('used', models.PositiveIntegerField(null=True)),
|
||||
('last_update', models.DateTimeField(null=True)),
|
||||
('allocated', models.PositiveIntegerField(null=True, blank=True)),
|
||||
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||
('resource', models.ForeignKey(related_name=b'dataset', 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,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('resources', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='resource',
|
||||
old_name='ondemand',
|
||||
new_name='on_demand',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,70 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import datetime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('resources', '0002_auto_20140926_1143'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='monitordata',
|
||||
name='date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='resourcedata',
|
||||
name='last_update',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='monitordata',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=datetime.datetime(2014, 9, 26, 13, 25, 33, 290000), verbose_name='created', auto_now_add=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='resourcedata',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(null=True, verbose_name='updated'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='monitordata',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='monitordata',
|
||||
name='object_id',
|
||||
field=models.PositiveIntegerField(verbose_name='object id'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='resourcedata',
|
||||
name='allocated',
|
||||
field=models.PositiveIntegerField(null=True, verbose_name='allocated', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='resourcedata',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='resourcedata',
|
||||
name='object_id',
|
||||
field=models.PositiveIntegerField(verbose_name='object id'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='resourcedata',
|
||||
name='resource',
|
||||
field=models.ForeignKey(related_name=b'dataset', verbose_name='resource', to='resources.Resource'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='resourcedata',
|
||||
name='used',
|
||||
field=models.PositiveIntegerField(null=True, verbose_name='used'),
|
||||
),
|
||||
]
|
0
orchestra/apps/resources/migrations/__init__.py
Normal file
0
orchestra/apps/resources/migrations/__init__.py
Normal file
|
@ -43,8 +43,7 @@ class Resource(models.Model):
|
|||
default=LAST,
|
||||
help_text=_("Operation used for aggregating this resource monitored"
|
||||
"data."))
|
||||
# TODO rename to on_deman
|
||||
ondemand = models.BooleanField(_("on demand"), default=False,
|
||||
on_demand = models.BooleanField(_("on demand"), default=False,
|
||||
help_text=_("If enabled the resource will not be pre-allocated, "
|
||||
"but allocated under the application demand"))
|
||||
default_allocation = models.PositiveIntegerField(_("default allocation"),
|
||||
|
@ -116,12 +115,12 @@ class Resource(models.Model):
|
|||
|
||||
class ResourceData(models.Model):
|
||||
""" Stores computed resource usage and allocation """
|
||||
resource = models.ForeignKey(Resource, related_name='dataset')
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
used = models.PositiveIntegerField(null=True)
|
||||
last_update = models.DateTimeField(null=True)
|
||||
allocated = models.PositiveIntegerField(null=True, blank=True)
|
||||
resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
|
||||
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
|
||||
object_id = models.PositiveIntegerField(_("object id"))
|
||||
used = models.PositiveIntegerField(_("used"), null=True)
|
||||
updated_at = models.DateTimeField(_("updated"), null=True)
|
||||
allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True)
|
||||
|
||||
content_object = GenericForeignKey()
|
||||
|
||||
|
@ -146,7 +145,7 @@ class ResourceData(models.Model):
|
|||
if current is None:
|
||||
current = self.get_used()
|
||||
self.used = current or 0
|
||||
self.last_update = timezone.now()
|
||||
self.updated_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
|
||||
|
@ -154,9 +153,9 @@ class MonitorData(models.Model):
|
|||
""" Stores monitored data """
|
||||
monitor = models.CharField(_("monitor"), max_length=256,
|
||||
choices=ServiceMonitor.get_plugin_choices())
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
date = models.DateTimeField(_("date"), auto_now_add=True)
|
||||
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
|
||||
object_id = models.PositiveIntegerField(_("object id"))
|
||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
||||
|
||||
content_object = GenericForeignKey()
|
||||
|
|
|
@ -44,12 +44,12 @@ if not running_syncdb():
|
|||
msg = "Unknown or duplicated resource '%s'." % resource
|
||||
raise serializers.ValidationError(msg)
|
||||
resources.remove(resource)
|
||||
if not resource.ondemand and not data.allocated:
|
||||
if not resource.on_demand and not data.allocated:
|
||||
data.allocated = resource.default_allocation
|
||||
result.append(data)
|
||||
for resource in resources:
|
||||
data = ResourceData(resource=resource)
|
||||
if not resource.ondemand:
|
||||
if not resource.on_demand:
|
||||
data.allocated = resource.default_allocation
|
||||
result.append(data)
|
||||
attrs[source] = result
|
||||
|
@ -64,7 +64,7 @@ if not running_syncdb():
|
|||
ret['available_resources'] = [
|
||||
{
|
||||
'name': resource.name,
|
||||
'ondemand': resource.ondemand,
|
||||
'on_demand': resource.on_demand,
|
||||
'default_allocation': resource.default_allocation
|
||||
} for resource in resources
|
||||
]
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from celery import shared_task
|
||||
from django.db.models.loading import get_model
|
||||
from django.utils import timezone
|
||||
|
||||
from orchestra.apps.orchestration.models import BackendOperation as Operation
|
||||
|
||||
|
@ -34,7 +33,7 @@ def monitor(resource_id):
|
|||
operations.append(op)
|
||||
elif data.used < data.allocated:
|
||||
op = Operation.create(backend, obj, Operation.RECOVERY)
|
||||
operation.append(op)
|
||||
operations.append(op)
|
||||
# data = ResourceData.get_or_create(obj, resource)
|
||||
# current = data.get_used()
|
||||
# if not resource.disable_trigger:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
|
@ -7,8 +7,23 @@ from django.utils.translation import ugettext_lazy as _
|
|||
def update_orders(modeladmin, request, queryset):
|
||||
for service in queryset:
|
||||
service.update_orders()
|
||||
modeladmin.log_change(request, transaction, 'Update orders')
|
||||
modeladmin.log_change(request, service, 'Update orders')
|
||||
msg = _("Orders for %s selected services have been updated.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
update_orders.url_name = 'update-orders'
|
||||
update_orders.verbose_name = _("Update orders")
|
||||
|
||||
|
||||
def view_help(modeladmin, request, queryset):
|
||||
opts = modeladmin.model._meta
|
||||
context = {
|
||||
'title': _("Need some help?"),
|
||||
'opts': opts,
|
||||
'queryset': queryset,
|
||||
'obj': queryset.get(),
|
||||
'action_name': _("help"),
|
||||
'app_label': opts.app_label,
|
||||
}
|
||||
return TemplateResponse(request, 'admin/services/service/help.html', context)
|
||||
view_help.url_name = 'help'
|
||||
view_help.verbose_name = _("Help")
|
||||
|
|
|
@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter
|
|||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
from orchestra.core import services
|
||||
|
||||
from .actions import update_orders
|
||||
from .actions import update_orders, view_help
|
||||
from .models import Plan, ContractedPlan, Rate, Service
|
||||
|
||||
|
||||
|
@ -52,7 +52,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
|||
)
|
||||
inlines = [RateInline]
|
||||
actions = [update_orders]
|
||||
change_view_actions = actions
|
||||
change_view_actions = actions + [view_help]
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Improve performance of account field and filter by account """
|
||||
|
|
|
@ -4,12 +4,11 @@ import decimal
|
|||
|
||||
from dateutil import relativedelta
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.utils import plugins
|
||||
from orchestra.utils.python import AttributeDict
|
||||
from orchestra.utils.python import AttrDict
|
||||
|
||||
from . import settings, helpers
|
||||
|
||||
|
@ -21,6 +20,8 @@ class ServiceHandler(plugins.Plugin):
|
|||
|
||||
Relax and enjoy the journey.
|
||||
"""
|
||||
_VOLUME = 'VOLUME'
|
||||
_COMPENSATION = 'COMPENSATION'
|
||||
|
||||
model = None
|
||||
|
||||
|
@ -160,7 +161,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
return None
|
||||
|
||||
def generate_discount(self, line, dtype, price):
|
||||
line.discounts.append(AttributeDict(**{
|
||||
line.discounts.append(AttrDict(**{
|
||||
'type': dtype,
|
||||
'total': price,
|
||||
}))
|
||||
|
@ -182,7 +183,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
if not computed:
|
||||
price = price * size
|
||||
subtotal = self.nominal_price * size * metric
|
||||
line = AttributeDict(**{
|
||||
line = AttrDict(**{
|
||||
'order': order,
|
||||
'subtotal': subtotal,
|
||||
'ini': ini,
|
||||
|
@ -197,7 +198,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
discounted += dprice
|
||||
subtotal += discounted
|
||||
if subtotal > price:
|
||||
self.generate_discount(line, 'volume', price-subtotal)
|
||||
self.generate_discount(line, self._VOLUME, price-subtotal)
|
||||
return line
|
||||
|
||||
def assign_compensations(self, givers, receivers, **options):
|
||||
|
@ -225,7 +226,6 @@ class ServiceHandler(plugins.Plugin):
|
|||
|
||||
def apply_compensations(self, order, only_beyond=False):
|
||||
dsize = 0
|
||||
discounts = ()
|
||||
ini = order.billed_until or order.registered_on
|
||||
end = order.new_billed_until
|
||||
beyond = end
|
||||
|
@ -296,7 +296,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
cprice += dsize*price
|
||||
if cprice:
|
||||
discounts = (
|
||||
('compensation', -cprice),
|
||||
(self._COMPENSATION, -cprice),
|
||||
)
|
||||
if new_end:
|
||||
size = self.get_price_size(order.new_billed_until, new_end)
|
||||
|
@ -323,11 +323,12 @@ class ServiceHandler(plugins.Plugin):
|
|||
discounts = ()
|
||||
dsize, new_end = self.apply_compensations(order)
|
||||
if dsize:
|
||||
discounts=(('compensation', -dsize*price),)
|
||||
discounts=(
|
||||
(self._COMPENSATION, -dsize*price),
|
||||
)
|
||||
if new_end:
|
||||
order.new_billed_until = new_end
|
||||
end = new_end
|
||||
size = self.get_price_size(ini, end)
|
||||
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
@ -395,7 +396,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
dsize, new_end = self.apply_compensations(order)
|
||||
if dsize:
|
||||
discounts=(
|
||||
('compensation', -dsize*price),
|
||||
(self._COMPENSATION, -dsize*price),
|
||||
)
|
||||
if new_end:
|
||||
order.new_billed_until = new_end
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
import decimal
|
||||
import sys
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import Q
|
||||
from django.db.models.loading import get_model
|
||||
from django.db.models.signals import pre_delete, post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import ValidationError
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import caches, services, accounts
|
||||
from orchestra.models import queryset
|
||||
from orchestra.utils.apps import autodiscover
|
||||
from orchestra.utils.python import import_class
|
||||
|
||||
from . import helpers, settings, rating
|
||||
from . import settings, rating
|
||||
from .handlers import ServiceHandler
|
||||
|
||||
|
||||
|
@ -329,7 +323,7 @@ class Service(models.Model):
|
|||
def update_orders(self):
|
||||
order_model = get_model(settings.SERVICES_ORDER_MODEL)
|
||||
related_model = self.content_type.model_class()
|
||||
for instance in related_model.objects.all():
|
||||
for instance in related_model.objects.all().select_related('account__user'):
|
||||
order_model.update_orders(instance, service=self)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import sys
|
||||
|
||||
from orchestra.utils.python import AttributeDict
|
||||
from orchestra.utils.python import AttrDict
|
||||
|
||||
|
||||
def _compute(rates, metric):
|
||||
|
@ -30,7 +30,7 @@ def _compute(rates, metric):
|
|||
quantity = metric - accumulated
|
||||
end = True
|
||||
price = rates[ix].price
|
||||
steps.append(AttributeDict(**{
|
||||
steps.append(AttrDict(**{
|
||||
'quantity': quantity,
|
||||
'price': price,
|
||||
'barrier': barrier,
|
||||
|
@ -109,7 +109,7 @@ def step_price(rates, metric):
|
|||
if result and result[-1].price == price:
|
||||
result[-1].quantity += quantity
|
||||
else:
|
||||
result.append(AttributeDict(quantity=quantity, price=price))
|
||||
result.append(AttrDict(quantity=quantity, price=price))
|
||||
ix = 0
|
||||
targets = []
|
||||
else:
|
||||
|
@ -139,7 +139,7 @@ def match_price(rates, metric):
|
|||
candidates.append(prev)
|
||||
candidates.sort(key=lambda r: r.price)
|
||||
if candidates:
|
||||
return [AttributeDict(**{
|
||||
return [AttrDict(**{
|
||||
'quantity': metric,
|
||||
'price': candidates[0].price,
|
||||
})]
|
||||
|
|
BIN
orchestra/apps/services/static/services/img/services.png
Normal file
BIN
orchestra/apps/services/static/services/img/services.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
485
orchestra/apps/services/static/services/img/services.svg
Normal file
485
orchestra/apps/services/static/services/img/services.svg
Normal file
|
@ -0,0 +1,485 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1052.3622"
|
||||
height="744.09448"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
sodipodi:docname="services.svg"
|
||||
inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/apps/services/static/services/img/services.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.70710678"
|
||||
inkscape:cx="559.86324"
|
||||
inkscape:cy="278.12745"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-308.2677)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:FreeMono Bold"
|
||||
x="132.85733"
|
||||
y="526.45612"
|
||||
id="text2985"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2987"
|
||||
x="132.85733"
|
||||
y="526.45612">Orders</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="133.94112"
|
||||
y="853.0473"
|
||||
id="text2989"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2991"
|
||||
x="133.94112"
|
||||
y="853.0473"
|
||||
style="font-size:22px;text-align:center;line-height:94.99999880999999391%;text-anchor:middle">Metric</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.6925"
|
||||
y="431.67795"
|
||||
id="text2993"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2995"
|
||||
x="294.6925"
|
||||
y="431.67795">Periodic</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.6925"
|
||||
y="453.67804"
|
||||
id="tspan2997">billing</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.77344"
|
||||
y="597.10419"
|
||||
id="text2999"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3001"
|
||||
x="294.77344"
|
||||
y="597.10419">One-time</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.77344"
|
||||
y="619.10431"
|
||||
id="tspan3003">service</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="472.50183"
|
||||
id="text3005"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007"
|
||||
x="488.67383"
|
||||
y="472.50183">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="494.50192"
|
||||
id="tspan3009">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="390.854"
|
||||
id="text3011"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013"
|
||||
x="488.91711"
|
||||
y="390.854">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="412.8541"
|
||||
id="tspan3015">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.6925"
|
||||
y="758.26892"
|
||||
id="text2993-8"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2995-2"
|
||||
x="294.6925"
|
||||
y="758.26892">Periodic</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.6925"
|
||||
y="780.26904"
|
||||
id="tspan2997-4">billing</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="294.77344"
|
||||
y="923.69543"
|
||||
id="text2999-4"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3001-2"
|
||||
x="294.77344"
|
||||
y="923.69543">One-time</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="294.77344"
|
||||
y="945.69556"
|
||||
id="tspan3003-9">service</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="554.14972"
|
||||
id="text3005-7"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007-9"
|
||||
x="488.67383"
|
||||
y="554.14972">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="576.14984"
|
||||
id="tspan3009-3">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="635.79749"
|
||||
id="text3011-6"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013-2"
|
||||
x="488.91711"
|
||||
y="635.79749">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="657.79761"
|
||||
id="tspan3015-0">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="799.09296"
|
||||
id="text3005-6"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007-8"
|
||||
x="488.67383"
|
||||
y="799.09296">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="821.09308"
|
||||
id="tspan3009-2">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="717.44519"
|
||||
id="text3011-1"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013-6"
|
||||
x="488.91711"
|
||||
y="717.44519">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="739.44531"
|
||||
id="tspan3015-3">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.67383"
|
||||
y="962.38898"
|
||||
id="text3005-1"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3007-3"
|
||||
x="488.67383"
|
||||
y="962.38898">Pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.67383"
|
||||
y="984.3891"
|
||||
id="tspan3009-7">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:center;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans Bold"
|
||||
x="488.91711"
|
||||
y="880.74097"
|
||||
id="text3011-64"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3013-1"
|
||||
x="488.91711"
|
||||
y="880.74097">No pricing</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="488.91711"
|
||||
y="902.74109"
|
||||
id="tspan3015-08">period</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="583.38361"
|
||||
y="379.86563"
|
||||
id="text3898"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3900"
|
||||
x="583.38361"
|
||||
y="379.86563"
|
||||
style="font-weight:bold;font-size:22px">Mail accounts</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="583.38361"
|
||||
y="401.86572"
|
||||
id="tspan3912">Concurrent (changes)</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="583.38361"
|
||||
y="423.86581"
|
||||
id="tspan3902">Compensate on <tspan
|
||||
style="font-weight:bold;line-height:94.99999880999999391%;-inkscape-font-specification:FreeMono Bold;font-size:22px"
|
||||
id="tspan3904">prepay</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="586.6123"
|
||||
y="461.51324"
|
||||
id="text3906"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3908"
|
||||
x="586.6123"
|
||||
y="461.51324"
|
||||
style="font-weight:bold;font-size:22px">Domains</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="586.6123"
|
||||
y="483.51334"
|
||||
id="tspan3914">Register or renew events</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="586.6123"
|
||||
y="505.51343"
|
||||
id="tspan3910">Compensate on <tspan
|
||||
style="font-weight:bold;line-height:94.99999880999999391%;font-size:22px"
|
||||
id="tspan3993">prepay</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="590.04663"
|
||||
y="554.09149"
|
||||
id="text3916"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3918"
|
||||
x="590.04663"
|
||||
y="554.09149"
|
||||
style="font-weight:bold;font-size:22px">Plans</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.04663"
|
||||
y="576.09161"
|
||||
id="tspan3920">Always one order</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="590.58252"
|
||||
y="635.97125"
|
||||
id="text3922"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.58252"
|
||||
y="635.97125"
|
||||
id="tspan3926"
|
||||
style="font-weight:bold;font-size:22px">CMS installation</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.58252"
|
||||
y="657.97137"
|
||||
id="tspan3930">Register or renew events</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.58252"
|
||||
y="679.97144"
|
||||
id="tspan3932" /></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="591.32349"
|
||||
y="777.26685"
|
||||
id="text3934"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3936"
|
||||
x="591.32349"
|
||||
y="777.26685"
|
||||
style="font-weight:bold;font-size:22px">Traffic consumption</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="799.26697"
|
||||
id="tspan3938">Metric period lookup</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="821.26703"
|
||||
id="tspan3940">Prepay and != billing_period</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="843.26715"
|
||||
id="tspan3995"
|
||||
style="font-style:italic;-inkscape-font-specification:FreeMono Italic;font-size:22px"> NotImplemented</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="591.32349"
|
||||
y="717.61884"
|
||||
id="text3942"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3944"
|
||||
x="591.32349"
|
||||
y="717.61884"
|
||||
style="font-weight:bold;font-size:22px">Mailbox size</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="591.32349"
|
||||
y="739.61896"
|
||||
id="tspan3946">Concurrent (changes)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="590.1192"
|
||||
y="882.65179"
|
||||
id="text3942-8"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3944-2"
|
||||
x="590.1192"
|
||||
y="882.65179"
|
||||
style="font-weight:bold;font-size:22px">Jobs</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="590.1192"
|
||||
y="904.65192"
|
||||
id="tspan3946-6">Last known metric</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:94.99999880999999391%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeMono;-inkscape-font-specification:Sans"
|
||||
x="591.06866"
|
||||
y="973.33081"
|
||||
id="text3972"
|
||||
sodipodi:linespacing="94.999999%"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3974"
|
||||
x="591.06866"
|
||||
y="973.33081"
|
||||
style="font-style:italic;-inkscape-font-specification:FreeMono Italic;font-size:22px">NotImplement</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 228.73934,436.50836 -23.61913,0 0,164.75267 23.53877,0"
|
||||
id="path4013"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 205.55487,521.14184 -23.98356,0"
|
||||
id="path4019"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 228.73934,764.42561 -23.61913,0 0,164.7526 23.53877,0"
|
||||
id="path4013-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 205.55487,849.05914 -23.98356,0"
|
||||
id="path4019-1"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,398.68928 -23.61908,0 0,80.25672 23.5387,0"
|
||||
id="path4013-6"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,438.12728 -23.98362,0"
|
||||
id="path4019-18"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,561.72165 -23.61908,0 0,80.25664 23.5387,0"
|
||||
id="path4013-6-63"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,601.15965 -23.98362,0"
|
||||
id="path4019-18-4"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,726.60658 -23.61908,0 0,80.25672 23.5387,0"
|
||||
id="path4013-6-8"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,766.04458 -23.98362,0"
|
||||
id="path4019-18-3"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 407.81779,889.63886 -23.61908,0 0,80.25667 23.5387,0"
|
||||
id="path4013-6-0"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.92632013999999996px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 384.6333,929.07689 -23.98362,0"
|
||||
id="path4019-18-8"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,14 @@
|
|||
{% extends "admin/orchestra/generic_confirmation.html" %}
|
||||
{% load i18n l10n %}
|
||||
{% load url from future %}
|
||||
{% load admin_urls static utils %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<div style="margin:20px;">
|
||||
Enjoy my friend.
|
||||
<img src="{% static "services/img/services.png" %}"</img>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -41,7 +41,7 @@ class DomainBillingTest(BaseBillingTest):
|
|||
return account.miscellaneous.create(service=domain_service, description=domain_name)
|
||||
|
||||
def test_domain(self):
|
||||
service = self.create_domain_service()
|
||||
self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = account.orders.bill()
|
||||
|
@ -69,7 +69,7 @@ class DomainBillingTest(BaseBillingTest):
|
|||
self.assertEqual(56, bills[0].get_total())
|
||||
|
||||
def test_domain_proforma(self):
|
||||
service = self.create_domain_service()
|
||||
self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = account.orders.bill(proforma=True, new_open=True)
|
||||
|
@ -97,7 +97,7 @@ class DomainBillingTest(BaseBillingTest):
|
|||
self.assertEqual(56, bills[0].get_total())
|
||||
|
||||
def test_domain_cumulative(self):
|
||||
service = self.create_domain_service()
|
||||
self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = account.orders.bill(proforma=True)
|
||||
|
@ -110,7 +110,7 @@ class DomainBillingTest(BaseBillingTest):
|
|||
self.assertEqual(30, bills[0].get_total())
|
||||
|
||||
def test_domain_new_open(self):
|
||||
service = self.create_domain_service()
|
||||
self.create_domain_service()
|
||||
account = self.create_account()
|
||||
self.create_domain(account=account)
|
||||
bills = account.orders.bill(new_open=True)
|
||||
|
|
|
@ -43,14 +43,14 @@ class FTPBillingTest(BaseBillingTest):
|
|||
|
||||
def test_ftp_account_1_year_fiexed(self):
|
||||
service = self.create_ftp_service()
|
||||
user = self.create_ftp()
|
||||
self.create_ftp()
|
||||
bp = timezone.now().date() + relativedelta(years=1)
|
||||
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||
self.assertEqual(10, bills[0].get_total())
|
||||
|
||||
def test_ftp_account_2_year_fiexed(self):
|
||||
service = self.create_ftp_service()
|
||||
user = self.create_ftp()
|
||||
self.create_ftp()
|
||||
bp = timezone.now().date() + relativedelta(years=2)
|
||||
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||
self.assertEqual(20, bills[0].get_total())
|
||||
|
@ -79,7 +79,7 @@ class FTPBillingTest(BaseBillingTest):
|
|||
|
||||
def test_ftp_account_with_compensation(self):
|
||||
account = self.create_account()
|
||||
service = self.create_ftp_service()
|
||||
self.create_ftp_service()
|
||||
user = self.create_ftp(account=account)
|
||||
first_bp = timezone.now().date() + relativedelta(years=2)
|
||||
bills = account.orders.bill(billing_point=first_bp, fixed_point=True)
|
||||
|
|
|
@ -39,13 +39,13 @@ class JobBillingTest(BaseBillingTest):
|
|||
return account.miscellaneous.create(service=service, description=description, amount=amount)
|
||||
|
||||
def test_job(self):
|
||||
service = self.create_job_service()
|
||||
self.create_job_service()
|
||||
account = self.create_account()
|
||||
|
||||
job = self.create_job(5, account=account)
|
||||
self.create_job(5, account=account)
|
||||
bill = account.orders.bill()[0]
|
||||
self.assertEqual(5*20, bill.get_total())
|
||||
|
||||
job = self.create_job(100, account=account)
|
||||
self.create_job(100, account=account)
|
||||
bill = account.orders.bill(new_open=True)[0]
|
||||
self.assertEqual(100*15, bill.get_total())
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.utils import timezone
|
|||
from freezegun import freeze_time
|
||||
|
||||
from orchestra.apps.mails.models import Mailbox
|
||||
from orchestra.apps.resources.models import Resource, ResourceData, MonitorData
|
||||
from orchestra.apps.resources.models import Resource, ResourceData
|
||||
from orchestra.utils.tests import random_ascii
|
||||
|
||||
from ...models import Service, Plan
|
||||
|
@ -62,7 +62,7 @@ class MailboxBillingTest(BaseBillingTest):
|
|||
verbose_name='Mailbox disk',
|
||||
unit='GB',
|
||||
scale=10**9,
|
||||
ondemand=False,
|
||||
on_demand=False,
|
||||
monitors='MaildirDisk',
|
||||
)
|
||||
return self.resource
|
||||
|
|
|
@ -42,7 +42,7 @@ class PlanBillingTest(BaseBillingTest):
|
|||
|
||||
def test_plan(self):
|
||||
account = self.create_account()
|
||||
service = self.create_plan_service()
|
||||
self.create_plan_service()
|
||||
self.create_plan(account=account)
|
||||
bill = account.orders.bill().pop()
|
||||
self.assertEqual(bill.FEE, bill.type)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import datetime
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import timezone
|
||||
|
@ -46,26 +44,23 @@ class BaseTrafficBillingTest(BaseBillingTest):
|
|||
verbose_name='Account Traffic',
|
||||
unit='GB',
|
||||
scale=10**9,
|
||||
ondemand=True,
|
||||
on_demand=True,
|
||||
monitors='FTPTraffic',
|
||||
)
|
||||
return self.resource
|
||||
|
||||
def report_traffic(self, account, value):
|
||||
ct = ContentType.objects.get_for_model(Account)
|
||||
object_id = account.pk
|
||||
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user,
|
||||
value=value, date=timezone.now())
|
||||
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value)
|
||||
data = ResourceData.get_or_create(account, self.resource)
|
||||
data.update()
|
||||
|
||||
|
||||
class TrafficBillingTest(BaseTrafficBillingTest):
|
||||
def test_traffic(self):
|
||||
service = self.create_traffic_service()
|
||||
resource = self.create_traffic_resource()
|
||||
self.create_traffic_service()
|
||||
self.create_traffic_resource()
|
||||
account = self.create_account()
|
||||
now = timezone.now().date()
|
||||
now = timezone.now()
|
||||
|
||||
self.report_traffic(account, 10**9)
|
||||
bill = account.orders.bill(commit=False)[0]
|
||||
|
@ -82,8 +77,8 @@ class TrafficBillingTest(BaseTrafficBillingTest):
|
|||
self.assertEqual((90-10)*10, bill.get_total())
|
||||
|
||||
def test_multiple_traffics(self):
|
||||
service = self.create_traffic_service()
|
||||
resource = self.create_traffic_resource()
|
||||
self.create_traffic_service()
|
||||
self.create_traffic_resource()
|
||||
account1 = self.create_account()
|
||||
account2 = self.create_account()
|
||||
self.report_traffic(account1, 10**10)
|
||||
|
@ -129,13 +124,13 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
|
|||
return account.miscellaneous.create(service=service, description=name, amount=amount)
|
||||
|
||||
def test_traffic_prepay(self):
|
||||
service = self.create_traffic_service()
|
||||
prepay_service = self.create_prepay_service()
|
||||
self.create_traffic_service()
|
||||
self.create_prepay_service()
|
||||
account = self.create_account()
|
||||
self.create_traffic_resource()
|
||||
now = timezone.now()
|
||||
|
||||
prepay = self.create_prepay(10, account=account)
|
||||
self.create_prepay(10, account=account)
|
||||
bill = account.orders.bill(proforma=True)[0]
|
||||
self.assertEqual(10*50, bill.get_total())
|
||||
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import datetime
|
||||
import decimal
|
||||
import sys
|
||||
|
||||
from dateutil import relativedelta
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import timezone
|
||||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
from orchestra.apps.users.models import User
|
||||
from orchestra.utils.tests import BaseTestCase, random_ascii
|
||||
from orchestra.utils.tests import BaseTestCase
|
||||
|
||||
from .. import settings, helpers
|
||||
from ..models import Service, Plan, Rate
|
||||
from .. import helpers
|
||||
from ..models import Service, Plan
|
||||
|
||||
|
||||
class Order(object):
|
||||
|
|
|
@ -55,7 +55,6 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
|
|||
def get_urls(self):
|
||||
""" Returns the additional urls for the change view links """
|
||||
urls = super(UserAdmin, self).get_urls()
|
||||
admin_site = self.admin_site
|
||||
opts = self.model._meta
|
||||
new_urls = patterns("")
|
||||
for role in self.roles:
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from orchestra.api import router, SetPasswordApiMixin
|
||||
from orchestra.apps.accounts.api import AccountApiMixin
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.contrib.auth import models as auth
|
||||
from django.core import validators
|
||||
from django.core.mail import send_mail
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
from ..models import User
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.admin.util import unquote, get_deleted_objects
|
||||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import router
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.template.response import TemplateResponse
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin.utils import insertattr
|
||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import insertattr, admin_link
|
||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
||||
from orchestra.apps.domains.forms import DomainIterator
|
||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||
|
||||
from .forms import MailRoleAdminForm
|
||||
|
|
|
@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController
|
|||
from orchestra.apps.resources import ServiceMonitor
|
||||
|
||||
from . import settings
|
||||
from .models import Address
|
||||
|
||||
|
||||
class MailSystemUserBackend(ServiceController):
|
||||
|
@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor):
|
|||
)
|
||||
|
||||
def get_context(self, mailbox):
|
||||
context = MailSystemUserBackend().get_context(site)
|
||||
context = MailSystemUserBackend().get_context(mailbox)
|
||||
context['home'] = settings.EMAILS_HOME % context
|
||||
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
||||
context['object_id'] = mailbox.pk
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import re
|
||||
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ def validate_forward(value):
|
|||
|
||||
|
||||
def validate_sieve(value):
|
||||
from .models import Mailbox
|
||||
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
|
||||
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
|
||||
with open(path, 'wb') as f:
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin.utils import insertattr
|
||||
from orchestra.apps.users.roles.admin import RoleAdmin
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.conf.urls import patterns
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.resources import ServiceMonitor
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm, ReadOnlyPasswordHashField
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from orchestra.api import router
|
||||
from orchestra.apps.accounts.api import AccountApiMixin
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import os
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.apps.orchestration import ServiceController
|
||||
|
@ -21,7 +23,7 @@ class DokuWikiMuBackend(WebAppServiceMixin, ServiceController):
|
|||
self.append("rm -fr %(app_path)s" % context)
|
||||
|
||||
def get_context(self, webapp):
|
||||
context = super(DokuwikiMuBackend, self).get_context(webapp)
|
||||
context = super(DokuWikiMuBackend, self).get_context(webapp)
|
||||
context.update({
|
||||
'template': settings.WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH,
|
||||
'app_path': os.path.join(settings.WEBAPPS_DOKUWIKIMU_FARM_PATH, webapp.name)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import re
|
||||
import sys
|
||||
|
||||
import requests
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -55,6 +54,7 @@ class WordpressMuBackend(WebAppServiceMixin, ServiceController):
|
|||
'blog[email]': email,
|
||||
'_wpnonce_add-blog': wpnonce,
|
||||
}
|
||||
# TODO validate response
|
||||
response = session.post(url, data=data)
|
||||
|
||||
def delete_blog(self, webapp, server):
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import admin_link, change_url
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
|
||||
from orchestra.apps.accounts.widgets import account_related_field_widget_factory
|
||||
|
||||
from .models import Content, Website, WebsiteOption
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from orchestra.api import router
|
||||
from orchestra.apps.accounts.api import AccountApiMixin
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import textwrap
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.template import Template, Context
|
||||
from django.utils import timezone
|
||||
|
@ -123,6 +124,7 @@ class Apache2Backend(ServiceController):
|
|||
def get_protections(self, site):
|
||||
protections = ""
|
||||
__, regex = settings.WEBSITES_OPTIONS['directory_protection']
|
||||
context = self.get_context(site)
|
||||
for protection in site.options.filter(name='directory_protection'):
|
||||
path, name, passwd = re.match(regex, protection.value).groups()
|
||||
path = os.path.join(context['root'], path)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue