Major cleanup

This commit is contained in:
Marc 2014-09-26 15:05:20 +00:00
parent af323ebe25
commit 99b14f9c9f
111 changed files with 1279 additions and 328 deletions

19
TODO.md
View File

@ -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 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 * backend logs with hal logo
* Use logs for storing monitored values * 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 ) * 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?) * Transaction states: CREATED, PROCESSED, EXECUTED, COMMITED, ABORTED (SECURED, REJECTED?)
* bill.send() -> transacction.EXECUTED when source=None * 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() return order.register_at.date()
* latest by 'id' *always* * mail backend related_models = ('resources__content_type') ??
* replace add_now by default=lambda: timezone.now() * ignore orders
* Redmine, BSCW and other applications management

View File

@ -1,6 +1,5 @@
from functools import wraps, partial from functools import wraps, partial
from django.contrib import messages
from django.contrib.admin import helpers from django.contrib.admin import helpers
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import available_attrs from django.utils.decorators import available_attrs
@ -61,8 +60,10 @@ def action_with_confirmation(action_name=None, extra_context={},
if len(queryset) == 1: if len(queryset) == 1:
objects_name = force_text(opts.verbose_name) objects_name = force_text(opts.verbose_name)
obj = queryset.get()
else: else:
objects_name = force_text(opts.verbose_name_plural) objects_name = force_text(opts.verbose_name_plural)
obj = None
if not action_name: if not action_name:
action_name = func.__name__ action_name = func.__name__
context = { context = {
@ -73,6 +74,7 @@ def action_with_confirmation(action_name=None, extra_context={},
'action_value': action_value, 'action_value': action_value,
'queryset': queryset, 'queryset': queryset,
'opts': opts, 'opts': opts,
'obj': obj,
'app_label': app_label, 'app_label': app_label,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
} }

View File

@ -9,7 +9,6 @@ from django.shortcuts import redirect
from django.utils import importlib from django.utils import importlib
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe 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.models.utils import get_field_value
from orchestra.utils import humanize from orchestra.utils import humanize

View File

@ -6,7 +6,6 @@ from orchestra.utils.apps import autodiscover as module_autodiscover
from orchestra.utils.python import import_class from orchestra.utils.python import import_class
from .helpers import insert_links, replace_collectionmethodname from .helpers import insert_links, replace_collectionmethodname
from .root import APIRoot
def collectionlink(**kwargs): def collectionlink(**kwargs):

View File

@ -2,7 +2,6 @@ from django import forms
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.admin.util import unquote from django.contrib.admin.util import unquote
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.six.moves.urllib.parse import parse_qsl from django.utils.six.moves.urllib.parse import parse_qsl
@ -115,7 +114,6 @@ class AccountListAdmin(AccountAdmin):
select_account.order_admin_field = 'user__username' select_account.order_admin_field = 'user__username'
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
opts = self.model._meta
original_app_label = request.META['PATH_INFO'].split('/')[-5] original_app_label = request.META['PATH_INFO'].split('/')[-5]
original_model = request.META['PATH_INFO'].split('/')[-4] original_model = request.META['PATH_INFO'].split('/')[-4]
context = { context = {
@ -182,7 +180,6 @@ class AccountAdminMixin(object):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None): def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
account_id = self.get_account_from_preserve_filters(request) account_id = self.get_account_from_preserve_filters(request)
verb = 'change' if object_id else 'add'
if not object_id: if not object_id:
if account_id: if account_id:
# Preselect account # Preselect account

View File

@ -1,5 +1,4 @@
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -1,10 +1,9 @@
from optparse import make_option 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 django.db import transaction
from orchestra.apps.accounts.models import Account from orchestra.apps.accounts.models import Account
from orchestra.apps.users.models import User
class Command(BaseCommand): class Command(BaseCommand):
@ -29,5 +28,4 @@ class Command(BaseCommand):
username = options.get('username') username = options.get('username')
password = options.get('password') password = options.get('password')
account = Account.objects.create() account = Account.objects.create()
user = User.objects.create_superuser(username, email, password, account.users.create_superuser(username, email, password, is_main=True)
account=account, is_main=True)

View 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 = [
('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,
),
]

View File

@ -1,6 +1,5 @@
from django.conf import settings as djsettings from django.conf import settings as djsettings
from django.db import models from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services from orchestra.core import services
@ -18,7 +17,7 @@ class Account(models.Model):
language = models.CharField(_("language"), max_length=2, language = models.CharField(_("language"), max_length=2,
choices=settings.ACCOUNTS_LANGUAGES, choices=settings.ACCOUNTS_LANGUAGES,
default=settings.ACCOUNTS_DEFAULT_LANGUAGE) 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) comments = models.TextField(_("comments"), max_length=256, blank=True)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)

View File

@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin 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 orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings from . import settings
@ -53,8 +53,8 @@ class BillLineInline(admin.TabularInline):
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'number', 'is_open', 'type_link', 'account_link', 'created_on_display', 'number', 'type_link', 'account_link', 'created_on_display',
'num_lines', 'display_total', 'display_payment_state' 'num_lines', 'display_total', 'display_payment_state', 'is_open'
) )
list_filter = (BillTypeListFilter, 'is_open',) list_filter = (BillTypeListFilter, 'is_open',)
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments') add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')

View 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'},
),
]

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

View 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'),
),
]

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

View 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',
),
]

View File

