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):