diff --git a/TODO.md b/TODO.md
index 46271e8f..f1a1f3fd 100644
--- a/TODO.md
+++ b/TODO.md
@@ -59,12 +59,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
dependency collector with max_recursion that matches the number of dots on service.match and service.metric
-* Be consistent with dates:
- * created_on date
- * created_at datetime
-
-at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
-
* backend logs with hal logo
* Use logs for storing monitored values
@@ -82,13 +76,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
* help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
-* Create ProForma from orders orders.bill(proforma=True)
-
-* generic confirmation breadcrumbs for single objects
-
-* DirectDebit due date = bill.due_date
-
-* settings.ENABLED_PLUGINS = ('path.module.ClassPlugin',)
* Transaction states: CREATED, PROCESSED, EXECUTED, COMMITED, ABORTED (SECURED, REJECTED?)
* bill.send() -> transacction.EXECUTED when source=None
@@ -108,5 +95,7 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
return order.register_at.date()
-* latest by 'id' *always*
-* replace add_now by default=lambda: timezone.now()
+* mail backend related_models = ('resources__content_type') ??
+* ignore orders
+
+* Redmine, BSCW and other applications management
diff --git a/orchestra/admin/decorators.py b/orchestra/admin/decorators.py
index 37bda702..029884a7 100644
--- a/orchestra/admin/decorators.py
+++ b/orchestra/admin/decorators.py
@@ -1,6 +1,5 @@
from functools import wraps, partial
-from django.contrib import messages
from django.contrib.admin import helpers
from django.template.response import TemplateResponse
from django.utils.decorators import available_attrs
@@ -61,8 +60,10 @@ def action_with_confirmation(action_name=None, extra_context={},
if len(queryset) == 1:
objects_name = force_text(opts.verbose_name)
+ obj = queryset.get()
else:
objects_name = force_text(opts.verbose_name_plural)
+ obj = None
if not action_name:
action_name = func.__name__
context = {
@@ -73,6 +74,7 @@ def action_with_confirmation(action_name=None, extra_context={},
'action_value': action_value,
'queryset': queryset,
'opts': opts,
+ 'obj': obj,
'app_label': app_label,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
}
diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py
index 2807a5b0..9be768dd 100644
--- a/orchestra/admin/utils.py
+++ b/orchestra/admin/utils.py
@@ -9,7 +9,6 @@ from django.shortcuts import redirect
from django.utils import importlib
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy as _
from orchestra.models.utils import get_field_value
from orchestra.utils import humanize
diff --git a/orchestra/api/options.py b/orchestra/api/options.py
index c56b8e33..d27aeb9e 100644
--- a/orchestra/api/options.py
+++ b/orchestra/api/options.py
@@ -6,7 +6,6 @@ from orchestra.utils.apps import autodiscover as module_autodiscover
from orchestra.utils.python import import_class
from .helpers import insert_links, replace_collectionmethodname
-from .root import APIRoot
def collectionlink(**kwargs):
diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py
index ea5eadf3..70bdb23d 100644
--- a/orchestra/apps/accounts/admin.py
+++ b/orchestra/apps/accounts/admin.py
@@ -2,7 +2,6 @@ from django import forms
from django.conf.urls import patterns, url
from django.contrib import admin, messages
from django.contrib.admin.util import unquote
-from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.utils.safestring import mark_safe
from django.utils.six.moves.urllib.parse import parse_qsl
@@ -115,7 +114,6 @@ class AccountListAdmin(AccountAdmin):
select_account.order_admin_field = 'user__username'
def changelist_view(self, request, extra_context=None):
- opts = self.model._meta
original_app_label = request.META['PATH_INFO'].split('/')[-5]
original_model = request.META['PATH_INFO'].split('/')[-4]
context = {
@@ -182,7 +180,6 @@ class AccountAdminMixin(object):
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
account_id = self.get_account_from_preserve_filters(request)
- verb = 'change' if object_id else 'add'
if not object_id:
if account_id:
# Preselect account
diff --git a/orchestra/apps/accounts/filters.py b/orchestra/apps/accounts/filters.py
index 078d44aa..84a27831 100644
--- a/orchestra/apps/accounts/filters.py
+++ b/orchestra/apps/accounts/filters.py
@@ -1,5 +1,4 @@
from django.contrib.admin import SimpleListFilter
-from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
diff --git a/orchestra/apps/accounts/management/commands/createinitialaccount.py b/orchestra/apps/accounts/management/commands/createinitialaccount.py
index 5ebde3bb..8e5b1bc5 100644
--- a/orchestra/apps/accounts/management/commands/createinitialaccount.py
+++ b/orchestra/apps/accounts/management/commands/createinitialaccount.py
@@ -1,10 +1,9 @@
from optparse import make_option
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
from django.db import transaction
from orchestra.apps.accounts.models import Account
-from orchestra.apps.users.models import User
class Command(BaseCommand):
@@ -29,5 +28,4 @@ class Command(BaseCommand):
username = options.get('username')
password = options.get('password')
account = Account.objects.create()
- user = User.objects.create_superuser(username, email, password,
- account=account, is_main=True)
+ account.users.create_superuser(username, email, password, is_main=True)
diff --git a/orchestra/apps/accounts/migrations/0003_auto_20140926_1325.py b/orchestra/apps/accounts/migrations/0003_auto_20140926_1325.py
new file mode 100644
index 00000000..735351a3
--- /dev/null
+++ b/orchestra/apps/accounts/migrations/0003_auto_20140926_1325.py
@@ -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,
+ ),
+ ]
diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py
index a35d9b02..08f10d74 100644
--- a/orchestra/apps/accounts/models.py
+++ b/orchestra/apps/accounts/models.py
@@ -1,6 +1,5 @@
from django.conf import settings as djsettings
from django.db import models
-from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
@@ -18,7 +17,7 @@ class Account(models.Model):
language = models.CharField(_("language"), max_length=2,
choices=settings.ACCOUNTS_LANGUAGES,
default=settings.ACCOUNTS_DEFAULT_LANGUAGE)
- register_date = models.DateTimeField(_("register date"), auto_now_add=True)
+ registered_on = models.DateField(_("registered"), auto_now_add=True)
comments = models.TextField(_("comments"), max_length=256, blank=True)
is_active = models.BooleanField(default=True)
diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py
index 7a196297..b0942c83 100644
--- a/orchestra/apps/bills/admin.py
+++ b/orchestra/apps/bills/admin.py
@@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
-from orchestra.admin.utils import admin_link, admin_date
+from orchestra.admin.utils import admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings
@@ -53,8 +53,8 @@ class BillLineInline(admin.TabularInline):
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = (
- 'number', 'is_open', 'type_link', 'account_link', 'created_on_display',
- 'num_lines', 'display_total', 'display_payment_state'
+ 'number', 'type_link', 'account_link', 'created_on_display',
+ 'num_lines', 'display_total', 'display_payment_state', 'is_open'
)
list_filter = (BillTypeListFilter, 'is_open',)
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
diff --git a/orchestra/apps/bills/migrations/0008_auto_20140926_1218.py b/orchestra/apps/bills/migrations/0008_auto_20140926_1218.py
new file mode 100644
index 00000000..561885ee
--- /dev/null
+++ b/orchestra/apps/bills/migrations/0008_auto_20140926_1218.py
@@ -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'},
+ ),
+ ]
diff --git a/orchestra/apps/bills/migrations/0009_auto_20140926_1220.py b/orchestra/apps/bills/migrations/0009_auto_20140926_1220.py
new file mode 100644
index 00000000..1382c851
--- /dev/null
+++ b/orchestra/apps/bills/migrations/0009_auto_20140926_1220.py
@@ -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,
+ ),
+ ]
diff --git a/orchestra/apps/bills/migrations/0010_auto_20140926_1326.py b/orchestra/apps/bills/migrations/0010_auto_20140926_1326.py
new file mode 100644
index 00000000..e4b5c90e
--- /dev/null
+++ b/orchestra/apps/bills/migrations/0010_auto_20140926_1326.py
@@ -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'),
+ ),
+ ]
diff --git a/orchestra/apps/bills/migrations/0011_auto_20140926_1334.py b/orchestra/apps/bills/migrations/0011_auto_20140926_1334.py
new file mode 100644
index 00000000..a31d489e
--- /dev/null
+++ b/orchestra/apps/bills/migrations/0011_auto_20140926_1334.py
@@ -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,
+ ),
+ ]
diff --git a/orchestra/apps/bills/migrations/0012_auto_20140926_1458.py b/orchestra/apps/bills/migrations/0012_auto_20140926_1458.py
new file mode 100644
index 00000000..321a4086
--- /dev/null
+++ b/orchestra/apps/bills/migrations/0012_auto_20140926_1458.py
@@ -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',
+ ),
+ ]
diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py
index ffe54030..0044d82b 100644
--- a/orchestra/apps/bills/models.py
+++ b/orchestra/apps/bills/models.py
@@ -1,6 +1,6 @@
-import inspect
from dateutil.relativedelta import relativedelta
+from django.core.validators import ValidationError
from django.db import models
from django.template import loader, Context
from django.utils import timezone
@@ -9,8 +9,8 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.models import Account
+from orchestra.apps.contacts.models import Contact
from orchestra.core import accounts
-from orchestra.utils.functional import cached
from orchestra.utils.html import html_to_pdf
from . import settings
@@ -53,13 +53,12 @@ class Bill(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s')
type = models.CharField(_("type"), max_length=16, choices=TYPES)
- created_on = models.DateTimeField(_("created on"), auto_now_add=True)
- closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
- # TODO rename to is_closed
+ created_on = models.DateField(_("created on"), auto_now_add=True)
+ closed_on = models.DateField(_("closed on"), blank=True, null=True)
is_open = models.BooleanField(_("is open"), default=True)
is_sent = models.BooleanField(_("is sent"), default=False)
due_on = models.DateField(_("due on"), null=True, blank=True)
- last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
+ updated_on = models.DateField(_("updated on"), auto_now=True)
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
comments = models.TextField(_("comments"), blank=True)
html = models.TextField(_("HTML"), blank=True)
@@ -145,7 +144,6 @@ class Bill(models.Model):
self.save()
def send(self):
- from orchestra.apps.contacts.models import Contact
self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={
@@ -241,9 +239,13 @@ class BillLine(models.Model):
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
tax = models.PositiveIntegerField(_("tax"))
- # TODO
-# order_id = models.ForeignKey('orders.Order', null=True, blank=True,
-# help_text=_("Informative link back to the order"))
+ # Undo
+ order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
+ help_text=_("Informative link back to the order"))
+ order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
+ order_billed_until = models.DateField(_("order billed until"), null=True, blank=True)
+ created_on = models.DateField(_("created"), auto_now_add=True)
+ # Amendment
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
@@ -262,6 +264,17 @@ class BillLine(models.Model):
total += subline.total
return total
+ def undo(self):
+ # TODO warn user that undoing bills with compensations lead to compensation lost
+ for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
+ if not getattr(self, attr):
+ raise ValidationError(_("Not enough information stored for undoing"))
+ if self.created_on != self.order.billed_on:
+ raise ValidationError(_("Dates don't match"))
+ self.order.billed_until = self.order_billed_until
+ self.order.billed_on = self.order_billed_on
+ self.delete()
+
def save(self, *args, **kwargs):
# TODO cost and consistency of this shit
super(BillLine, self).save(*args, **kwargs)
@@ -272,10 +285,20 @@ class BillLine(models.Model):
class BillSubline(models.Model):
""" Subline used for describing an item discount """
+ VOLUME = 'VOLUME'
+ COMPENSATION = 'COMPENSATION'
+ OTHER = 'OTHER'
+ TYPES = (
+ (VOLUME, _("Volume")),
+ (COMPENSATION, _("Compensation")),
+ (OTHER, _("Other")),
+ )
+
+ # TODO: order info for undoing
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
description = models.CharField(_("description"), max_length=256)
total = models.DecimalField(max_digits=12, decimal_places=2)
- # TODO type ? Volume and Compensation
+ type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)
def save(self, *args, **kwargs):
# TODO cost of this shit
diff --git a/orchestra/apps/bills/settings.py b/orchestra/apps/bills/settings.py
index ac590005..75702302 100644
--- a/orchestra/apps/bills/settings.py
+++ b/orchestra/apps/bills/settings.py
@@ -36,3 +36,6 @@ BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT', '0000
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
'bills/bill-notification.email')
+
+
+BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')
diff --git a/orchestra/apps/bills/templates/bills/microspective.html b/orchestra/apps/bills/templates/bills/microspective.html
index a2c4f776..6cbf2cfc 100644
--- a/orchestra/apps/bills/templates/bills/microspective.html
+++ b/orchestra/apps/bills/templates/bills/microspective.html
@@ -78,9 +78,9 @@
{% with sublines=line.sublines.all %}
{{ line.id }}
{{ line.description }}
- {{ line.amount|default:" " }}
+ {{ line.quantity|default:" " }}
{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}
- {{ line.total }} &{{ currency.lower }};
+ {{ line.subtotal }} &{{ currency.lower }};
{% for subline in sublines %}
diff --git a/orchestra/apps/contacts/settings.py b/orchestra/apps/contacts/settings.py
index 96ac58c4..0190d989 100644
--- a/orchestra/apps/contacts/settings.py
+++ b/orchestra/apps/contacts/settings.py
@@ -1,5 +1,4 @@
from django.conf import settings
-from django.utils.translation import ugettext_lazy as _
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
diff --git a/orchestra/apps/databases/admin.py b/orchestra/apps/databases/admin.py
index 30c45af4..c54a9bfe 100644
--- a/orchestra/apps/databases/admin.py
+++ b/orchestra/apps/databases/admin.py
@@ -1,8 +1,6 @@
-from django.db import models
from django.conf.urls import patterns
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
-from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
diff --git a/orchestra/apps/databases/api.py b/orchestra/apps/databases/api.py
index 2cec89c4..a04ee569 100644
--- a/orchestra/apps/databases/api.py
+++ b/orchestra/apps/databases/api.py
@@ -1,7 +1,4 @@
from rest_framework import viewsets
-from rest_framework import status
-from rest_framework.decorators import action
-from rest_framework.response import Response
from orchestra.api import router, SetPasswordApiMixin
from orchestra.apps.accounts.api import AccountApiMixin
diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py
index 5579adb5..89cfc80e 100644
--- a/orchestra/apps/databases/backends.py
+++ b/orchestra/apps/databases/backends.py
@@ -77,21 +77,21 @@ class MysqlDisk(ServiceMonitor):
verbose_name = _("MySQL disk")
def exceeded(self, db):
- context = self.get_context(obj)
+ context = self.get_context(db)
self.append("mysql -e '"
"UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\""
" WHERE Db=\"%(db_name)s\";'" % context
)
def recovery(self, db):
- context = self.get_context(obj)
+ context = self.get_context(db)
self.append("mysql -e '"
"UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\""
" WHERE Db=\"%(db_name)s\";'" % context
)
def monitor(self, db):
- context = self.get_context(obj)
+ context = self.get_context(db)
self.append(
"echo %(db_id)s $(mysql -B -e '"
" SELECT sum( data_length + index_length ) \"Size\"\n"
diff --git a/orchestra/apps/databases/forms.py b/orchestra/apps/databases/forms.py
index 5e5f13c1..a9ff2d38 100644
--- a/orchestra/apps/databases/forms.py
+++ b/orchestra/apps/databases/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from django.contrib.auth.forms import UserCreationForm, ReadOnlyPasswordHashField
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
@@ -109,7 +109,6 @@ class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField):
if 'Invalid' not in original:
return original
encoded = value
- final_attrs = self.build_attrs(attrs)
if not encoded:
summary = mark_safe("%s" % _("No password set."))
else:
diff --git a/orchestra/apps/domains/actions.py b/orchestra/apps/domains/actions.py
new file mode 100644
index 00000000..7818e953
--- /dev/null
+++ b/orchestra/apps/domains/actions.py
@@ -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")
diff --git a/orchestra/apps/domains/admin.py b/orchestra/apps/domains/admin.py
index 2f7fafcc..0ee63fac 100644
--- a/orchestra/apps/domains/admin.py
+++ b/orchestra/apps/domains/admin.py
@@ -1,17 +1,13 @@
from django import forms
-from django.conf.urls import patterns, url
from django.contrib import admin
-from django.contrib.admin.util import unquote
-from django.core.urlresolvers import reverse
-from django.db.models import F
-from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
-from orchestra.admin.utils import wrap_admin_view, admin_link, change_url
+from orchestra.admin.utils import admin_link, change_url
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.utils import apps
+from .actions import view_zone
from .forms import RecordInlineFormSet, DomainAdminForm
from .filters import TopDomainListFilter
from .models import Domain, Record
@@ -61,6 +57,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
('top_domain', 'True'),
)
form = DomainAdminForm
+ change_view_actions = [view_zone]
def structured_name(self, domain):
if not domain.is_top:
@@ -89,35 +86,12 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
websites.short_description = _("Websites")
websites.allow_tags = True
- def get_urls(self):
- """ Returns the additional urls for the change view links """
- urls = super(DomainAdmin, self).get_urls()
- admin_site = self.admin_site
- opts = self.model._meta
- urls = patterns("",
- url('^(\d+)/view-zone/$',
- wrap_admin_view(self, self.view_zone_view),
- name='domains_domain_view_zone')
- ) + urls
- return urls
-
- def view_zone_view(self, request, object_id):
- zone = self.get_object(request, unquote(object_id))
- context = {
- 'opts': self.model._meta,
- 'object': zone,
- 'title': _("%s zone content") % zone.origin.name
- }
- return TemplateResponse(request, 'admin/domains/domain/view_zone.html',
- context)
-
def get_queryset(self, request):
""" Order by structured name and imporve performance """
qs = super(DomainAdmin, self).get_queryset(request)
qs = qs.select_related('top')
-# qs = qs.select_related('top')
# For some reason if we do this we know for sure that join table will be called T4
- __ = str(qs.query)
+ str(qs.query)
qs = qs.extra(
select={'structured_name': 'CONCAT(T4.name, domains_domain.name)'},
).order_by('structured_name')
diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py
index acc5592b..460d03e1 100644
--- a/orchestra/apps/domains/backends.py
+++ b/orchestra/apps/domains/backends.py
@@ -1,5 +1,3 @@
-import os
-
from django.utils.translation import ugettext_lazy as _
from . import settings
diff --git a/orchestra/apps/domains/migrations/0001_initial.py b/orchestra/apps/domains/migrations/0001_initial.py
new file mode 100644
index 00000000..5fb4bd30
--- /dev/null
+++ b/orchestra/apps/domains/migrations/0001_initial.py
@@ -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,),
+ ),
+ ]
diff --git a/orchestra/apps/domains/migrations/0002_record_ttl.py b/orchestra/apps/domains/migrations/0002_record_ttl.py
new file mode 100644
index 00000000..8ce4f8ff
--- /dev/null
+++ b/orchestra/apps/domains/migrations/0002_record_ttl.py
@@ -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,
+ ),
+ ]
diff --git a/orchestra/apps/domains/migrations/__init__.py b/orchestra/apps/domains/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py
index 1f7cf33a..0f7ca93b 100644
--- a/orchestra/apps/domains/models.py
+++ b/orchestra/apps/domains/models.py
@@ -1,11 +1,11 @@
-from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
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
@@ -69,13 +69,17 @@ class Domain(models.Model):
# Update serial and insert at 0
value = record.value.split()
value[2] = str(self.serial)
- records.insert(0, (record.SOA, ' '.join(value)))
+ records.insert(0,
+ AttrDict(type=record.SOA, ttl=record.get_ttl(), value=' '.join(value))
+ )
else:
- records.append((record.type, record.value))
+ records.append(
+ AttrDict(type=record.type, ttl=record.get_ttl(), value=record.value)
+ )
if not self.top:
if Record.NS not in types:
for ns in settings.DOMAINS_DEFAULT_NS:
- records.append((Record.NS, ns))
+ records.append(AttrDict(type=Record.NS, value=ns))
if Record.SOA not in types:
soa = [
"%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
@@ -86,18 +90,28 @@ class Domain(models.Model):
settings.DOMAINS_DEFAULT_EXPIRATION,
settings.DOMAINS_DEFAULT_MIN_CACHING_TIME
]
- records.insert(0, (Record.SOA, ' '.join(soa)))
+ records.insert(0, AttrDict(type=Record.SOA, value=' '.join(soa)))
no_cname = Record.CNAME not in types
if Record.MX not in types and no_cname:
for mx in settings.DOMAINS_DEFAULT_MX:
- records.append((Record.MX, mx))
+ records.append(AttrDict(type=Record.MX, value=mx))
if (Record.A not in types and Record.AAAA not in types) and no_cname:
- records.append((Record.A, settings.DOMAINS_DEFAULT_A))
+ records.append(AttrDict(type=Record.A, value=settings.DOMAINS_DEFAULT_A))
result = ''
- for type, value in records:
- name = '%s.%s' % (self.name, ' '*(37-len(self.name)))
- type = '%s %s' % (type, ' '*(7-len(type)))
- result += '%s IN %s %s\n' % (name, type, value)
+ for record in records:
+ name = '{name}.{spaces}'.format(
+ name=self.name, spaces=' ' * (37-len(self.name))
+ )
+ ttl = record.get('ttl', settings.DOMAINS_DEFAULT_TTL)
+ ttl = '{spaces}{ttl}'.format(
+ spaces=' ' * (7-len(ttl)), ttl=ttl
+ )
+ type = '{type} {spaces}'.format(
+ type=record.type, spaces=' ' * (7-len(record.type))
+ )
+ result += '{name} {ttl} IN {type} {value}\n'.format(
+ name=name, ttl=ttl, type=type, value=record.value
+ )
return result
def save(self, *args, **kwargs):
@@ -150,13 +164,15 @@ class Record(models.Model):
(SOA, "SOA"),
)
- # TODO TTL
domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
- type = models.CharField(max_length=32, choices=TYPE_CHOICES)
- value = models.CharField(max_length=256)
+ ttl = models.CharField(_("TTL"), max_length=8, blank=True,
+ help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
+ validators=[validators.validate_zone_interval])
+ type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
+ value = models.CharField(_("value"), max_length=256)
def __unicode__(self):
- return "%s IN %s %s" % (self.domain, self.type, self.value)
+ return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value)
def clean(self):
""" validates record value based on its type """
@@ -172,6 +188,8 @@ class Record(models.Model):
self.SOA: validators.validate_soa_record,
}
mapp[self.type](self.value)
-
+
+ def get_ttl(self):
+ return self.ttl or settings.DOMAINS_DEFAULT_TTL
services.register(Domain)
diff --git a/orchestra/apps/domains/tests/functional_tests/tests.py b/orchestra/apps/domains/tests/functional_tests/tests.py
index b0681725..a5268ae6 100644
--- a/orchestra/apps/domains/tests/functional_tests/tests.py
+++ b/orchestra/apps/domains/tests/functional_tests/tests.py
@@ -124,7 +124,7 @@ class DomainTestMixin(object):
self.assertNotEqual(hostmaster, soa[5])
def validate_update(self, server_addr, domain_name):
- domain = Domain.objects.get(name=domain_name)
+ Domain.objects.get(name=domain_name)
context = {
'domain_name': domain_name,
'server_addr': server_addr
diff --git a/orchestra/apps/domains/tests/test_domains.py b/orchestra/apps/domains/tests/test_domains.py
index e370bda3..82acc3ac 100644
--- a/orchestra/apps/domains/tests/test_domains.py
+++ b/orchestra/apps/domains/tests/test_domains.py
@@ -1,4 +1,3 @@
-from django.db import IntegrityError, transaction
from django.test import TestCase
from ..models import Domain
diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py
index 0f4896ed..80096cca 100644
--- a/orchestra/apps/issues/admin.py
+++ b/orchestra/apps/issues/admin.py
@@ -56,7 +56,7 @@ class MessageReadOnlyInline(admin.TabularInline):
def content_html(self, msg):
context = {
'number': msg.number,
- 'time': admin_date('created_on')(msg),
+ 'time': admin_date('created_at')(msg),
'author': admin_link('author')(msg) if msg.author else msg.author_name,
}
summary = _("#%(number)i Updated by %(author)s about %(time)s") % context
@@ -98,11 +98,11 @@ class MessageInline(admin.TabularInline):
class TicketInline(admin.TabularInline):
fields = [
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
- 'colored_priority', 'created', 'last_modified'
+ 'colored_priority', 'created', 'updated'
]
readonly_fields = [
'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state',
- 'colored_priority', 'created', 'last_modified'
+ 'colored_priority', 'created', 'updated'
]
model = Ticket
extra = 0
@@ -110,8 +110,8 @@ class TicketInline(admin.TabularInline):
creator_link = admin_link('creator')
owner_link = admin_link('owner')
- created = admin_link('created_on')
- last_modified = admin_link('last_modified_on')
+ created = admin_link('created_at')
+ updated = admin_link('updated_at')
colored_state = admin_colored('state', colors=STATE_COLORS, bold=False)
colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
@@ -121,10 +121,10 @@ class TicketInline(admin.TabularInline):
ticket_id.allow_tags = True
-class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions,
+class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
list_display = [
'unbold_id', 'bold_subject', 'display_creator', 'display_owner',
- 'display_queue', 'display_priority', 'display_state', 'last_modified'
+ 'display_queue', 'display_priority', 'display_state', 'updated'
]
list_display_links = ('unbold_id', 'bold_subject')
list_filter = [
@@ -134,7 +134,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
('my_tickets', lambda r: 'True' if not r.user.is_superuser else 'False'),
('state', 'OPEN')
)
- date_hierarchy = 'created_on'
+ date_hierarchy = 'created_at'
search_fields = [
'id', 'subject', 'creator__username', 'creator__email', 'queue__name',
'owner__username'
@@ -192,20 +192,20 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
display_creator = admin_link('creator')
display_queue = admin_link('queue')
display_owner = admin_link('owner')
- last_modified = admin_date('last_modified_on')
+ updated = admin_date('updated')
display_state = admin_colored('state', colors=STATE_COLORS, bold=False)
display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False)
def display_summary(self, ticket):
context = {
'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name,
- 'created': admin_date('created_on')(ticket),
+ 'created': admin_date('created_at')(ticket),
'updated': '',
}
msg = ticket.messages.last()
if msg:
context.update({
- 'updated': admin_date('created_on')(msg),
+ 'updated': admin_date('created_at')(msg),
'updater': admin_link('author')(self, msg) if msg.author else msg.author_name,
})
context['updated'] = '. Updated by %(updater)s about %(updated)s' % context
@@ -283,7 +283,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView
def message_preview_view(self, request):
""" markdown preview render via ajax """
data = request.POST.get("data")
- data_formated = markdowt_tn(strip_tags(data))
+ data_formated = markdown(strip_tags(data))
return HttpResponse(data_formated)
def get_queryset(self, request):
diff --git a/orchestra/apps/issues/forms.py b/orchestra/apps/issues/forms.py
index 5d26db9c..ae0dcafc 100644
--- a/orchestra/apps/issues/forms.py
+++ b/orchestra/apps/issues/forms.py
@@ -1,11 +1,9 @@
from django import forms
-from django.core.urlresolvers import reverse
from django.utils.html import strip_tags
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from markdown import markdown
-from orchestra.admin.utils import change_url
from orchestra.apps.users.models import User
from orchestra.forms.widgets import ReadOnlyWidget
@@ -42,7 +40,6 @@ class MessageInlineForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MessageInlineForm, self).__init__(*args, **kwargs)
- admin_link = change_url(self.user)
self.fields['created_on'].widget = ReadOnlyWidget('')
def clean_content(self):
diff --git a/orchestra/apps/issues/models.py b/orchestra/apps/issues/models.py
index 2341da5b..0a83ecc2 100644
--- a/orchestra/apps/issues/models.py
+++ b/orchestra/apps/issues/models.py
@@ -1,6 +1,5 @@
from django.conf import settings as djsettings
from django.db import models
-from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.contacts import settings as contacts_settings
@@ -68,13 +67,13 @@ class Ticket(models.Model):
priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES,
default=MEDIUM)
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
- created_on = models.DateTimeField(_("created on"), auto_now_add=True)
- last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
+ created_at = models.DateTimeField(_("created"), auto_now_add=True)
+ updated_at = models.DateTimeField(_("modified"), auto_now=True)
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"),
blank=True)
class Meta:
- ordering = ["-last_modified_on"]
+ ordering = ['-updated_at']
def __unicode__(self):
return unicode(self.pk)
diff --git a/orchestra/apps/lists/backends.py b/orchestra/apps/lists/backends.py
index 73b3e40b..d23b686a 100644
--- a/orchestra/apps/lists/backends.py
+++ b/orchestra/apps/lists/backends.py
@@ -1,8 +1,6 @@
import textwrap
-from django.template import Template, Context
from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
diff --git a/orchestra/apps/mails/admin.py b/orchestra/apps/mails/admin.py
index 280e02fe..c00656ae 100644
--- a/orchestra/apps/mails/admin.py
+++ b/orchestra/apps/mails/admin.py
@@ -1,15 +1,12 @@
from django import forms
from django.contrib import admin
-from django.contrib.auth import get_user_model
-from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
-from orchestra.admin.utils import insertattr, admin_link, change_url
+from orchestra.admin.utils import admin_link, change_url
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
-from orchestra.apps.domains.forms import DomainIterator
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
from .models import Mailbox, Address, Autoresponse
diff --git a/orchestra/apps/mails/backends.py b/orchestra/apps/mails/backends.py
index 24a10276..b8724c27 100644
--- a/orchestra/apps/mails/backends.py
+++ b/orchestra/apps/mails/backends.py
@@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from . import settings
+from .models import Address
class MailSystemUserBackend(ServiceController):
@@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor):
)
def get_context(self, mailbox):
- context = MailSystemUserBackend().get_context(site)
+ context = MailSystemUserBackend().get_context(mailbox)
context['home'] = settings.EMAILS_HOME % context
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
context['object_id'] = mailbox.pk
diff --git a/orchestra/apps/mails/models.py b/orchestra/apps/mails/models.py
index 78637dad..70d5778c 100644
--- a/orchestra/apps/mails/models.py
+++ b/orchestra/apps/mails/models.py
@@ -1,6 +1,3 @@
-import re
-
-from django.contrib.auth.hashers import check_password, make_password
from django.core.validators import RegexValidator
from django.db import models
from django.utils.translation import ugettext_lazy as _
diff --git a/orchestra/apps/mails/validators.py b/orchestra/apps/mails/validators.py
index 55d241a4..eab400fa 100644
--- a/orchestra/apps/mails/validators.py
+++ b/orchestra/apps/mails/validators.py
@@ -44,7 +44,6 @@ def validate_forward(value):
def validate_sieve(value):
- from .models import Mailbox
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
with open(path, 'wb') as f:
diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py
index db1632ae..862f12d4 100644
--- a/orchestra/apps/orchestration/admin.py
+++ b/orchestra/apps/orchestration/admin.py
@@ -1,5 +1,4 @@
from django.contrib import admin
-from django.core.urlresolvers import reverse
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
@@ -89,18 +88,18 @@ class BackendLogAdmin(admin.ModelAdmin):
)
list_display_links = ('id', 'backend')
list_filter = ('state', 'backend')
- date_hierarchy = 'last_update'
+ date_hierarchy = 'updated_at'
inlines = [BackendOperationInline]
fields = [
'backend', 'server_link', 'state', 'mono_script', 'mono_stdout',
'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created',
- 'display_last_update', 'execution_time'
+ 'display_updated', 'execution_time'
]
readonly_fields = fields
server_link = admin_link('server')
- display_last_update = admin_date('last_update')
- display_created = admin_date('created')
+ display_updated = admin_date('updated_at')
+ display_created = admin_date('created_at')
display_state = admin_colored('state', colors=STATE_COLORS)
mono_script = display_mono('script')
mono_stdout = display_mono('stdout')
@@ -111,6 +110,9 @@ class BackendLogAdmin(admin.ModelAdmin):
""" Order by structured name and imporve performance """
qs = super(BackendLogAdmin, self).get_queryset(request)
return qs.select_related('server').defer('script', 'stdout')
+
+ def has_add_permission(self, *args, **kwargs):
+ return False
class ServerAdmin(admin.ModelAdmin):
diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py
index 7f351d89..73f68a53 100644
--- a/orchestra/apps/orchestration/backends.py
+++ b/orchestra/apps/orchestration/backends.py
@@ -1,7 +1,6 @@
from functools import partial
from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py
index dcd68c0c..91bf1f93 100644
--- a/orchestra/apps/orchestration/models.py
+++ b/orchestra/apps/orchestration/models.py
@@ -1,7 +1,5 @@
from django.contrib.contenttypes import generic
-from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
@@ -62,8 +60,8 @@ class BackendLog(models.Model):
exit_code = models.IntegerField(_("exit code"), null=True)
task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True,
help_text="Celery task ID when used as execution backend")
- created = models.DateTimeField(_("created"), auto_now_add=True)
- last_update = models.DateTimeField(_("last update"), auto_now=True)
+ created_at = models.DateTimeField(_("created"), auto_now_add=True)
+ updated_at = models.DateTimeField(_("updated"), auto_now=True)
class Meta:
get_latest_by = 'id'
@@ -89,7 +87,7 @@ class BackendOperation(models.Model):
action = models.CharField(_("action"), max_length=64)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
- # TODO rename to content_object
+
instance = generic.GenericForeignKey('content_type', 'object_id')
class Meta:
diff --git a/orchestra/apps/orchestration/tests/test_route.py b/orchestra/apps/orchestration/tests/test_route.py
index 34767203..9a30647d 100644
--- a/orchestra/apps/orchestration/tests/test_route.py
+++ b/orchestra/apps/orchestration/tests/test_route.py
@@ -1,5 +1,3 @@
-from django.db import IntegrityError, transaction
-
from orchestra.utils.tests import BaseTestCase
from .. import operations, backends
diff --git a/orchestra/apps/orders/actions.py b/orchestra/apps/orders/actions.py
index d1317aea..8b031ce8 100644
--- a/orchestra/apps/orders/actions.py
+++ b/orchestra/apps/orders/actions.py
@@ -78,7 +78,7 @@ class BillSelectedOrders(object):
if int(request.POST.get('step')) >= 3:
bills = self.queryset.bill(commit=True, **self.options)
for order in self.queryset:
- modeladmin.log_change(request, order, 'Billed')
+ self.modeladmin.log_change(request, order, 'Billed')
if not bills:
msg = _("Selected orders do not have pending billing")
self.modeladmin.message_user(request, msg, messages.WARNING)
diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py
index 363f2305..6cf277cb 100644
--- a/orchestra/apps/orders/admin.py
+++ b/orchestra/apps/orders/admin.py
@@ -3,7 +3,6 @@ from django.utils import timezone
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
-from orchestra.admin import ChangeListDefaultFilter
from orchestra.admin.utils import admin_link, admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.utils.humanize import naturaldate
@@ -13,7 +12,7 @@ from .filters import ActiveOrderListFilter, BilledOrderListFilter
from .models import Order, MetricStorage
-class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
+class OrderAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = (
'id', 'service', 'account_link', 'content_object_link',
'display_registered_on', 'display_billed_until', 'display_cancelled_on'
@@ -22,9 +21,6 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',)
actions = (BillSelectedOrders(),)
date_hierarchy = 'registered_on'
- default_changelist_filters = (
- ('is_active', 'True'),
- )
content_object_link = admin_link('content_object', order=False)
display_registered_on = admin_date('registered_on')
diff --git a/orchestra/apps/orders/billing.py b/orchestra/apps/orders/billing.py
index 39942379..0ab28688 100644
--- a/orchestra/apps/orders/billing.py
+++ b/orchestra/apps/orders/billing.py
@@ -2,7 +2,7 @@ import datetime
from django.utils.translation import ugettext_lazy as _
-from orchestra.apps.bills.models import Invoice, Fee, ProForma, BillLine, BillSubline
+from orchestra.apps.bills.models import Invoice, Fee, ProForma
class BillsBackend(object):
@@ -39,6 +39,10 @@ class BillsBackend(object):
subtotal=line.subtotal,
tax=service.tax,
description=self.get_line_description(line),
+
+ order=line.order,
+ order_billed_on=line.order.old_billed_on,
+ order_billed_until=line.order.old_billed_until
)
self.create_sublines(billine, line.discounts)
return bills
@@ -46,7 +50,6 @@ class BillsBackend(object):
def format_period(self, ini, end):
ini = ini.strftime("%b, %Y")
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
- # TODO if diff is less than a month: write the month only
if ini == end:
return ini
return _("{ini} to {end}").format(ini=ini, end=end)
@@ -67,6 +70,7 @@ class BillsBackend(object):
def create_sublines(self, line, discounts):
for discount in discounts:
line.sublines.create(
- description=_("Discount per %s") % discount.type,
+ description=_("Discount per %s") % discount.type.lower(),
total=discount.total,
+ type=discount.type,
)
diff --git a/orchestra/apps/orders/filters.py b/orchestra/apps/orders/filters.py
index 87d1c7a7..303d0538 100644
--- a/orchestra/apps/orders/filters.py
+++ b/orchestra/apps/orders/filters.py
@@ -13,7 +13,6 @@ class ActiveOrderListFilter(SimpleListFilter):
return (
('True', _("Active")),
('False', _("Inactive")),
- ('None', _("All")),
)
def queryset(self, request, queryset):
@@ -23,12 +22,6 @@ class ActiveOrderListFilter(SimpleListFilter):
return queryset.inactive()
return queryset
- def choices(self, cl):
- """ Remove default All """
- choices = iter(super(ActiveOrderListFilter, self).choices(cl))
- choices.next()
- return choices
-
class BilledOrderListFilter(SimpleListFilter):
""" Filter tickets by created_by according to request.user """
diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py
index 651d5126..dbbaa764 100644
--- a/orchestra/apps/orders/models.py
+++ b/orchestra/apps/orders/models.py
@@ -1,28 +1,24 @@
import datetime
import decimal
import logging
-import sys
from django.db import models
from django.db.migrations.recorder import MigrationRecorder
from django.db.models import F, Q
from django.db.models.loading import get_model
-from django.db.models.signals import pre_delete, post_delete, post_save
+from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.contrib.admin.models import LogEntry
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
-from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
-from orchestra.core import caches, services, accounts
+from orchestra.core import accounts
from orchestra.models import queryset
-from orchestra.utils.apps import autodiscover
from orchestra.utils.python import import_class
from . import helpers, settings
-from .handlers import ServiceHandler
logger = logging.getLogger(__name__)
@@ -39,8 +35,13 @@ class OrderQuerySet(models.QuerySet):
for account, services in qs.group_by('account', 'service').iteritems():
bill_lines = []
for service, orders in services.iteritems():
+ for order in orders:
+ # Saved for undoing support
+ order.old_billed_on = order.billed_on
+ order.old_billed_until = order.billed_until
lines = service.handler.generate_bill_lines(orders, account, **options)
bill_lines.extend(lines)
+ # TODO make this consistent always returning the same fucking objects
if commit:
bills += bill_backend.create_bills(account, bill_lines, **options)
else:
@@ -73,7 +74,7 @@ class OrderQuerySet(models.QuerySet):
def inactive(self, **kwargs):
""" return inactive orders """
- return self.filter(cancelled_on__lt=timezone.now(), **kwargs)
+ return self.filter(cancelled_on__lte=timezone.now(), **kwargs)
class Order(models.Model):
diff --git a/orchestra/apps/orders/settings.py b/orchestra/apps/orders/settings.py
index beb06d73..539a8614 100644
--- a/orchestra/apps/orders/settings.py
+++ b/orchestra/apps/orders/settings.py
@@ -1,5 +1,4 @@
from django.conf import settings
-from django.utils.translation import ugettext_lazy as _
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
diff --git a/orchestra/apps/payments/actions.py b/orchestra/apps/payments/actions.py
index 4896a5f4..258ade55 100644
--- a/orchestra/apps/payments/actions.py
+++ b/orchestra/apps/payments/actions.py
@@ -26,8 +26,8 @@ def process_transactions(modeladmin, request, queryset):
method = PaymentMethod.get_plugin(method)
procs = method.process(transactions)
processes += procs
- for transaction in transactions:
- modeladmin.log_change(request, transaction, 'Processed')
+ for trans in transactions:
+ modeladmin.log_change(request, trans, 'Processed')
if not processes:
return
opts = modeladmin.model._meta
@@ -44,9 +44,9 @@ def process_transactions(modeladmin, request, queryset):
@transaction.atomic
@action_with_confirmation()
def mark_as_executed(modeladmin, request, queryset, extra_context={}):
- for transaction in queryset:
- transaction.mark_as_executed()
- modeladmin.log_change(request, transaction, 'Executed')
+ for trans in queryset:
+ trans.mark_as_executed()
+ modeladmin.log_change(request, trans, 'Executed')
msg = _("%s selected transactions have been marked as executed.") % queryset.count()
modeladmin.message_user(request, msg)
mark_as_executed.url_name = 'execute'
@@ -56,9 +56,9 @@ mark_as_executed.verbose_name = _("Mark as executed")
@transaction.atomic
@action_with_confirmation()
def mark_as_secured(modeladmin, request, queryset):
- for transaction in queryset:
- transaction.mark_as_secured()
- modeladmin.log_change(request, transaction, 'Secured')
+ for trans in queryset:
+ trans.mark_as_secured()
+ modeladmin.log_change(request, trans, 'Secured')
msg = _("%s selected transactions have been marked as secured.") % queryset.count()
modeladmin.message_user(request, msg)
mark_as_secured.url_name = 'secure'
@@ -68,9 +68,9 @@ mark_as_secured.verbose_name = _("Mark as secured")
@transaction.atomic
@action_with_confirmation()
def mark_as_rejected(modeladmin, request, queryset):
- for transaction in queryset:
- transaction.mark_as_rejected()
- modeladmin.log_change(request, transaction, 'Rejected')
+ for trans in queryset:
+ trans.mark_as_rejected()
+ modeladmin.log_change(request, trans, 'Rejected')
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
modeladmin.message_user(request, msg)
mark_as_rejected.url_name = 'reject'
@@ -90,7 +90,7 @@ def _format_display_objects(modeladmin, request, queryset, related):
for related in getattr(obj.transactions, attr)():
subobjects.append(
mark_safe('{0}: {2} will be marked as {3}'.format(
- capfirst(subobj.get_type().lower()), change_url(subobj), subobj, verb))
+ capfirst(related.get_type().lower()), change_url(related), related, verb))
)
objects.append(subobjects)
return {'display_objects': objects}
@@ -127,9 +127,9 @@ abort.verbose_name = _("Abort")
@transaction.atomic
@action_with_confirmation(extra_context=_format_commit)
def commit(modeladmin, request, queryset):
- for transaction in queryset:
- transaction.mark_as_rejected()
- modeladmin.log_change(request, transaction, 'Rejected')
+ for trans in queryset:
+ trans.mark_as_rejected()
+ modeladmin.log_change(request, trans, 'Rejected')
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
modeladmin.message_user(request, msg)
commit.url_name = 'commit'
diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py
index 68473204..18335ef5 100644
--- a/orchestra/apps/payments/admin.py
+++ b/orchestra/apps/payments/admin.py
@@ -1,4 +1,3 @@
-from django import forms
from django.conf.urls import patterns, url
from django.contrib import admin
from django.core.urlresolvers import reverse
@@ -37,7 +36,6 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
def get_urls(self):
""" Hooks select account url """
urls = super(PaymentSourceAdmin, self).get_urls()
- admin_site = self.admin_site
opts = self.model._meta
info = opts.app_label, opts.model_name
select_urls = patterns("",
diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py
index 8cb0d92b..5eb55601 100644
--- a/orchestra/apps/payments/models.py
+++ b/orchestra/apps/payments/models.py
@@ -1,6 +1,5 @@
from django.core.exceptions import ValidationError
from django.db import models
-from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField
@@ -99,8 +98,8 @@ class Transaction(models.Model):
default=WAITTING_PROCESSING)
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
- created_on = models.DateTimeField(auto_now_add=True)
- modified_on = models.DateTimeField(auto_now=True)
+ created_at = models.DateTimeField(_("created"), auto_now_add=True)
+ modified_at = models.DateTimeField(_("modified"), auto_now=True)
objects = TransactionQuerySet.as_manager()
diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py
index 2914d265..9c558246 100644
--- a/orchestra/apps/resources/admin.py
+++ b/orchestra/apps/resources/admin.py
@@ -15,16 +15,16 @@ from .models import Resource, ResourceData, MonitorData
class ResourceAdmin(ExtendedModelAdmin):
list_display = (
- 'id', 'verbose_name', 'content_type', 'period', 'ondemand',
+ 'id', 'verbose_name', 'content_type', 'period', 'on_demand',
'default_allocation', 'unit', 'disable_trigger', 'crontab',
)
- list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger')
+ list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger')
fieldsets = (
(None, {
'fields': ('name', 'content_type', 'period'),
}),
(_("Configuration"), {
- 'fields': ('verbose_name', 'unit', 'scale', 'ondemand',
+ 'fields': ('verbose_name', 'unit', 'scale', 'on_demand',
'default_allocation', 'disable_trigger', 'is_active'),
}),
(_("Monitoring"), {
@@ -64,7 +64,7 @@ class ResourceAdmin(ExtendedModelAdmin):
class ResourceDataAdmin(admin.ModelAdmin):
list_display = (
- 'id', 'resource', 'used', 'allocated', 'last_update', 'content_object_link'
+ 'id', 'resource', 'used', 'allocated', 'updated_at', 'content_object_link'
)
list_filter = ('resource',)
readonly_fields = ('content_object_link',)
@@ -77,7 +77,7 @@ class ResourceDataAdmin(admin.ModelAdmin):
class MonitorDataAdmin(admin.ModelAdmin):
- list_display = ('id', 'monitor', 'date', 'value', 'content_object_link')
+ list_display = ('id', 'monitor', 'created_at', 'value', 'content_object_link')
list_filter = ('monitor',)
readonly_fields = ('content_object_link',)
@@ -118,16 +118,16 @@ def resource_inline_factory(resources):
formset = ResourceInlineFormSet
can_delete = False
fields = (
- 'verbose_name', 'used', 'display_last_update', 'allocated', 'unit'
+ 'verbose_name', 'used', 'display_updated', 'allocated', 'unit'
)
- readonly_fields = ('used', 'display_last_update')
+ readonly_fields = ('used', 'display_updated')
class Media:
css = {
'all': ('orchestra/css/hide-inline-id.css',)
}
- display_last_update = admin_date('last_update', default=_("Never"))
+ display_updated = admin_date('updated_at', default=_("Never"))
def has_add_permission(self, *args, **kwargs):
""" Hidde add another """
diff --git a/orchestra/apps/resources/apps.py b/orchestra/apps/resources/apps.py
index 36fc3aed..1b5d8199 100644
--- a/orchestra/apps/resources/apps.py
+++ b/orchestra/apps/resources/apps.py
@@ -1,5 +1,4 @@
from django.apps import AppConfig
-from django.contrib.contenttypes import generic
from orchestra.utils import running_syncdb
diff --git a/orchestra/apps/resources/forms.py b/orchestra/apps/resources/forms.py
index 7caa529d..abc4d149 100644
--- a/orchestra/apps/resources/forms.py
+++ b/orchestra/apps/resources/forms.py
@@ -1,7 +1,5 @@
from django import forms
-from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
-from djcelery.humanize import naturaldate
from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
@@ -21,7 +19,7 @@ class ResourceForm(forms.ModelForm):
if self.resource:
self.fields['verbose_name'].initial = self.resource.verbose_name
self.fields['unit'].initial = self.resource.unit
- if self.resource.ondemand:
+ if self.resource.on_demand:
self.fields['allocated'].required = False
self.fields['allocated'].widget = ReadOnlyWidget(None, '')
else:
diff --git a/orchestra/apps/resources/helpers.py b/orchestra/apps/resources/helpers.py
index be1d96ac..c4da0d9d 100644
--- a/orchestra/apps/resources/helpers.py
+++ b/orchestra/apps/resources/helpers.py
@@ -38,13 +38,15 @@ def compute_resource_usage(data):
continue
has_result = True
epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc)
- total = (epoch-last.date).total_seconds()
- dataset = dataset.filter(date__year=today.year, date__month=today.month)
+ total = (last.created_at-epoch).total_seconds()
+ dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month)
+ ini = epoch
for data in dataset:
- slot = (previous-data.date).total_seconds()
+ slot = (data.created_at-ini).total_seconds()
result += data.value * slot/total
+ ini = data.created_at
elif resource.period == resource.MONTHLY_SUM:
- dataset = dataset.filter(date__year=today.year, date__month=today.month)
+ dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month)
# FIXME Aggregation of 0s returns None! django bug?
# value = dataset.aggregate(models.Sum('value'))['value__sum']
values = dataset.values_list('value', flat=True)
diff --git a/orchestra/apps/resources/migrations/0001_initial.py b/orchestra/apps/resources/migrations/0001_initial.py
new file mode 100644
index 00000000..17be9bb7
--- /dev/null
+++ b/orchestra/apps/resources/migrations/0001_initial.py
@@ -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')]),
+ ),
+ ]
diff --git a/orchestra/apps/resources/migrations/0002_auto_20140926_1143.py b/orchestra/apps/resources/migrations/0002_auto_20140926_1143.py
new file mode 100644
index 00000000..5e2bc3f6
--- /dev/null
+++ b/orchestra/apps/resources/migrations/0002_auto_20140926_1143.py
@@ -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',
+ ),
+ ]
diff --git a/orchestra/apps/resources/migrations/0003_auto_20140926_1325.py b/orchestra/apps/resources/migrations/0003_auto_20140926_1325.py
new file mode 100644
index 00000000..ee2eab56
--- /dev/null
+++ b/orchestra/apps/resources/migrations/0003_auto_20140926_1325.py
@@ -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'),
+ ),
+ ]
diff --git a/orchestra/apps/resources/migrations/__init__.py b/orchestra/apps/resources/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py
index 108e0b92..af2de256 100644
--- a/orchestra/apps/resources/models.py
+++ b/orchestra/apps/resources/models.py
@@ -43,8 +43,7 @@ class Resource(models.Model):
default=LAST,
help_text=_("Operation used for aggregating this resource monitored"
"data."))
- # TODO rename to on_deman
- ondemand = models.BooleanField(_("on demand"), default=False,
+ on_demand = models.BooleanField(_("on demand"), default=False,
help_text=_("If enabled the resource will not be pre-allocated, "
"but allocated under the application demand"))
default_allocation = models.PositiveIntegerField(_("default allocation"),
@@ -116,12 +115,12 @@ class Resource(models.Model):
class ResourceData(models.Model):
""" Stores computed resource usage and allocation """
- resource = models.ForeignKey(Resource, related_name='dataset')
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- used = models.PositiveIntegerField(null=True)
- last_update = models.DateTimeField(null=True)
- allocated = models.PositiveIntegerField(null=True, blank=True)
+ resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
+ content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
+ object_id = models.PositiveIntegerField(_("object id"))
+ used = models.PositiveIntegerField(_("used"), null=True)
+ updated_at = models.DateTimeField(_("updated"), null=True)
+ allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True)
content_object = GenericForeignKey()
@@ -146,7 +145,7 @@ class ResourceData(models.Model):
if current is None:
current = self.get_used()
self.used = current or 0
- self.last_update = timezone.now()
+ self.updated_at = timezone.now()
self.save()
@@ -154,9 +153,9 @@ class MonitorData(models.Model):
""" Stores monitored data """
monitor = models.CharField(_("monitor"), max_length=256,
choices=ServiceMonitor.get_plugin_choices())
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- date = models.DateTimeField(_("date"), auto_now_add=True)
+ content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
+ object_id = models.PositiveIntegerField(_("object id"))
+ created_at = models.DateTimeField(_("created"), auto_now_add=True)
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
content_object = GenericForeignKey()
diff --git a/orchestra/apps/resources/serializers.py b/orchestra/apps/resources/serializers.py
index 64fbaa4d..a427fa62 100644
--- a/orchestra/apps/resources/serializers.py
+++ b/orchestra/apps/resources/serializers.py
@@ -44,12 +44,12 @@ if not running_syncdb():
msg = "Unknown or duplicated resource '%s'." % resource
raise serializers.ValidationError(msg)
resources.remove(resource)
- if not resource.ondemand and not data.allocated:
+ if not resource.on_demand and not data.allocated:
data.allocated = resource.default_allocation
result.append(data)
for resource in resources:
data = ResourceData(resource=resource)
- if not resource.ondemand:
+ if not resource.on_demand:
data.allocated = resource.default_allocation
result.append(data)
attrs[source] = result
@@ -64,7 +64,7 @@ if not running_syncdb():
ret['available_resources'] = [
{
'name': resource.name,
- 'ondemand': resource.ondemand,
+ 'on_demand': resource.on_demand,
'default_allocation': resource.default_allocation
} for resource in resources
]
diff --git a/orchestra/apps/resources/tasks.py b/orchestra/apps/resources/tasks.py
index 414e7edf..5719f8bd 100644
--- a/orchestra/apps/resources/tasks.py
+++ b/orchestra/apps/resources/tasks.py
@@ -1,6 +1,5 @@
from celery import shared_task
from django.db.models.loading import get_model
-from django.utils import timezone
from orchestra.apps.orchestration.models import BackendOperation as Operation
@@ -34,7 +33,7 @@ def monitor(resource_id):
operations.append(op)
elif data.used < data.allocated:
op = Operation.create(backend, obj, Operation.RECOVERY)
- operation.append(op)
+ operations.append(op)
# data = ResourceData.get_or_create(obj, resource)
# current = data.get_used()
# if not resource.disable_trigger:
diff --git a/orchestra/apps/services/actions.py b/orchestra/apps/services/actions.py
index 0b84330f..33186f31 100644
--- a/orchestra/apps/services/actions.py
+++ b/orchestra/apps/services/actions.py
@@ -1,5 +1,5 @@
-from django.contrib import messages
from django.db import transaction
+from django.template.response import TemplateResponse
from django.utils.translation import ugettext_lazy as _
@@ -7,8 +7,23 @@ from django.utils.translation import ugettext_lazy as _
def update_orders(modeladmin, request, queryset):
for service in queryset:
service.update_orders()
- modeladmin.log_change(request, transaction, 'Update orders')
+ modeladmin.log_change(request, service, 'Update orders')
msg = _("Orders for %s selected services have been updated.") % queryset.count()
modeladmin.message_user(request, msg)
update_orders.url_name = 'update-orders'
update_orders.verbose_name = _("Update orders")
+
+
+def view_help(modeladmin, request, queryset):
+ opts = modeladmin.model._meta
+ context = {
+ 'title': _("Need some help?"),
+ 'opts': opts,
+ 'queryset': queryset,
+ 'obj': queryset.get(),
+ 'action_name': _("help"),
+ 'app_label': opts.app_label,
+ }
+ return TemplateResponse(request, 'admin/services/service/help.html', context)
+view_help.url_name = 'help'
+view_help.verbose_name = _("Help")
diff --git a/orchestra/apps/services/admin.py b/orchestra/apps/services/admin.py
index 555995b2..3334b5c9 100644
--- a/orchestra/apps/services/admin.py
+++ b/orchestra/apps/services/admin.py
@@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services
-from .actions import update_orders
+from .actions import update_orders, view_help
from .models import Plan, ContractedPlan, Rate, Service
@@ -52,7 +52,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
)
inlines = [RateInline]
actions = [update_orders]
- change_view_actions = actions
+ change_view_actions = actions + [view_help]
def formfield_for_dbfield(self, db_field, **kwargs):
""" Improve performance of account field and filter by account """
diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py
index 3434efce..c26eff6d 100644
--- a/orchestra/apps/services/handlers.py
+++ b/orchestra/apps/services/handlers.py
@@ -4,12 +4,11 @@ import decimal
from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType
-from django.db.models import Q
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
-from orchestra.utils.python import AttributeDict
+from orchestra.utils.python import AttrDict
from . import settings, helpers
@@ -21,6 +20,8 @@ class ServiceHandler(plugins.Plugin):
Relax and enjoy the journey.
"""
+ _VOLUME = 'VOLUME'
+ _COMPENSATION = 'COMPENSATION'
model = None
@@ -160,7 +161,7 @@ class ServiceHandler(plugins.Plugin):
return None
def generate_discount(self, line, dtype, price):
- line.discounts.append(AttributeDict(**{
+ line.discounts.append(AttrDict(**{
'type': dtype,
'total': price,
}))
@@ -182,7 +183,7 @@ class ServiceHandler(plugins.Plugin):
if not computed:
price = price * size
subtotal = self.nominal_price * size * metric
- line = AttributeDict(**{
+ line = AttrDict(**{
'order': order,
'subtotal': subtotal,
'ini': ini,
@@ -197,7 +198,7 @@ class ServiceHandler(plugins.Plugin):
discounted += dprice
subtotal += discounted
if subtotal > price:
- self.generate_discount(line, 'volume', price-subtotal)
+ self.generate_discount(line, self._VOLUME, price-subtotal)
return line
def assign_compensations(self, givers, receivers, **options):
@@ -225,7 +226,6 @@ class ServiceHandler(plugins.Plugin):
def apply_compensations(self, order, only_beyond=False):
dsize = 0
- discounts = ()
ini = order.billed_until or order.registered_on
end = order.new_billed_until
beyond = end
@@ -296,7 +296,7 @@ class ServiceHandler(plugins.Plugin):
cprice += dsize*price
if cprice:
discounts = (
- ('compensation', -cprice),
+ (self._COMPENSATION, -cprice),
)
if new_end:
size = self.get_price_size(order.new_billed_until, new_end)
@@ -323,11 +323,12 @@ class ServiceHandler(plugins.Plugin):
discounts = ()
dsize, new_end = self.apply_compensations(order)
if dsize:
- discounts=(('compensation', -dsize*price),)
+ discounts=(
+ (self._COMPENSATION, -dsize*price),
+ )
if new_end:
order.new_billed_until = new_end
end = new_end
- size = self.get_price_size(ini, end)
line = self.generate_line(order, price, ini, end, discounts=discounts)
lines.append(line)
return lines
@@ -395,7 +396,7 @@ class ServiceHandler(plugins.Plugin):
dsize, new_end = self.apply_compensations(order)
if dsize:
discounts=(
- ('compensation', -dsize*price),
+ (self._COMPENSATION, -dsize*price),
)
if new_end:
order.new_billed_until = new_end
diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py
index 80e72eb7..e8dec4f1 100644
--- a/orchestra/apps/services/models.py
+++ b/orchestra/apps/services/models.py
@@ -1,24 +1,18 @@
import decimal
-import sys
from django.db import models
-from django.db.models import F, Q
+from django.db.models import Q
from django.db.models.loading import get_model
-from django.db.models.signals import pre_delete, post_delete, post_save
-from django.dispatch import receiver
-from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
-from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.core import caches, services, accounts
from orchestra.models import queryset
from orchestra.utils.apps import autodiscover
-from orchestra.utils.python import import_class
-from . import helpers, settings, rating
+from . import settings, rating
from .handlers import ServiceHandler
@@ -329,7 +323,7 @@ class Service(models.Model):
def update_orders(self):
order_model = get_model(settings.SERVICES_ORDER_MODEL)
related_model = self.content_type.model_class()
- for instance in related_model.objects.all():
+ for instance in related_model.objects.all().select_related('account__user'):
order_model.update_orders(instance, service=self)
diff --git a/orchestra/apps/services/rating.py b/orchestra/apps/services/rating.py
index 369e5a57..4b6bc0b6 100644
--- a/orchestra/apps/services/rating.py
+++ b/orchestra/apps/services/rating.py
@@ -1,6 +1,6 @@
import sys
-from orchestra.utils.python import AttributeDict
+from orchestra.utils.python import AttrDict
def _compute(rates, metric):
@@ -30,7 +30,7 @@ def _compute(rates, metric):
quantity = metric - accumulated
end = True
price = rates[ix].price
- steps.append(AttributeDict(**{
+ steps.append(AttrDict(**{
'quantity': quantity,
'price': price,
'barrier': barrier,
@@ -109,7 +109,7 @@ def step_price(rates, metric):
if result and result[-1].price == price:
result[-1].quantity += quantity
else:
- result.append(AttributeDict(quantity=quantity, price=price))
+ result.append(AttrDict(quantity=quantity, price=price))
ix = 0
targets = []
else:
@@ -139,7 +139,7 @@ def match_price(rates, metric):
candidates.append(prev)
candidates.sort(key=lambda r: r.price)
if candidates:
- return [AttributeDict(**{
+ return [AttrDict(**{
'quantity': metric,
'price': candidates[0].price,
})]
diff --git a/orchestra/apps/services/static/services/img/services.png b/orchestra/apps/services/static/services/img/services.png
new file mode 100644
index 00000000..0cf8792c
Binary files /dev/null and b/orchestra/apps/services/static/services/img/services.png differ
diff --git a/orchestra/apps/services/static/services/img/services.svg b/orchestra/apps/services/static/services/img/services.svg
new file mode 100644
index 00000000..0b40c712
--- /dev/null
+++ b/orchestra/apps/services/static/services/img/services.svg
@@ -0,0 +1,485 @@
+
+
+
+
diff --git a/orchestra/apps/services/templates/admin/services/service/help.html b/orchestra/apps/services/templates/admin/services/service/help.html
new file mode 100644
index 00000000..069252b9
--- /dev/null
+++ b/orchestra/apps/services/templates/admin/services/service/help.html
@@ -0,0 +1,14 @@
+{% extends "admin/orchestra/generic_confirmation.html" %}
+{% load i18n l10n %}
+{% load url from future %}
+{% load admin_urls static utils %}
+
+
+{% block content %}
+