diff --git a/TODO.md b/TODO.md
index 3312404f..403fd06c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -355,5 +355,3 @@ make django admin taskstate uncollapse fucking traceback, ( if exists ?)
# backend.context and backned.instance provided when an action is called? like forms.cleaned_data: do it on manager.generation(backend.context = backend.get_context()) or in backend.__getattr__ ? also backend.head,tail,content switching on manager.generate()?
# replace return_code by exit_code everywhere
-
-# plan.rate registry
diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py
index 92506be2..fc980c16 100644
--- a/orchestra/contrib/orchestration/manager.py
+++ b/orchestra/contrib/orchestration/manager.py
@@ -105,7 +105,7 @@ def execute(scripts, serialize=False, async=None):
async: do not join threads (overrides route.async)
"""
if settings.ORCHESTRATION_DISABLE_EXECUTION:
- logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION settings.')
+ logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION.')
return []
# Execute scripts on each server
executions = []
@@ -122,6 +122,7 @@ def execute(scripts, serialize=False, async=None):
kwargs = {
'async': async,
}
+ # we clone the connection just in case we are isolated inside a transaction
with db.clone(model=BackendLog) as handle:
log = backend.create_log(*args, using=handle.target)
log._state.db = handle.origin
diff --git a/orchestra/contrib/plans/admin.py b/orchestra/contrib/plans/admin.py
index 1ae7de53..16d08931 100644
--- a/orchestra/contrib/plans/admin.py
+++ b/orchestra/contrib/plans/admin.py
@@ -30,6 +30,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin):
list_select_related = ('plan', 'account')
search_fields = ('account__username', 'plan__name', 'id')
+
admin.site.register(Plan, PlanAdmin)
admin.site.register(ContractedPlan, ContractedPlanAdmin)
diff --git a/orchestra/contrib/plans/models.py b/orchestra/contrib/plans/models.py
index 4e5d68f0..b083a570 100644
--- a/orchestra/contrib/plans/models.py
+++ b/orchestra/contrib/plans/models.py
@@ -6,8 +6,10 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.core.validators import validate_name
from orchestra.models import queryset
+from orchestra.utils.functional import cached
+from orchestra.utils.python import import_class
-from . import rating
+from . import settings
class Plan(models.Model):
@@ -66,15 +68,6 @@ class RateQuerySet(models.QuerySet):
class Rate(models.Model):
- STEP_PRICE = 'STEP_PRICE'
- MATCH_PRICE = 'MATCH_PRICE'
- BEST_PRICE = 'BEST_PRICE'
- RATE_METHODS = {
- STEP_PRICE: rating.step_price,
- MATCH_PRICE: rating.match_price,
- BEST_PRICE: rating.best_price,
- }
-
service = models.ForeignKey('services.Service', verbose_name=_("service"),
related_name='rates')
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates')
@@ -91,12 +84,18 @@ class Rate(models.Model):
return "{}-{}".format(str(self.price), self.quantity)
@classmethod
+ @cached
def get_methods(cls):
- return cls.RATE_METHODS
+ return dict((method, import_class(method)) for method in settings.PLANS_RATE_METHODS)
@classmethod
+ @cached
def get_choices(cls):
choices = []
- for name, method in cls.RATE_METHODS.items():
+ for name, method in cls.get_methods().items():
choices.append((name, method.verbose_name))
return choices
+
+ @classmethod
+ def get_default(cls):
+ return settings.PLANS_DEFAULT_RATE_METHOD
diff --git a/orchestra/contrib/plans/settings.py b/orchestra/contrib/plans/settings.py
new file mode 100644
index 00000000..f787b86d
--- /dev/null
+++ b/orchestra/contrib/plans/settings.py
@@ -0,0 +1,15 @@
+from orchestra.contrib.settings import Setting
+
+
+PLANS_RATE_METHODS = Setting('PLANS_RATE_METHODS',
+ (
+ 'orchestra.contrib.plans.rating.step_price',
+ 'orchestra.contrib.plans.rating.match_price',
+ 'orchestra.contrib.plans.rating.best_price',
+ )
+)
+
+
+PLANS_DEFAULT_RATE_METHOD = Setting('PLANS_DEFAULT_RATE_METHOD',
+ 'orchestra.contrib.plans.rating.step_price',
+)
diff --git a/orchestra/contrib/services/migrations/0002_auto_20150509_1501.py b/orchestra/contrib/services/migrations/0002_auto_20150509_1501.py
new file mode 100644
index 00000000..26e2d632
--- /dev/null
+++ b/orchestra/contrib/services/migrations/0002_auto_20150509_1501.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('services', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='service',
+ name='rate_algorithm',
+ field=models.CharField(choices=[('orchestra.contrib.plans.rating.best_price', 'Best price'), ('orchestra.contrib.plans.rating.step_price', 'Step price'), ('orchestra.contrib.plans.rating.match_price', 'Match price')], help_text='Algorithm used to interprete the rating table.
Best price: Produces the best possible price given all active rating lines.
Step price: All rates with a quantity lower than the metric are applied. Nominal price will be used when initial block is missing.
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm', default='orchestra.contrib.plans.rating.step_price'),
+ ),
+ migrations.AlterField(
+ model_name='service',
+ name='tax',
+ field=models.PositiveIntegerField(choices=[(0, 'Duty free'), (21, '21%')], verbose_name='tax', default=21),
+ ),
+ ]
diff --git a/orchestra/contrib/services/models.py b/orchestra/contrib/services/models.py
index 862f026d..fdca42ef 100644
--- a/orchestra/contrib/services/models.py
+++ b/orchestra/contrib/services/models.py
@@ -127,11 +127,13 @@ class Service(models.Model):
(ANUAL, _("Anual data")),
),
default=BILLING_PERIOD)
- rate_algorithm = models.CharField(_("rate algorithm"), max_length=16,
+ rate_algorithm = models.CharField(_("rate algorithm"), max_length=64,
+ choices=rate_class.get_choices(),
+ default=rate_class.get_default(),
help_text=string_concat(_("Algorithm used to interprete the rating table."), *[
string_concat('
', method.verbose_name, ': ', method.help_text)
for name, method in rate_class.get_methods().items()
- ]), choices=rate_class.get_choices(), default=rate_class.get_choices()[0][0])
+ ]))
on_cancel = models.CharField(_("on cancel"), max_length=16,
help_text=_("Defines the cancellation behaviour of this service."),
choices=(
diff --git a/orchestra/contrib/tasks/decorators.py b/orchestra/contrib/tasks/decorators.py
index c42ad824..4a662d69 100644
--- a/orchestra/contrib/tasks/decorators.py
+++ b/orchestra/contrib/tasks/decorators.py
@@ -1,3 +1,4 @@
+import logging
import traceback
from functools import partial, wraps, update_wrapper
from multiprocessing import Process
@@ -15,6 +16,9 @@ from orchestra.utils.python import AttrDict, OrderedSet
from .utils import get_name, get_id
+logger = logging.getLogger(__name__)
+
+
def keep_state(fn):
""" logs task on djcelery's TaskState model """
@wraps(fn)
@@ -30,14 +34,14 @@ def keep_state(fn):
try:
result = fn(*args, **kwargs)
except:
+ trace = traceback.format_exc()
+ subject = 'EXCEPTION executing task %s(args=%s, kwargs=%s)' % (_name, str(args), str(kwargs))
+ logger.error(subject)
+ logger.error(trace)
state.state = states.FAILURE
state.traceback = trace
state.runtime = (timezone.now()-now).total_seconds()
state.save()
- subject = 'EXCEPTION executing task %s(args=%s, kwargs=%s)' % (name, str(args), str(kwargs))
- trace = traceback.format_exc()
- logger.error(subject)
- logger.error(trace)
mail_admins(subject, trace)
raise
else:
diff --git a/orchestra/utils/sys.py b/orchestra/utils/sys.py
index d17ea275..29dbed56 100644
--- a/orchestra/utils/sys.py
+++ b/orchestra/utils/sys.py
@@ -6,6 +6,7 @@ import re
import select
import subprocess
import sys
+import time
from django.core.management.base import CommandError
@@ -165,6 +166,10 @@ def touch(fname, mode=0o666, dir_fd=None, **kwargs):
dir_fd=None if os.supports_fd else dir_fd, **kwargs)
+class OperationLocked(Exception):
+ pass
+
+
class LockFile(object):
""" File-based lock mechanism used for preventing concurrency problems """
def __init__(self, lockfile, expire=5*60, unlocked=False):
@@ -188,8 +193,8 @@ class LockFile(object):
def __enter__(self):
if not self.unlocked:
if not self.acquire():
- raise OperationLocked('%s lock file exists and its mtime is less '
- 'than %s seconds' % (self.lockfile, self.expire))
+ raise OperationLocked("%s lock file exists and its mtime is less than %s seconds" %
+ (self.lockfile, self.expire))
return True
def __exit__(self, type, value, traceback):