@ -1,6 +1,6 @@
import inspect
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.template import loader, Context from django.template import loader, Context
from django.utils import timezone 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 django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.models import Account from orchestra.apps.accounts.models import Account
from orchestra.apps.contacts.models import Contact
from orchestra.core import accounts from orchestra.core import accounts
from orchestra.utils.functional import cached
from orchestra.utils.html import html_to_pdf from orchestra.utils.html import html_to_pdf
from . import settings from . import settings
@ -53,13 +53,12 @@ class Bill(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s') related_name='%(class)s')
type = models.CharField(_("type"), max_length=16, choices=TYPES) type = models.CharField(_("type"), max_length=16, choices=TYPES)
created_on = models.DateTimeField(_("created on"), auto_now_add=True) created_on = models.DateField(_("created on"), auto_now_add=True)
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True) closed_on = models.DateField(_("closed on"), blank=True, null=True)
# TODO rename to is_closed
is_open = models.BooleanField(_("is open"), default=True) is_open = models.BooleanField(_("is open"), default=True)
is_sent = models.BooleanField(_("is sent"), default=False) is_sent = models.BooleanField(_("is sent"), default=False)
due_on = models.DateField(_("due on"), null=True, blank=True) 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) total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
comments = models.TextField(_("comments"), blank=True) comments = models.TextField(_("comments"), blank=True)
html = models.TextField(_("HTML"), blank=True) html = models.TextField(_("HTML"), blank=True)
@ -145,7 +144,6 @@ class Bill(models.Model):
self.save() self.save()
def send(self): def send(self):
from orchestra.apps.contacts.models import Contact
self.account.send_email( self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE, template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={ context={
@ -241,9 +239,13 @@ class BillLine(models.Model):
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2) quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2) subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
tax = models.PositiveIntegerField(_("tax")) tax = models.PositiveIntegerField(_("tax"))
# TODO # Undo
# order_id = models.ForeignKey('orders.Order', null=True, blank=True, order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
# help_text=_("Informative link back to the order")) 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"), amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True) related_name='amendment_lines', null=True, blank=True)
@ -262,6 +264,17 @@ class BillLine(models.Model):
total += subline.total total += subline.total
return 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): def save(self, *args, **kwargs):
# TODO cost and consistency of this shit # TODO cost and consistency of this shit
super(BillLine, self).save(*args, **kwargs) super(BillLine, self).save(*args, **kwargs)
@ -272,10 +285,20 @@ class BillLine(models.Model):
class BillSubline(models.Model): class BillSubline(models.Model):
""" Subline used for describing an item discount """ """ 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') line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
description = models.CharField(_("description"), max_length=256) description = models.CharField(_("description"), max_length=256)
total = models.DecimalField(max_digits=12, decimal_places=2) 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): def save(self, *args, **kwargs):
# TODO cost of this shit # TODO cost of this shit

View File

@ -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_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
'bills/bill-notification.email') 'bills/bill-notification.email')
BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')

View File

@ -78,9 +78,9 @@
{% with sublines=line.sublines.all %} {% 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-id">{{ line.id }}</span>
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</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:"&nbsp;" }}</span> <span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.quantity|default:"&nbsp;" }}</span>
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span> <span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% 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> <br>
{% for subline in sublines %} {% for subline in sublines %}
<span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span>

View File

@ -1,5 +1,4 @@
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES', CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',

View File

@ -1,8 +1,6 @@
from django.db import models
from django.conf.urls import patterns from django.conf.urls import patterns
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -1,7 +1,4 @@
from rest_framework import viewsets 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.api import router, SetPasswordApiMixin
from orchestra.apps.accounts.api import AccountApiMixin from orchestra.apps.accounts.api import AccountApiMixin

View File

@ -77,21 +77,21 @@ class MysqlDisk(ServiceMonitor):
verbose_name = _("MySQL disk") verbose_name = _("MySQL disk")
def exceeded(self, db): def exceeded(self, db):
context = self.get_context(obj) context = self.get_context(db)
self.append("mysql -e '" self.append("mysql -e '"
"UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\"" "UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\""
" WHERE Db=\"%(db_name)s\";'" % context " WHERE Db=\"%(db_name)s\";'" % context
) )
def recovery(self, db): def recovery(self, db):
context = self.get_context(obj) context = self.get_context(db)
self.append("mysql -e '" self.append("mysql -e '"
"UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\"" "UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\""
" WHERE Db=\"%(db_name)s\";'" % context " WHERE Db=\"%(db_name)s\";'" % context
) )
def monitor(self, db): def monitor(self, db):
context = self.get_context(obj) context = self.get_context(db)
self.append( self.append(
"echo %(db_id)s $(mysql -B -e '" "echo %(db_id)s $(mysql -B -e '"
" SELECT sum( data_length + index_length ) \"Size\"\n" " SELECT sum( data_length + index_length ) \"Size\"\n"

View File

@ -1,5 +1,5 @@
from django import forms 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.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -109,7 +109,6 @@ class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
if 'Invalid' not in original: if 'Invalid' not in original:
return original return original
encoded = value encoded = value
final_attrs = self.build_attrs(attrs)
if not encoded: if not encoded:
summary = mark_safe("<strong>%s</strong>" % _("No password set.")) summary = mark_safe("<strong>%s</strong>" % _("No password set."))
else: else:

View 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")

View File

@ -1,17 +1,13 @@
from django import forms from django import forms
from django.conf.urls import patterns, url
from django.contrib import admin 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 django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin 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.apps.accounts.admin import AccountAdminMixin
from orchestra.utils import apps from orchestra.utils import apps
from .actions import view_zone
from .forms import RecordInlineFormSet, DomainAdminForm from .forms import RecordInlineFormSet, DomainAdminForm
from .filters import TopDomainListFilter from .filters import TopDomainListFilter
from .models import Domain, Record from .models import Domain, Record
@ -61,6 +57,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
('top_domain', 'True'), ('top_domain', 'True'),
) )
form = DomainAdminForm form = DomainAdminForm
change_view_actions = [view_zone]
def structured_name(self, domain): def structured_name(self, domain):
if not domain.is_top: if not domain.is_top:
@ -89,35 +86,12 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
websites.short_description = _("Websites") websites.short_description = _("Websites")
websites.allow_tags = True 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): def get_queryset(self, request):
""" Order by structured name and imporve performance """ """ Order by structured name and imporve performance """
qs = super(DomainAdmin, self).get_queryset(request) qs = super(DomainAdmin, self).get_queryset(request)
qs = qs.select_related('top') 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 # 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( qs = qs.extra(
select={'structured_name': 'CONCAT(T4.name, domains_domain.name)'}, select={'structured_name': 'CONCAT(T4.name, domains_domain.name)'},
).order_by('structured_name') ).order_by('structured_name')

View File

@ -1,5 +1,3 @@
import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from . import settings from . import settings

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

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

View File

@ -1,11 +1,11 @@
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services from orchestra.core import services
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address, from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address,
validate_hostname, validate_ascii) validate_hostname, validate_ascii)
from orchestra.utils.python import AttrDict
from . import settings, validators, utils from . import settings, validators, utils
@ -69,13 +69,17 @@ class Domain(models.Model):
# Update serial and insert at 0 # Update serial and insert at 0
value = record.value.split() value = record.value.split()
value[2] = str(self.serial) 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: 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 not self.top:
if Record.NS not in types: if Record.NS not in types:
for ns in settings.DOMAINS_DEFAULT_NS: 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: if Record.SOA not in types:
soa = [ soa = [
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER, "%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
@ -86,18 +90,28 @@ class Domain(models.Model):
settings.DOMAINS_DEFAULT_EXPIRATION, settings.DOMAINS_DEFAULT_EXPIRATION,
settings.DOMAINS_DEFAULT_MIN_CACHING_TIME 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 no_cname = Record.CNAME not in types
if Record.MX not in types and no_cname: if Record.MX not in types and no_cname:
for mx in settings.DOMAINS_DEFAULT_MX: 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: 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 = '' result = ''
for type, value in records: for record in records:
name = '%s.%s' % (self.name, ' '*(37-len(self.name))) name = '{name}.{spaces}'.format(
type = '%s %s' % (type, ' '*(7-len(type))) name=self.name, spaces=' ' * (37-len(self.name))
result += '%s IN %s %s\n' % (name, type, value) )
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 return result
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -150,13 +164,15 @@ class Record(models.Model):
(SOA, "SOA"), (SOA, "SOA"),
) )
# TODO TTL
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records') domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
type = models.CharField(max_length=32, choices=TYPE_CHOICES) ttl = models.CharField(_("TTL"), max_length=8, blank=True,
value = models.CharField(max_length=256) 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): 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): def clean(self):
""" validates record value based on its type """ """ validates record value based on its type """
@ -173,5 +189,7 @@ class Record(models.Model):
} }
mapp[self.type](self.value) mapp[self.type](self.value)
def get_ttl(self):
return self.ttl or settings.DOMAINS_DEFAULT_TTL
services.register(Domain) services.register(Domain)

View File

@ -124,7 +124,7 @@ class DomainTestMixin(object):
self.assertNotEqual(hostmaster, soa[5]) self.assertNotEqual(hostmaster, soa[5])
def validate_update(self, server_addr, domain_name): def validate_update(self, server_addr, domain_name):
domain = Domain.objects.get(name=domain_name) Domain.objects.get(name=domain_name)
context = { context = {
'domain_name': domain_name, 'domain_name': domain_name,
'server_addr': server_addr 'server_addr': server_addr

View File

@ -1,4 +1,3 @@
from django.db import IntegrityError, transaction
from django.test import TestCase from django.test import TestCase
from ..models import Domain from ..models import Domain

View File

@ -56,7 +56,7 @@ class MessageReadOnlyInline(admin.TabularInline):
def content_html(self, msg): def content_html(self, msg):
context = { context = {
'number': msg.number, '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, 'author': admin_link('author')(msg) if msg.author else msg.author_name,
} }
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
@ -98,11 +98,11 @@ class MessageInline(admin.TabularInline):
class TicketInline(admin.TabularInline): class TicketInline(admin.TabularInline):
fields = [ fields = [
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state', 'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
'colored_priority', 'created', 'last_modified' 'colored_priority', 'created', 'updated'
] ]
readonly_fields = [ readonly_fields = [
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state', 'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
'colored_priority', 'created', 'last_modified' 'colored_priority', 'created', 'updated'
] ]
model = Ticket model = Ticket
extra = 0 extra = 0
@ -110,8 +110,8 @@ class TicketInline(admin.TabularInline):
creator_link = admin_link('creator') creator_link = admin_link('creator')
owner_link = admin_link('owner') owner_link = admin_link('owner')
created = admin_link('created_on') created = admin_link('created_at')
last_modified = admin_link('last_modified_on') updated = admin_link('updated_at')
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False) colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
colored_priority = admin_colored('priority', colors=PRIORITY_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 ticket_id.allow_tags = True
class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions, class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
list_display = [ list_display = [
'unbold_id', 'bold_subject', 'display_creator', 'display_owner', '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_display_links = ('unbold_id', 'bold_subject')
list_filter = [ 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'), ('my_tickets', lambda r: 'True' if not r.user.is_superuser else 'False'),
('state', 'OPEN') ('state', 'OPEN')
) )
date_hierarchy = 'created_on' date_hierarchy = 'created_at'
search_fields = [ search_fields = [
'id', 'subject', 'creator__username', 'creator__email', 'queue__name', 'id', 'subject', 'creator__username', 'creator__email', 'queue__name',
'owner__username' 'owner__username'
@ -192,20 +192,20 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
display_creator = admin_link('creator') display_creator = admin_link('creator')
display_queue = admin_link('queue') display_queue = admin_link('queue')
display_owner = admin_link('owner') 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_state = admin_colored('state', colors=STATE_COLORS, bold=False)
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False) display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
def display_summary(self, ticket): def display_summary(self, ticket):
context = { context = {
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name, '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': '', 'updated': '',
} }
msg = ticket.messages.last() msg = ticket.messages.last()
if msg: if msg:
context.update({ 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, 'updater': admin_link('author')(self, msg) if msg.author else msg.author_name,
}) })
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context 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): def message_preview_view(self, request):
""" markdown preview render via ajax """ """ markdown preview render via ajax """
data = request.POST.get("data") data = request.POST.get("data")
data_formated = markdowt_tn(strip_tags(data)) data_formated = markdown(strip_tags(data))
return HttpResponse(data_formated) return HttpResponse(data_formated)
def get_queryset(self, request): def get_queryset(self, request):

View File

@ -1,11 +1,9 @@
from django import forms from django import forms
from django.core.urlresolvers import reverse
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from markdown import markdown from markdown import markdown
from orchestra.admin.utils import change_url
from orchestra.apps.users.models import User from orchestra.apps.users.models import User
from orchestra.forms.widgets import ReadOnlyWidget from orchestra.forms.widgets import ReadOnlyWidget
@ -42,7 +40,6 @@ class MessageInlineForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MessageInlineForm, self).__init__(*args, **kwargs) super(MessageInlineForm, self).__init__(*args, **kwargs)
admin_link = change_url(self.user)
self.fields['created_on'].widget = ReadOnlyWidget('') self.fields['created_on'].widget = ReadOnlyWidget('')
def clean_content(self): def clean_content(self):

View File

@ -1,6 +1,5 @@
from django.conf import settings as djsettings from django.conf import settings as djsettings
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.contacts import settings as contacts_settings 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, priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES,
default=MEDIUM) default=MEDIUM)
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW) state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
created_on = models.DateTimeField(_("created on"), auto_now_add=True) created_at = models.DateTimeField(_("created"), auto_now_add=True)
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True) updated_at = models.DateTimeField(_("modified"), auto_now=True)
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"),
blank=True) blank=True)
class Meta: class Meta:
ordering = ["-last_modified_on"] ordering = ['-updated_at']
def __unicode__(self): def __unicode__(self):
return unicode(self.pk) return unicode(self.pk)

View File

@ -1,8 +1,6 @@
import textwrap import textwrap
from django.template import Template, Context
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor from orchestra.apps.resources import ServiceMonitor

View File

@ -1,15 +1,12 @@
from django import forms from django import forms
from django.contrib import admin 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.core.urlresolvers import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin 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.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
from orchestra.apps.domains.forms import DomainIterator
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
from .models import Mailbox, Address, Autoresponse from .models import Mailbox, Address, Autoresponse

View File

@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor from orchestra.apps.resources import ServiceMonitor
from . import settings from . import settings
from .models import Address
class MailSystemUserBackend(ServiceController): class MailSystemUserBackend(ServiceController):
@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor):
) )
def get_context(self, mailbox): def get_context(self, mailbox):
context = MailSystemUserBackend().get_context(site) context = MailSystemUserBackend().get_context(mailbox)
context['home'] = settings.EMAILS_HOME % context context['home'] = settings.EMAILS_HOME % context
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize') context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
context['object_id'] = mailbox.pk context['object_id'] = mailbox.pk

View File

@ -1,6 +1,3 @@
import re
from django.contrib.auth.hashers import check_password, make_password
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -44,7 +44,6 @@ def validate_forward(value):
def validate_sieve(value): def validate_sieve(value):
from .models import Mailbox
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest() sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name) path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
with open(path, 'wb') as f: with open(path, 'wb') as f:

View File

@ -1,5 +1,4 @@
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -89,18 +88,18 @@ class BackendLogAdmin(admin.ModelAdmin):
) )
list_display_links = ('id', 'backend') list_display_links = ('id', 'backend')
list_filter = ('state', 'backend') list_filter = ('state', 'backend')
date_hierarchy = 'last_update' date_hierarchy = 'updated_at'
inlines = [BackendOperationInline] inlines = [BackendOperationInline]
fields = [ fields = [
'backend', 'server_link', 'state', 'mono_script', 'mono_stdout', 'backend', 'server_link', 'state', 'mono_script', 'mono_stdout',
'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created', 'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created',
'display_last_update', 'execution_time' 'display_updated', 'execution_time'
] ]
readonly_fields = fields readonly_fields = fields
server_link = admin_link('server') server_link = admin_link('server')
display_last_update = admin_date('last_update') display_updated = admin_date('updated_at')
display_created = admin_date('created') display_created = admin_date('created_at')
display_state = admin_colored('state', colors=STATE_COLORS) display_state = admin_colored('state', colors=STATE_COLORS)
mono_script = display_mono('script') mono_script = display_mono('script')
mono_stdout = display_mono('stdout') mono_stdout = display_mono('stdout')
@ -112,6 +111,9 @@ class BackendLogAdmin(admin.ModelAdmin):
qs = super(BackendLogAdmin, self).get_queryset(request) qs = super(BackendLogAdmin, self).get_queryset(request)
return qs.select_related('server').defer('script', 'stdout') return qs.select_related('server').defer('script', 'stdout')
def has_add_permission(self, *args, **kwargs):
return False
class ServerAdmin(admin.ModelAdmin): class ServerAdmin(admin.ModelAdmin):
list_display = ('name', 'address', 'os') list_display = ('name', 'address', 'os')

View File

@ -1,7 +1,6 @@
from functools import partial from functools import partial
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins from orchestra.utils import plugins

View File

@ -1,7 +1,5 @@
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -62,8 +60,8 @@ class BackendLog(models.Model):
exit_code = models.IntegerField(_("exit code"), null=True) exit_code = models.IntegerField(_("exit code"), null=True)
task_id = models.CharField(_("task ID"), max_length=36, unique=True, 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") help_text="Celery task ID when used as execution backend")
created = models.DateTimeField(_("created"), auto_now_add=True) created_at = models.DateTimeField(_("created"), auto_now_add=True)
last_update = models.DateTimeField(_("last update"), auto_now=True) updated_at = models.DateTimeField(_("updated"), auto_now=True)
class Meta: class Meta:
get_latest_by = 'id' get_latest_by = 'id'
@ -89,7 +87,7 @@ class BackendOperation(models.Model):
action = models.CharField(_("action"), max_length=64) action = models.CharField(_("action"), max_length=64)
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
# TODO rename to content_object
instance = generic.GenericForeignKey('content_type', 'object_id') instance = generic.GenericForeignKey('content_type', 'object_id')
class Meta: class Meta:

View File

@ -1,5 +1,3 @@
from django.db import IntegrityError, transaction
from orchestra.utils.tests import BaseTestCase from orchestra.utils.tests import BaseTestCase
from .. import operations, backends from .. import operations, backends

View File

@ -78,7 +78,7 @@ class BillSelectedOrders(object):
if int(request.POST.get('step')) >= 3: if int(request.POST.get('step')) >= 3:
bills = self.queryset.bill(commit=True, **self.options) bills = self.queryset.bill(commit=True, **self.options)
for order in self.queryset: for order in self.queryset:
modeladmin.log_change(request, order, 'Billed') self.modeladmin.log_change(request, order, 'Billed')
if not bills: if not bills:
msg = _("Selected orders do not have pending billing") msg = _("Selected orders do not have pending billing")
self.modeladmin.message_user(request, msg, messages.WARNING) self.modeladmin.message_user(request, msg, messages.WARNING)

View File

@ -3,7 +3,6 @@ from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeListDefaultFilter
from orchestra.admin.utils import admin_link, admin_date from orchestra.admin.utils import admin_link, admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.utils.humanize import naturaldate from orchestra.utils.humanize import naturaldate
@ -13,7 +12,7 @@ from .filters import ActiveOrderListFilter, BilledOrderListFilter
from .models import Order, MetricStorage from .models import Order, MetricStorage
class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin): class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ( list_display = (
'id', 'service', 'account_link', 'content_object_link', 'id', 'service', 'account_link', 'content_object_link',
'display_registered_on', 'display_billed_until', 'display_cancelled_on' 'display_registered_on', 'display_billed_until', 'display_cancelled_on'
@ -22,9 +21,6 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',) list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',)
actions = (BillSelectedOrders(),) actions = (BillSelectedOrders(),)
date_hierarchy = 'registered_on' date_hierarchy = 'registered_on'
default_changelist_filters = (
('is_active', 'True'),
)
content_object_link = admin_link('content_object', order=False) content_object_link = admin_link('content_object', order=False)
display_registered_on = admin_date('registered_on') display_registered_on = admin_date('registered_on')

View File

@ -2,7 +2,7 @@ import datetime
from django.utils.translation import ugettext_lazy as _ 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): class BillsBackend(object):
@ -39,6 +39,10 @@ class BillsBackend(object):
subtotal=line.subtotal, subtotal=line.subtotal,
tax=service.tax, tax=service.tax,
description=self.get_line_description(line), 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) self.create_sublines(billine, line.discounts)
return bills return bills
@ -46,7 +50,6 @@ class BillsBackend(object):
def format_period(self, ini, end): def format_period(self, ini, end):
ini = ini.strftime("%b, %Y") ini = ini.strftime("%b, %Y")
end = (end-datetime.timedelta(seconds=1)).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: if ini == end:
return ini return ini
return _("{ini} to {end}").format(ini=ini, end=end) return _("{ini} to {end}").format(ini=ini, end=end)
@ -67,6 +70,7 @@ class BillsBackend(object):
def create_sublines(self, line, discounts): def create_sublines(self, line, discounts):
for discount in discounts: for discount in discounts:
line.sublines.create( line.sublines.create(
description=_("Discount per %s") % discount.type, description=_("Discount per %s") % discount.type.lower(),
total=discount.total, total=discount.total,
type=discount.type,
) )

View File

@ -13,7 +13,6 @@ class ActiveOrderListFilter(SimpleListFilter):
return ( return (
('True', _("Active")), ('True', _("Active")),
('False', _("Inactive")), ('False', _("Inactive")),
('None', _("All")),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
@ -23,12 +22,6 @@ class ActiveOrderListFilter(SimpleListFilter):
return queryset.inactive() return queryset.inactive()
return queryset return queryset
def choices(self, cl):
""" Remove default All """
choices = iter(super(ActiveOrderListFilter, self).choices(cl))
choices.next()
return choices
class BilledOrderListFilter(SimpleListFilter): class BilledOrderListFilter(SimpleListFilter):
""" Filter tickets by created_by according to request.user """ """ Filter tickets by created_by according to request.user """

View File

@ -1,28 +1,24 @@
import datetime import datetime
import decimal import decimal
import logging import logging
import sys
from django.db import models from django.db import models
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models.loading import get_model 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.dispatch import receiver
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ 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.models import queryset
from orchestra.utils.apps import autodiscover
from orchestra.utils.python import import_class from orchestra.utils.python import import_class
from . import helpers, settings from . import helpers, settings
from .handlers import ServiceHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -39,8 +35,13 @@ class OrderQuerySet(models.QuerySet):
for account, services in qs.group_by('account', 'service').iteritems(): for account, services in qs.group_by('account', 'service').iteritems():
bill_lines = [] bill_lines = []
for service, orders in services.iteritems(): 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) lines = service.handler.generate_bill_lines(orders, account, **options)
bill_lines.extend(lines) bill_lines.extend(lines)
# TODO make this consistent always returning the same fucking objects
if commit: if commit:
bills += bill_backend.create_bills(account, bill_lines, **options) bills += bill_backend.create_bills(account, bill_lines, **options)
else: else:
@ -73,7 +74,7 @@ class OrderQuerySet(models.QuerySet):
def inactive(self, **kwargs): def inactive(self, **kwargs):
""" return inactive orders """ """ 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): class Order(models.Model):

View File

@ -1,5 +1,4 @@
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND', ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',

View File

@ -26,8 +26,8 @@ def process_transactions(modeladmin, request, queryset):
method = PaymentMethod.get_plugin(method) method = PaymentMethod.get_plugin(method)
procs = method.process(transactions) procs = method.process(transactions)
processes += procs processes += procs
for transaction in transactions: for trans in transactions:
modeladmin.log_change(request, transaction, 'Processed') modeladmin.log_change(request, trans, 'Processed')
if not processes: if not processes:
return return
opts = modeladmin.model._meta opts = modeladmin.model._meta
@ -44,9 +44,9 @@ def process_transactions(modeladmin, request, queryset):
@transaction.atomic @transaction.atomic
@action_with_confirmation() @action_with_confirmation()
def mark_as_executed(modeladmin, request, queryset, extra_context={}): def mark_as_executed(modeladmin, request, queryset, extra_context={}):
for transaction in queryset: for trans in queryset:
transaction.mark_as_executed() trans.mark_as_executed()
modeladmin.log_change(request, transaction, 'Executed') modeladmin.log_change(request, trans, 'Executed')
msg = _("%s selected transactions have been marked as executed.") % queryset.count() msg = _("%s selected transactions have been marked as executed.") % queryset.count()
modeladmin.message_user(request, msg) modeladmin.message_user(request, msg)
mark_as_executed.url_name = 'execute' mark_as_executed.url_name = 'execute'
@ -56,9 +56,9 @@ mark_as_executed.verbose_name = _("Mark as executed")
@transaction.atomic @transaction.atomic
@action_with_confirmation() @action_with_confirmation()
def mark_as_secured(modeladmin, request, queryset): def mark_as_secured(modeladmin, request, queryset):
for transaction in queryset: for trans in queryset:
transaction.mark_as_secured() trans.mark_as_secured()
modeladmin.log_change(request, transaction, 'Secured') modeladmin.log_change(request, trans, 'Secured')
msg = _("%s selected transactions have been marked as secured.") % queryset.count() msg = _("%s selected transactions have been marked as secured.") % queryset.count()
modeladmin.message_user(request, msg) modeladmin.message_user(request, msg)
mark_as_secured.url_name = 'secure' mark_as_secured.url_name = 'secure'
@ -68,9 +68,9 @@ mark_as_secured.verbose_name = _("Mark as secured")
@transaction.atomic @transaction.atomic
@action_with_confirmation() @action_with_confirmation()
def mark_as_rejected(modeladmin, request, queryset): def mark_as_rejected(modeladmin, request, queryset):
for transaction in queryset: for trans in queryset:
transaction.mark_as_rejected() trans.mark_as_rejected()
modeladmin.log_change(request, transaction, 'Rejected') modeladmin.log_change(request, trans, 'Rejected')
msg = _("%s selected transactions have been marked as rejected.") % queryset.count() msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
modeladmin.message_user(request, msg) modeladmin.message_user(request, msg)
mark_as_rejected.url_name = 'reject' 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)(): for related in getattr(obj.transactions, attr)():
subobjects.append( subobjects.append(
mark_safe('{0}: <a href="{1}">{2}</a> will be marked as {3}'.format( 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) objects.append(subobjects)
return {'display_objects': objects} return {'display_objects': objects}
@ -127,9 +127,9 @@ abort.verbose_name = _("Abort")
@transaction.atomic @transaction.atomic
@action_with_confirmation(extra_context=_format_commit) @action_with_confirmation(extra_context=_format_commit)
def commit(modeladmin, request, queryset): def commit(modeladmin, request, queryset):
for transaction in queryset: for trans in queryset:
transaction.mark_as_rejected() trans.mark_as_rejected()
modeladmin.log_change(request, transaction, 'Rejected') modeladmin.log_change(request, trans, 'Rejected')
msg = _("%s selected transactions have been marked as rejected.") % queryset.count() msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
modeladmin.message_user(request, msg) modeladmin.message_user(request, msg)
commit.url_name = 'commit' commit.url_name = 'commit'

View File

@ -1,4 +1,3 @@
from django import forms
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -37,7 +36,6 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
def get_urls(self): def get_urls(self):
""" Hooks select account url """ """ Hooks select account url """
urls = super(PaymentSourceAdmin, self).get_urls() urls = super(PaymentSourceAdmin, self).get_urls()
admin_site = self.admin_site
opts = self.model._meta opts = self.model._meta
info = opts.app_label, opts.model_name info = opts.app_label, opts.model_name
select_urls = patterns("", select_urls = patterns("",

View File

@ -1,6 +1,5 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField from jsonfield import JSONField
@ -99,8 +98,8 @@ class Transaction(models.Model):
default=WAITTING_PROCESSING) default=WAITTING_PROCESSING)
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2) amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY) currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
created_on = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(_("created"), auto_now_add=True)
modified_on = models.DateTimeField(auto_now=True) modified_at = models.DateTimeField(_("modified"), auto_now=True)
objects = TransactionQuerySet.as_manager() objects = TransactionQuerySet.as_manager()

View File

@ -15,16 +15,16 @@ from .models import Resource, ResourceData, MonitorData
class ResourceAdmin(ExtendedModelAdmin): class ResourceAdmin(ExtendedModelAdmin):
list_display = ( list_display = (
'id', 'verbose_name', 'content_type', 'period', 'ondemand', 'id', 'verbose_name', 'content_type', 'period', 'on_demand',
'default_allocation', 'unit', 'disable_trigger', 'crontab', 'default_allocation', 'unit', 'disable_trigger', 'crontab',
) )
list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger') list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger')
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('name', 'content_type', 'period'), 'fields': ('name', 'content_type', 'period'),
}), }),
(_("Configuration"), { (_("Configuration"), {
'fields': ('verbose_name', 'unit', 'scale', 'ondemand', 'fields': ('verbose_name', 'unit', 'scale', 'on_demand',
'default_allocation', 'disable_trigger', 'is_active'), 'default_allocation', 'disable_trigger', 'is_active'),
}), }),
(_("Monitoring"), { (_("Monitoring"), {
@ -64,7 +64,7 @@ class ResourceAdmin(ExtendedModelAdmin):
class ResourceDataAdmin(admin.ModelAdmin): class ResourceDataAdmin(admin.ModelAdmin):
list_display = ( list_display = (
'id', 'resource', 'used', 'allocated', 'last_update', 'content_object_link' 'id', 'resource', 'used', 'allocated', 'updated_at', 'content_object_link'
) )
list_filter = ('resource',) list_filter = ('resource',)
readonly_fields = ('content_object_link',) readonly_fields = ('content_object_link',)
@ -77,7 +77,7 @@ class ResourceDataAdmin(admin.ModelAdmin):
class MonitorDataAdmin(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',) list_filter = ('monitor',)
readonly_fields = ('content_object_link',) readonly_fields = ('content_object_link',)
@ -118,16 +118,16 @@ def resource_inline_factory(resources):
formset = ResourceInlineFormSet formset = ResourceInlineFormSet
can_delete = False can_delete = False
fields = ( 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: class Media:
css = { css = {
'all': ('orchestra/css/hide-inline-id.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): def has_add_permission(self, *args, **kwargs):
""" Hidde add another """ """ Hidde add another """

View File

@ -1,5 +1,4 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.contrib.contenttypes import generic
from orchestra.utils import running_syncdb from orchestra.utils import running_syncdb

View File

@ -1,7 +1,5 @@
from django import forms from django import forms
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from djcelery.humanize import naturaldate
from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
@ -21,7 +19,7 @@ class ResourceForm(forms.ModelForm):
if self.resource: if self.resource:
self.fields['verbose_name'].initial = self.resource.verbose_name self.fields['verbose_name'].initial = self.resource.verbose_name
self.fields['unit'].initial = self.resource.unit self.fields['unit'].initial = self.resource.unit
if self.resource.ondemand: if self.resource.on_demand:
self.fields['allocated'].required = False self.fields['allocated'].required = False
self.fields['allocated'].widget = ReadOnlyWidget(None, '') self.fields['allocated'].widget = ReadOnlyWidget(None, '')
else: else:

View File

@ -38,13 +38,15 @@ def compute_resource_usage(data):
continue continue
has_result = True has_result = True
epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc) epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc)
total = (epoch-last.date).total_seconds() total = (last.created_at-epoch).total_seconds()
dataset = dataset.filter(date__year=today.year, date__month=today.month) dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month)
ini = epoch
for data in dataset: for data in dataset:
slot = (previous-data.date).total_seconds() slot = (data.created_at-ini).total_seconds()
result += data.value * slot/total result += data.value * slot/total
ini = data.created_at
elif resource.period == resource.MONTHLY_SUM: 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? # FIXME Aggregation of 0s returns None! django bug?
# value = dataset.aggregate(models.Sum('value'))['value__sum'] # value = dataset.aggregate(models.Sum('value'))['value__sum']
values = dataset.values_list('value', flat=True) values = dataset.values_list('value', flat=True)

View 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')]),
),
]

View File

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

View File

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

View File

@ -43,8 +43,7 @@ class Resource(models.Model):
default=LAST, default=LAST,
help_text=_("Operation used for aggregating this resource monitored" help_text=_("Operation used for aggregating this resource monitored"
"data.")) "data."))
# TODO rename to on_deman on_demand = models.BooleanField(_("on demand"), default=False,
ondemand = models.BooleanField(_("on demand"), default=False,
help_text=_("If enabled the resource will not be pre-allocated, " help_text=_("If enabled the resource will not be pre-allocated, "
"but allocated under the application demand")) "but allocated under the application demand"))
default_allocation = models.PositiveIntegerField(_("default allocation"), default_allocation = models.PositiveIntegerField(_("default allocation"),
@ -116,12 +115,12 @@ class Resource(models.Model):
class ResourceData(models.Model): class ResourceData(models.Model):
""" Stores computed resource usage and allocation """ """ Stores computed resource usage and allocation """
resource = models.ForeignKey(Resource, related_name='dataset') resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField(_("object id"))
used = models.PositiveIntegerField(null=True) used = models.PositiveIntegerField(_("used"), null=True)
last_update = models.DateTimeField(null=True) updated_at = models.DateTimeField(_("updated"), null=True)
allocated = models.PositiveIntegerField(null=True, blank=True) allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True)
content_object = GenericForeignKey() content_object = GenericForeignKey()
@ -146,7 +145,7 @@ class ResourceData(models.Model):
if current is None: if current is None:
current = self.get_used() current = self.get_used()
self.used = current or 0 self.used = current or 0
self.last_update = timezone.now() self.updated_at = timezone.now()
self.save() self.save()
@ -154,9 +153,9 @@ class MonitorData(models.Model):
""" Stores monitored data """ """ Stores monitored data """
monitor = models.CharField(_("monitor"), max_length=256, monitor = models.CharField(_("monitor"), max_length=256,
choices=ServiceMonitor.get_plugin_choices()) choices=ServiceMonitor.get_plugin_choices())
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField(_("object id"))
date = models.DateTimeField(_("date"), auto_now_add=True) created_at = models.DateTimeField(_("created"), auto_now_add=True)
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
content_object = GenericForeignKey() content_object = GenericForeignKey()

View File

@ -44,12 +44,12 @@ if not running_syncdb():
msg = "Unknown or duplicated resource '%s'." % resource msg = "Unknown or duplicated resource '%s'." % resource
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
resources.remove(resource) 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 data.allocated = resource.default_allocation
result.append(data) result.append(data)
for resource in resources: for resource in resources:
data = ResourceData(resource=resource) data = ResourceData(resource=resource)
if not resource.ondemand: if not resource.on_demand:
data.allocated = resource.default_allocation data.allocated = resource.default_allocation
result.append(data) result.append(data)
attrs[source] = result attrs[source] = result
@ -64,7 +64,7 @@ if not running_syncdb():
ret['available_resources'] = [ ret['available_resources'] = [
{ {
'name': resource.name, 'name': resource.name,
'ondemand': resource.ondemand, 'on_demand': resource.on_demand,
'default_allocation': resource.default_allocation 'default_allocation': resource.default_allocation
} for resource in resources } for resource in resources
] ]

View File

@ -1,6 +1,5 @@
from celery import shared_task from celery import shared_task
from django.db.models.loading import get_model from django.db.models.loading import get_model
from django.utils import timezone
from orchestra.apps.orchestration.models import BackendOperation as Operation from orchestra.apps.orchestration.models import BackendOperation as Operation
@ -34,7 +33,7 @@ def monitor(resource_id):
operations.append(op) operations.append(op)
elif data.used < data.allocated: elif data.used < data.allocated:
op = Operation.create(backend, obj, Operation.RECOVERY) op = Operation.create(backend, obj, Operation.RECOVERY)
operation.append(op) operations.append(op)
# data = ResourceData.get_or_create(obj, resource) # data = ResourceData.get_or_create(obj, resource)
# current = data.get_used() # current = data.get_used()
# if not resource.disable_trigger: # if not resource.disable_trigger:

View File

@ -1,5 +1,5 @@
from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _ 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): def update_orders(modeladmin, request, queryset):
for service in queryset: for service in queryset:
service.update_orders() 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() msg = _("Orders for %s selected services have been updated.") % queryset.count()
modeladmin.message_user(request, msg) modeladmin.message_user(request, msg)
update_orders.url_name = 'update-orders' update_orders.url_name = 'update-orders'
update_orders.verbose_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")

View File

@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services from orchestra.core import services
from .actions import update_orders from .actions import update_orders, view_help
from .models import Plan, ContractedPlan, Rate, Service from .models import Plan, ContractedPlan, Rate, Service
@ -52,7 +52,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
) )
inlines = [RateInline] inlines = [RateInline]
actions = [update_orders] actions = [update_orders]
change_view_actions = actions change_view_actions = actions + [view_help]
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Improve performance of account field and filter by account """ """ Improve performance of account field and filter by account """

View File

@ -4,12 +4,11 @@ import decimal
from dateutil import relativedelta from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins from orchestra.utils import plugins
from orchestra.utils.python import AttributeDict from orchestra.utils.python import AttrDict
from . import settings, helpers from . import settings, helpers
@ -21,6 +20,8 @@ class ServiceHandler(plugins.Plugin):
Relax and enjoy the journey. Relax and enjoy the journey.
""" """
_VOLUME = 'VOLUME'
_COMPENSATION = 'COMPENSATION'
model = None model = None
@ -160,7 +161,7 @@ class ServiceHandler(plugins.Plugin):
return None return None
def generate_discount(self, line, dtype, price): def generate_discount(self, line, dtype, price):
line.discounts.append(AttributeDict(**{ line.discounts.append(AttrDict(**{
'type': dtype, 'type': dtype,
'total': price, 'total': price,
})) }))
@ -182,7 +183,7 @@ class ServiceHandler(plugins.Plugin):
if not computed: if not computed:
price = price * size price = price * size
subtotal = self.nominal_price * size * metric subtotal = self.nominal_price * size * metric
line = AttributeDict(**{ line = AttrDict(**{
'order': order, 'order': order,
'subtotal': subtotal, 'subtotal': subtotal,
'ini': ini, 'ini': ini,
@ -197,7 +198,7 @@ class ServiceHandler(plugins.Plugin):
discounted += dprice discounted += dprice
subtotal += discounted subtotal += discounted
if subtotal > price: if subtotal > price:
self.generate_discount(line, 'volume', price-subtotal) self.generate_discount(line, self._VOLUME, price-subtotal)
return line return line
def assign_compensations(self, givers, receivers, **options): def assign_compensations(self, givers, receivers, **options):
@ -225,7 +226,6 @@ class ServiceHandler(plugins.Plugin):
def apply_compensations(self, order, only_beyond=False): def apply_compensations(self, order, only_beyond=False):
dsize = 0 dsize = 0
discounts = ()
ini = order.billed_until or order.registered_on ini = order.billed_until or order.registered_on
end = order.new_billed_until end = order.new_billed_until
beyond = end beyond = end
@ -296,7 +296,7 @@ class ServiceHandler(plugins.Plugin):
cprice += dsize*price cprice += dsize*price
if cprice: if cprice:
discounts = ( discounts = (
('compensation', -cprice), (self._COMPENSATION, -cprice),
) )
if new_end: if new_end:
size = self.get_price_size(order.new_billed_until, new_end) size = self.get_price_size(order.new_billed_until, new_end)
@ -323,11 +323,12 @@ class ServiceHandler(plugins.Plugin):
discounts = () discounts = ()
dsize, new_end = self.apply_compensations(order) dsize, new_end = self.apply_compensations(order)
if dsize: if dsize:
discounts=(('compensation', -dsize*price),) discounts=(
(self._COMPENSATION, -dsize*price),
)
if new_end: if new_end:
order.new_billed_until = new_end order.new_billed_until = new_end
end = new_end end = new_end
size = self.get_price_size(ini, end)
line = self.generate_line(order, price, ini, end, discounts=discounts) line = self.generate_line(order, price, ini, end, discounts=discounts)
lines.append(line) lines.append(line)
return lines return lines
@ -395,7 +396,7 @@ class ServiceHandler(plugins.Plugin):
dsize, new_end = self.apply_compensations(order) dsize, new_end = self.apply_compensations(order)
if dsize: if dsize:
discounts=( discounts=(
('compensation', -dsize*price), (self._COMPENSATION, -dsize*price),
) )
if new_end: if new_end:
order.new_billed_until = new_end order.new_billed_until = new_end

View File

@ -1,24 +1,18 @@
import decimal import decimal
import sys
from django.db import models 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.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.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import caches, services, accounts from orchestra.core import caches, services, accounts
from orchestra.models import queryset from orchestra.models import queryset
from orchestra.utils.apps import autodiscover 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 from .handlers import ServiceHandler
@ -329,7 +323,7 @@ class Service(models.Model):
def update_orders(self): def update_orders(self):
order_model = get_model(settings.SERVICES_ORDER_MODEL) order_model = get_model(settings.SERVICES_ORDER_MODEL)
related_model = self.content_type.model_class() 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) order_model.update_orders(instance, service=self)

View File

@ -1,6 +1,6 @@
import sys import sys
from orchestra.utils.python import AttributeDict from orchestra.utils.python import AttrDict
def _compute(rates, metric): def _compute(rates, metric):
@ -30,7 +30,7 @@ def _compute(rates, metric):
quantity = metric - accumulated quantity = metric - accumulated
end = True end = True
price = rates[ix].price price = rates[ix].price
steps.append(AttributeDict(**{ steps.append(AttrDict(**{
'quantity': quantity, 'quantity': quantity,
'price': price, 'price': price,
'barrier': barrier, 'barrier': barrier,
@ -109,7 +109,7 @@ def step_price(rates, metric):
if result and result[-1].price == price: if result and result[-1].price == price:
result[-1].quantity += quantity result[-1].quantity += quantity
else: else:
result.append(AttributeDict(quantity=quantity, price=price)) result.append(AttrDict(quantity=quantity, price=price))
ix = 0 ix = 0
targets = [] targets = []
else: else:
@ -139,7 +139,7 @@ def match_price(rates, metric):
candidates.append(prev) candidates.append(prev)
candidates.sort(key=lambda r: r.price) candidates.sort(key=lambda r: r.price)
if candidates: if candidates:
return [AttributeDict(**{ return [AttrDict(**{
'quantity': metric, 'quantity': metric,
'price': candidates[0].price, 'price': candidates[0].price,
})] })]

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View 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

View File

@ -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 %}

View File

@ -41,7 +41,7 @@ class DomainBillingTest(BaseBillingTest):
return account.miscellaneous.create(service=domain_service, description=domain_name) return account.miscellaneous.create(service=domain_service, description=domain_name)
def test_domain(self): def test_domain(self):
service = self.create_domain_service() self.create_domain_service()
account = self.create_account() account = self.create_account()
self.create_domain(account=account) self.create_domain(account=account)
bills = account.orders.bill() bills = account.orders.bill()
@ -69,7 +69,7 @@ class DomainBillingTest(BaseBillingTest):
self.assertEqual(56, bills[0].get_total()) self.assertEqual(56, bills[0].get_total())
def test_domain_proforma(self): def test_domain_proforma(self):
service = self.create_domain_service() self.create_domain_service()
account = self.create_account() account = self.create_account()
self.create_domain(account=account) self.create_domain(account=account)
bills = account.orders.bill(proforma=True, new_open=True) bills = account.orders.bill(proforma=True, new_open=True)
@ -97,7 +97,7 @@ class DomainBillingTest(BaseBillingTest):
self.assertEqual(56, bills[0].get_total()) self.assertEqual(56, bills[0].get_total())
def test_domain_cumulative(self): def test_domain_cumulative(self):
service = self.create_domain_service() self.create_domain_service()
account = self.create_account() account = self.create_account()
self.create_domain(account=account) self.create_domain(account=account)
bills = account.orders.bill(proforma=True) bills = account.orders.bill(proforma=True)
@ -110,7 +110,7 @@ class DomainBillingTest(BaseBillingTest):
self.assertEqual(30, bills[0].get_total()) self.assertEqual(30, bills[0].get_total())
def test_domain_new_open(self): def test_domain_new_open(self):
service = self.create_domain_service() self.create_domain_service()
account = self.create_account() account = self.create_account()
self.create_domain(account=account) self.create_domain(account=account)
bills = account.orders.bill(new_open=True) bills = account.orders.bill(new_open=True)

View File

@ -43,14 +43,14 @@ class FTPBillingTest(BaseBillingTest):
def test_ftp_account_1_year_fiexed(self): def test_ftp_account_1_year_fiexed(self):
service = self.create_ftp_service() service = self.create_ftp_service()
user = self.create_ftp() self.create_ftp()
bp = timezone.now().date() + relativedelta(years=1) bp = timezone.now().date() + relativedelta(years=1)
bills = service.orders.bill(billing_point=bp, fixed_point=True) bills = service.orders.bill(billing_point=bp, fixed_point=True)
self.assertEqual(10, bills[0].get_total()) self.assertEqual(10, bills[0].get_total())
def test_ftp_account_2_year_fiexed(self): def test_ftp_account_2_year_fiexed(self):
service = self.create_ftp_service() service = self.create_ftp_service()
user = self.create_ftp() self.create_ftp()
bp = timezone.now().date() + relativedelta(years=2) bp = timezone.now().date() + relativedelta(years=2)
bills = service.orders.bill(billing_point=bp, fixed_point=True) bills = service.orders.bill(billing_point=bp, fixed_point=True)
self.assertEqual(20, bills[0].get_total()) self.assertEqual(20, bills[0].get_total())
@ -79,7 +79,7 @@ class FTPBillingTest(BaseBillingTest):
def test_ftp_account_with_compensation(self): def test_ftp_account_with_compensation(self):
account = self.create_account() account = self.create_account()
service = self.create_ftp_service() self.create_ftp_service()
user = self.create_ftp(account=account) user = self.create_ftp(account=account)
first_bp = timezone.now().date() + relativedelta(years=2) first_bp = timezone.now().date() + relativedelta(years=2)
bills = account.orders.bill(billing_point=first_bp, fixed_point=True) bills = account.orders.bill(billing_point=first_bp, fixed_point=True)

View File

@ -39,13 +39,13 @@ class JobBillingTest(BaseBillingTest):
return account.miscellaneous.create(service=service, description=description, amount=amount) return account.miscellaneous.create(service=service, description=description, amount=amount)
def test_job(self): def test_job(self):
service = self.create_job_service() self.create_job_service()
account = self.create_account() account = self.create_account()
job = self.create_job(5, account=account) self.create_job(5, account=account)
bill = account.orders.bill()[0] bill = account.orders.bill()[0]
self.assertEqual(5*20, bill.get_total()) 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] bill = account.orders.bill(new_open=True)[0]
self.assertEqual(100*15, bill.get_total()) self.assertEqual(100*15, bill.get_total())

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from freezegun import freeze_time from freezegun import freeze_time
from orchestra.apps.mails.models import Mailbox 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 orchestra.utils.tests import random_ascii
from ...models import Service, Plan from ...models import Service, Plan
@ -62,7 +62,7 @@ class MailboxBillingTest(BaseBillingTest):
verbose_name='Mailbox disk', verbose_name='Mailbox disk',
unit='GB', unit='GB',
scale=10**9, scale=10**9,
ondemand=False, on_demand=False,
monitors='MaildirDisk', monitors='MaildirDisk',
) )
return self.resource return self.resource

View File

@ -42,7 +42,7 @@ class PlanBillingTest(BaseBillingTest):
def test_plan(self): def test_plan(self):
account = self.create_account() account = self.create_account()
service = self.create_plan_service() self.create_plan_service()
self.create_plan(account=account) self.create_plan(account=account)
bill = account.orders.bill().pop() bill = account.orders.bill().pop()
self.assertEqual(bill.FEE, bill.type) self.assertEqual(bill.FEE, bill.type)

View File

@ -1,5 +1,3 @@
import datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
@ -46,26 +44,23 @@ class BaseTrafficBillingTest(BaseBillingTest):
verbose_name='Account Traffic', verbose_name='Account Traffic',
unit='GB', unit='GB',
scale=10**9, scale=10**9,
ondemand=True, on_demand=True,
monitors='FTPTraffic', monitors='FTPTraffic',
) )
return self.resource return self.resource
def report_traffic(self, account, value): def report_traffic(self, account, value):
ct = ContentType.objects.get_for_model(Account) MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value)
object_id = account.pk
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user,
value=value, date=timezone.now())
data = ResourceData.get_or_create(account, self.resource) data = ResourceData.get_or_create(account, self.resource)
data.update() data.update()
class TrafficBillingTest(BaseTrafficBillingTest): class TrafficBillingTest(BaseTrafficBillingTest):
def test_traffic(self): def test_traffic(self):
service = self.create_traffic_service() self.create_traffic_service()
resource = self.create_traffic_resource() self.create_traffic_resource()
account = self.create_account() account = self.create_account()
now = timezone.now().date() now = timezone.now()
self.report_traffic(account, 10**9) self.report_traffic(account, 10**9)
bill = account.orders.bill(commit=False)[0] bill = account.orders.bill(commit=False)[0]
@ -82,8 +77,8 @@ class TrafficBillingTest(BaseTrafficBillingTest):
self.assertEqual((90-10)*10, bill.get_total()) self.assertEqual((90-10)*10, bill.get_total())
def test_multiple_traffics(self): def test_multiple_traffics(self):
service = self.create_traffic_service() self.create_traffic_service()
resource = self.create_traffic_resource() self.create_traffic_resource()
account1 = self.create_account() account1 = self.create_account()
account2 = self.create_account() account2 = self.create_account()
self.report_traffic(account1, 10**10) self.report_traffic(account1, 10**10)
@ -129,13 +124,13 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
return account.miscellaneous.create(service=service, description=name, amount=amount) return account.miscellaneous.create(service=service, description=name, amount=amount)
def test_traffic_prepay(self): def test_traffic_prepay(self):
service = self.create_traffic_service() self.create_traffic_service()
prepay_service = self.create_prepay_service() self.create_prepay_service()
account = self.create_account() account = self.create_account()
self.create_traffic_resource() self.create_traffic_resource()
now = timezone.now() now = timezone.now()
prepay = self.create_prepay(10, account=account) self.create_prepay(10, account=account)
bill = account.orders.bill(proforma=True)[0] bill = account.orders.bill(proforma=True)[0]
self.assertEqual(10*50, bill.get_total()) self.assertEqual(10*50, bill.get_total())

View File

@ -1,17 +1,15 @@
import datetime import datetime
import decimal import decimal
import sys
from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
from orchestra.apps.accounts.models import Account from orchestra.apps.accounts.models import Account
from orchestra.apps.users.models import User 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 .. import helpers
from ..models import Service, Plan, Rate from ..models import Service, Plan
class Order(object): class Order(object):

View File

@ -55,7 +55,6 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
def get_urls(self): def get_urls(self):
""" Returns the additional urls for the change view links """ """ Returns the additional urls for the change view links """
urls = super(UserAdmin, self).get_urls() urls = super(UserAdmin, self).get_urls()
admin_site = self.admin_site
opts = self.model._meta opts = self.model._meta
new_urls = patterns("") new_urls = patterns("")
for role in self.roles: for role in self.roles:

View File

@ -1,8 +1,5 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import viewsets 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.api import router, SetPasswordApiMixin
from orchestra.apps.accounts.api import AccountApiMixin from orchestra.apps.accounts.api import AccountApiMixin

View File

@ -1,5 +1,6 @@
from django.contrib.auth import models as auth from django.contrib.auth import models as auth
from django.core import validators from django.core import validators
from django.core.mail import send_mail
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -1,5 +1,3 @@
from django.db import models
from ..models import User from ..models import User

View File

@ -1,7 +1,6 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.util import unquote, get_deleted_objects from django.contrib.admin.util import unquote, get_deleted_objects
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.core.urlresolvers import reverse
from django.db import router from django.db import router
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse

View File

@ -1,6 +1,4 @@
from django.contrib import admin
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.utils import insertattr from orchestra.admin.utils import insertattr
from orchestra.apps.users.roles.admin import RoleAdmin from orchestra.apps.users.roles.admin import RoleAdmin

View File

@ -1,15 +1,12 @@
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import insertattr, admin_link from orchestra.admin.utils import insertattr, admin_link
from orchestra.apps.accounts.admin import SelectAccountAdminMixin from orchestra.apps.accounts.admin import SelectAccountAdminMixin
from orchestra.apps.domains.forms import DomainIterator
from orchestra.apps.users.roles.admin import RoleAdmin from orchestra.apps.users.roles.admin import RoleAdmin
from .forms import MailRoleAdminForm from .forms import MailRoleAdminForm

View File

@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor from orchestra.apps.resources import ServiceMonitor
from . import settings from . import settings
from .models import Address
class MailSystemUserBackend(ServiceController): class MailSystemUserBackend(ServiceController):
@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor):
) )
def get_context(self, mailbox): def get_context(self, mailbox):
context = MailSystemUserBackend().get_context(site) context = MailSystemUserBackend().get_context(mailbox)
context['home'] = settings.EMAILS_HOME % context context['home'] = settings.EMAILS_HOME % context
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize') context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
context['object_id'] = mailbox.pk context['object_id'] = mailbox.pk

View File

@ -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.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -44,7 +44,6 @@ def validate_forward(value):
def validate_sieve(value): def validate_sieve(value):
from .models import Mailbox
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest() sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name) path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
with open(path, 'wb') as f: with open(path, 'wb') as f:

View File

@ -1,6 +1,4 @@
from django.contrib import admin
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.utils import insertattr from orchestra.admin.utils import insertattr
from orchestra.apps.users.roles.admin import RoleAdmin from orchestra.apps.users.roles.admin import RoleAdmin

View File

@ -1,7 +1,6 @@
from django.conf.urls import patterns from django.conf.urls import patterns
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin

View File

@ -1,5 +1,3 @@
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.resources import ServiceMonitor from orchestra.apps.resources import ServiceMonitor

View File

@ -1,5 +1,5 @@
from django import forms 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 _ from django.utils.translation import ugettext_lazy as _

View File

@ -1,6 +1,5 @@
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin

View File

@ -1,5 +1,4 @@
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.response import Response
from orchestra.api import router from orchestra.api import router
from orchestra.apps.accounts.api import AccountApiMixin from orchestra.apps.accounts.api import AccountApiMixin

View File

@ -1,3 +1,5 @@
import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController from orchestra.apps.orchestration import ServiceController
@ -21,7 +23,7 @@ class DokuWikiMuBackend(WebAppServiceMixin, ServiceController):
self.append("rm -fr %(app_path)s" % context) self.append("rm -fr %(app_path)s" % context)
def get_context(self, webapp): def get_context(self, webapp):
context = super(DokuwikiMuBackend, self).get_context(webapp) context = super(DokuWikiMuBackend, self).get_context(webapp)
context.update({ context.update({
'template': settings.WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH, 'template': settings.WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH,
'app_path': os.path.join(settings.WEBAPPS_DOKUWIKIMU_FARM_PATH, webapp.name) 'app_path': os.path.join(settings.WEBAPPS_DOKUWIKIMU_FARM_PATH, webapp.name)

View File

@ -1,5 +1,4 @@
import re import re
import sys
import requests import requests
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -55,6 +54,7 @@ class WordpressMuBackend(WebAppServiceMixin, ServiceController):
'blog[email]': email, 'blog[email]': email,
'_wpnonce_add-blog': wpnonce, '_wpnonce_add-blog': wpnonce,
} }
# TODO validate response
response = session.post(url, data=data) response = session.post(url, data=data)
def delete_blog(self, webapp, server): def delete_blog(self, webapp, server):

View File

@ -1,13 +1,10 @@
from django import forms from django import forms
from django.contrib import admin 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 django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_link, change_url from orchestra.admin.utils import admin_link, change_url
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin 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 from .models import Content, Website, WebsiteOption

View File

@ -1,5 +1,4 @@
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.response import Response
from orchestra.api import router from orchestra.api import router
from orchestra.apps.accounts.api import AccountApiMixin from orchestra.apps.accounts.api import AccountApiMixin

View File

@ -1,5 +1,6 @@
import textwrap import textwrap
import os import os
import re
from django.template import Template, Context from django.template import Template, Context
from django.utils import timezone from django.utils import timezone
@ -123,6 +124,7 @@ class Apache2Backend(ServiceController):
def get_protections(self, site): def get_protections(self, site):
protections = "" protections = ""
__, regex = settings.WEBSITES_OPTIONS['directory_protection'] __, regex = settings.WEBSITES_OPTIONS['directory_protection']
context = self.get_context(site)
for protection in site.options.filter(name='directory_protection'): for protection in site.options.filter(name='directory_protection'):
path, name, passwd = re.match(regex, protection.value).groups() path, name, passwd = re.match(regex, protection.value).groups()
path = os.path.join(context['root'], path) path = os.path.join(context['root'], path)

Some files were not shown because too many files have changed in this diff Show More