Refactored model classmethods to manager methos
This commit is contained in:
parent
ad37c5fd71
commit
8753b94b8c
7
TODO.md
7
TODO.md
|
@ -407,8 +407,7 @@ Case
|
|||
|
||||
# Don't enforce one contact per account? remove account.email in favour of contacts?
|
||||
|
||||
#change class LogEntry(models.Model):
|
||||
action_time = models.DateTimeField(_('action time'), auto_now=True) to auto_now_add
|
||||
|
||||
# Model operations on Manager instead of model method
|
||||
# Mailer: mark as sent
|
||||
|
||||
# Pending filter filter out orders zero metric from pending
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ from django.utils.html import escape
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
|
||||
from ..utils.python import random_ascii
|
||||
from ..utils.python import random_ascii, pairwise
|
||||
|
||||
from .forms import AdminPasswordChangeForm
|
||||
#from django.contrib.auth.forms import AdminPasswordChangeForm
|
||||
|
@ -70,6 +70,31 @@ class AtLeastOneRequiredInlineFormSet(BaseInlineFormSet):
|
|||
raise forms.ValidationError('At least one item required.')
|
||||
|
||||
|
||||
class EnhaceSearchMixin(object):
|
||||
def lookup_allowed(self, lookup, value):
|
||||
""" allows any lookup """
|
||||
if 'password' in lookup:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_search_results(self, request, queryset, search_term):
|
||||
""" allows to specify field <field_name>:<search_term> """
|
||||
search_fields = self.get_search_fields(request)
|
||||
if ':' in search_term:
|
||||
fields = {field.split('__')[0]: field for field in search_fields}
|
||||
new_search_term = []
|
||||
for part in search_term.split():
|
||||
cur_search_term = ''
|
||||
for field, term in pairwise(part.split(':')):
|
||||
if field in fields:
|
||||
queryset = queryset.filter(**{'%s__icontains' % fields[field]: term})
|
||||
else:
|
||||
cur_search_term += ':'.join((field, term))
|
||||
new_search_term.append(cur_search_term)
|
||||
search_term = ' '.join(new_search_term)
|
||||
return super(EnhaceSearchMixin, self).get_search_results(request, queryset, search_term)
|
||||
|
||||
|
||||
class ChangeViewActionsMixin(object):
|
||||
""" Makes actions visible on the admin change view page. """
|
||||
change_view_actions = ()
|
||||
|
@ -176,7 +201,11 @@ class ChangeAddFieldsMixin(object):
|
|||
return super(ChangeAddFieldsMixin, self).get_form(request, obj, **defaults)
|
||||
|
||||
|
||||
class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, ChangeListDefaultFilter, admin.ModelAdmin):
|
||||
class ExtendedModelAdmin(ChangeViewActionsMixin,
|
||||
ChangeAddFieldsMixin,
|
||||
ChangeListDefaultFilter,
|
||||
EnhaceSearchMixin,
|
||||
admin.ModelAdmin):
|
||||
list_prefetch_related = None
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
|
|
@ -13,6 +13,11 @@ from orchestra.utils.mail import send_email_template
|
|||
from . import settings
|
||||
|
||||
|
||||
class AccountManager(auth.UserManager):
|
||||
def get_main(self):
|
||||
return self.get(pk=settings.ACCOUNTS_MAIN_PK)
|
||||
|
||||
|
||||
class Account(auth.AbstractBaseUser):
|
||||
# Username max_length determined by LINUX system user/group lentgh: 32
|
||||
username = models.CharField(_("username"), max_length=32, unique=True,
|
||||
|
@ -39,7 +44,7 @@ class Account(auth.AbstractBaseUser):
|
|||
"Unselect this instead of deleting accounts."))
|
||||
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
|
||||
|
||||
objects = auth.UserManager()
|
||||
objects = AccountManager()
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
REQUIRED_FIELDS = ['email']
|
||||
|
@ -55,10 +60,6 @@ class Account(auth.AbstractBaseUser):
|
|||
def is_staff(self):
|
||||
return self.is_superuser
|
||||
|
||||
@classmethod
|
||||
def get_main(cls):
|
||||
return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK)
|
||||
|
||||
def save(self, active_systemuser=False, *args, **kwargs):
|
||||
created = not self.pk
|
||||
if not created:
|
||||
|
|
|
@ -17,7 +17,7 @@ def validate_contact(request, bill, error=True):
|
|||
message = msg.format(relation=_("Related"), account=account, url=url)
|
||||
send(request, mark_safe(message))
|
||||
valid = False
|
||||
main = type(bill).account.field.rel.to.get_main()
|
||||
main = type(bill).account.field.rel.to.objects.get_main()
|
||||
if not hasattr(main, 'billcontact'):
|
||||
account = force_text(main)
|
||||
url = reverse('admin:accounts_account_change', args=(main.id,))
|
||||
|
|
|
@ -130,7 +130,7 @@ class Bill(models.Model):
|
|||
|
||||
@cached_property
|
||||
def seller(self):
|
||||
return Account.get_main().billcontact
|
||||
return Account.objects.get_main().billcontact
|
||||
|
||||
@cached_property
|
||||
def buyer(self):
|
||||
|
|
|
@ -111,7 +111,7 @@ class Bind9MasterDomainBackend(ServiceController):
|
|||
from orchestra.contrib.orchestration.manager import router
|
||||
operation = Operation(backend, domain, Operation.SAVE)
|
||||
servers = []
|
||||
for route in router.get_routes(operation):
|
||||
for route in router.objects.get_for_operation(operation):
|
||||
servers.append(route.host.get_ip())
|
||||
return servers
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class BatchDomainCreationAdminForm(forms.ModelForm):
|
|||
if not cleaned_data['account']:
|
||||
account = None
|
||||
for name in [cleaned_data['name']] + self.extra_names:
|
||||
parent = Domain.get_parent_domain(name)
|
||||
parent = Domain.objects.get_parent(name)
|
||||
if not parent:
|
||||
# Fake an account to make django validation happy
|
||||
account_model = self.fields['account']._queryset.model
|
||||
|
|
|
@ -8,6 +8,21 @@ from orchestra.utils.python import AttrDict
|
|||
from . import settings, validators, utils
|
||||
|
||||
|
||||
class DomainQuerySet(models.QuerySet):
|
||||
def get_parent(self, name, top=False):
|
||||
""" get the next domain on the chain """
|
||||
split = name.split('.')
|
||||
parent = None
|
||||
for i in range(1, len(split)-1):
|
||||
name = '.'.join(split[i:])
|
||||
domain = Domain.objects.filter(name=name)
|
||||
if domain:
|
||||
parent = domain.get()
|
||||
if not top:
|
||||
return parent
|
||||
return parent
|
||||
|
||||
|
||||
class Domain(models.Model):
|
||||
name = models.CharField(_("name"), max_length=256, unique=True,
|
||||
help_text=_("Domain or subdomain name."),
|
||||
|
@ -51,23 +66,11 @@ class Domain(models.Model):
|
|||
"servers how long they should keep the data in cache. "
|
||||
"The default value is <tt>%s</tt>.") % settings.DOMAINS_DEFAULT_MIN_TTL)
|
||||
|
||||
objects = DomainQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_parent_domain(cls, name, top=False):
|
||||
""" get the next domain on the chain """
|
||||
split = name.split('.')
|
||||
parent = None
|
||||
for i in range(1, len(split)-1):
|
||||
name = '.'.join(split[i:])
|
||||
domain = Domain.objects.filter(name=name)
|
||||
if domain:
|
||||
parent = domain.get()
|
||||
if not top:
|
||||
return parent
|
||||
return parent
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self.top or self
|
||||
|
@ -122,7 +125,7 @@ class Domain(models.Model):
|
|||
return self.origin.subdomain_set.all().prefetch_related('records')
|
||||
|
||||
def get_parent(self, top=False):
|
||||
return self.get_parent_domain(self.name, top=top)
|
||||
return type(self).objects.get_parent(self.name, top=top)
|
||||
|
||||
def render_zone(self):
|
||||
origin = self.origin
|
||||
|
|
|
@ -31,7 +31,7 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
|
|||
def clean_name(self, attrs, source):
|
||||
""" prevent users creating subdomains of other users domains """
|
||||
name = attrs[source]
|
||||
parent = Domain.get_parent_domain(name)
|
||||
parent = Domain.objects.get_parent(name)
|
||||
if parent and parent.account != self.account:
|
||||
raise ValidationError(_("Can not create subdomains of other users domains"))
|
||||
return attrs
|
||||
|
|
|
@ -4,28 +4,59 @@ from django.core.urlresolvers import reverse, NoReverseMatch
|
|||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.admin.utils import unquote
|
||||
from django.contrib.admin.templatetags.admin_static import static
|
||||
|
||||
from orchestra.admin.utils import admin_link, admin_date
|
||||
|
||||
|
||||
class LogEntryAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'__str__', 'display_action_time', 'user_link',
|
||||
'id', 'display_message', 'display_action_time', 'user_link',
|
||||
)
|
||||
list_filter = (
|
||||
'action_flag',
|
||||
('content_type', admin.RelatedOnlyFieldListFilter),
|
||||
)
|
||||
list_filter = ('action_flag', 'content_type',)
|
||||
date_hierarchy = 'action_time'
|
||||
search_fields = ('object_repr', 'change_message')
|
||||
search_fields = ('object_repr', 'change_message', 'user__username')
|
||||
fields = (
|
||||
'user_link', 'content_object_link', 'display_action_time', 'display_action', 'change_message'
|
||||
'user_link', 'content_object_link', 'display_action_time', 'display_action',
|
||||
'change_message'
|
||||
)
|
||||
readonly_fields = (
|
||||
'user_link', 'content_object_link', 'display_action_time', 'display_action',
|
||||
)
|
||||
actions = None
|
||||
list_select_related = ('user', 'content_type')
|
||||
|
||||
user_link = admin_link('user')
|
||||
display_action_time = admin_date('action_time', short_description=_("Time"))
|
||||
|
||||
def display_message(self, log):
|
||||
edit = '<a href="%(url)s"><img src="%(img)s"></img></a>' % {
|
||||
'url': reverse('admin:admin_logentry_change', args=(log.pk,)),
|
||||
'img': static('admin/img/icon_changelink.gif'),
|
||||
}
|
||||
if log.is_addition():
|
||||
return _('Added "%(link)s". %(edit)s') % {
|
||||
'link': self.content_object_link(log),
|
||||
'edit': edit
|
||||
}
|
||||
elif log.is_change():
|
||||
return _('Changed "%(link)s" - %(changes)s %(edit)s') % {
|
||||
'link': self.content_object_link(log),
|
||||
'changes': log.change_message,
|
||||
'edit': edit,
|
||||
}
|
||||
elif log.is_deletion():
|
||||
return _('Deleted "%(object)s." %(edit)s') % {
|
||||
'object': log.object_repr,
|
||||
'edit': edit,
|
||||
}
|
||||
display_message.short_description = _("Message")
|
||||
display_message.admin_order_field = 'action_flag'
|
||||
display_message.allow_tags = True
|
||||
|
||||
def display_action(self, log):
|
||||
if log.is_addition():
|
||||
return _("Added")
|
||||
|
|
|
@ -92,7 +92,7 @@ class Command(BaseCommand):
|
|||
context = {
|
||||
'servers': ', '.join(servers),
|
||||
}
|
||||
if not confirm("\n\nAre your sure to execute the previous scripts on %(servers)s (yes/no)? " % context)
|
||||
if not confirm("\n\nAre your sure to execute the previous scripts on %(servers)s (yes/no)? " % context):
|
||||
return
|
||||
if not dry:
|
||||
logs = manager.execute(scripts, serialize=serialize, async=True)
|
||||
|
|
|
@ -59,7 +59,7 @@ def generate(operations):
|
|||
for operation in operations:
|
||||
logger.debug("Queued %s" % str(operation))
|
||||
if operation.routes is None:
|
||||
operation.routes = router.get_routes(operation, cache=cache)
|
||||
operation.routes = router.objects.get_for_operation(operation, cache=cache)
|
||||
for route in operation.routes:
|
||||
# TODO key by action.async
|
||||
async_action = route.action_is_async(operation.action)
|
||||
|
@ -196,7 +196,7 @@ def collect(instance, action, **kwargs):
|
|||
continue
|
||||
operation = Operation(backend_cls, selected, iaction)
|
||||
# Only schedule operations if the router has execution routes
|
||||
routes = router.get_routes(operation, cache=route_cache)
|
||||
routes = router.objects.get_for_operation(operation, cache=route_cache)
|
||||
if routes:
|
||||
operation.routes = routes
|
||||
if iaction != Operation.DELETE:
|
||||
|
|
|
@ -144,6 +144,31 @@ class BackendOperation(models.Model):
|
|||
autodiscover_modules('backends')
|
||||
|
||||
|
||||
class RouteQuerySet(models.QuerySet):
|
||||
def get_for_operation(self, operation, **kwargs):
|
||||
cache = kwargs.get('cache', {})
|
||||
if not cache:
|
||||
for route in self.filter(is_active=True).select_related('host'):
|
||||
for action in route.backend_class.get_actions():
|
||||
key = (route.backend, action)
|
||||
try:
|
||||
cache[key].append(route)
|
||||
except KeyError:
|
||||
cache[key] = [route]
|
||||
routes = []
|
||||
backend_cls = operation.backend
|
||||
key = (backend_cls.get_name(), operation.action)
|
||||
try:
|
||||
target_routes = cache[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for route in target_routes:
|
||||
if route.matches(operation.instance):
|
||||
routes.append(route)
|
||||
return routes
|
||||
|
||||
|
||||
class Route(models.Model):
|
||||
"""
|
||||
Defines the routing that determine in which server a backend is executed
|
||||
|
@ -163,6 +188,7 @@ class Route(models.Model):
|
|||
# default=MethodBackend.get_default())
|
||||
is_active = models.BooleanField(_("active"), default=True)
|
||||
|
||||
objects = RouteQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('backend', 'host')
|
||||
|
@ -174,30 +200,6 @@ class Route(models.Model):
|
|||
def backend_class(self):
|
||||
return ServiceBackend.get_backend(self.backend)
|
||||
|
||||
@classmethod
|
||||
def get_routes(cls, operation, **kwargs):
|
||||
cache = kwargs.get('cache', {})
|
||||
if not cache:
|
||||
for route in cls.objects.filter(is_active=True).select_related('host'):
|
||||
for action in route.backend_class.get_actions():
|
||||
key = (route.backend, action)
|
||||
try:
|
||||
cache[key].append(route)
|
||||
except KeyError:
|
||||
cache[key] = [route]
|
||||
routes = []
|
||||
backend_cls = operation.backend
|
||||
key = (backend_cls.get_name(), operation.action)
|
||||
try:
|
||||
target_routes = cache[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for route in target_routes:
|
||||
if route.matches(operation.instance):
|
||||
routes.append(route)
|
||||
return routes
|
||||
|
||||
def clean(self):
|
||||
if not self.match:
|
||||
self.match = 'True'
|
||||
|
|
|
@ -30,12 +30,12 @@ class RouterTests(BaseTestCase):
|
|||
|
||||
route = Route.objects.create(backend=backend, host=self.host, match='True')
|
||||
operation = Operation(backend=TestBackend, instance=route, action='save')
|
||||
self.assertEqual(1, len(Route.get_routes(operation)))
|
||||
self.assertEqual(1, len(Route.objects.get_for_operation(operation)))
|
||||
|
||||
route = Route.objects.create(backend=backend, host=self.host1,
|
||||
match='route.backend == "%s"' % TestBackend.get_name())
|
||||
self.assertEqual(2, len(Route.get_routes(operation)))
|
||||
self.assertEqual(2, len(Route.objects.get_for_operation(operation)))
|
||||
|
||||
route = Route.objects.create(backend=backend, host=self.host2,
|
||||
match='route.backend == "something else"')
|
||||
self.assertEqual(2, len(Route.get_routes(operation)))
|
||||
self.assertEqual(2, len(Route.objects.get_for_operation(operation)))
|
||||
|
|
|
@ -49,7 +49,7 @@ class BilledOrderListFilter(SimpleListFilter):
|
|||
metric_pks = []
|
||||
prefetch_valid_metrics = Prefetch('metrics', to_attr='valid_metrics',
|
||||
queryset=MetricStorage.objects.filter(created_on__gt=F('order__billed_on'),
|
||||
created_on__lte=(F('updated_on')-mindelta))
|
||||
created_on__lte=(F('updated_on')-mindelta)).exclude(value=0)
|
||||
)
|
||||
metric_queryset = queryset.exclude(service__metric='').exclude(billed_on__isnull=True)
|
||||
for order in metric_queryset.prefetch_related(prefetch_valid_metrics):
|
||||
|
@ -61,26 +61,36 @@ class BilledOrderListFilter(SimpleListFilter):
|
|||
break
|
||||
return metric_pks
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
def filter_pending(self, queryset, reverse=False):
|
||||
now = timezone.now()
|
||||
Service = apps.get_model(settings.ORDERS_SERVICE_MODEL)
|
||||
ignore_qs = Q()
|
||||
for order in queryset.distinct('service_id').only('service'):
|
||||
service = order.service
|
||||
delta = service.handler.get_ignore_delta()
|
||||
if delta is not None:
|
||||
ignore_qs = ignore_qs | Q(service_id=service.id, registered_on__gt=now-delta)
|
||||
ignore_qs = queryset.exclude(ignore_qs)
|
||||
pending_qs = Q(
|
||||
Q(pk__in=self.get_pending_metric_pks(ignore_qs)) |
|
||||
Q(billed_until__isnull=True) | Q(~Q(service__billing_period=Service.NEVER) &
|
||||
Q(billed_until__lt=now))
|
||||
)
|
||||
if reverse:
|
||||
return queryset.exclude(pending_qs)
|
||||
else:
|
||||
return ignore_qs.filter(pending_qs)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
now = timezone.now()
|
||||
if self.value() == 'yes':
|
||||
return queryset.filter(billed_until__isnull=False, billed_until__gte=timezone.now())
|
||||
return queryset.filter(billed_until__isnull=False, billed_until__gte=now)
|
||||
elif self.value() == 'no':
|
||||
return queryset.exclude(billed_until__isnull=False, billed_until__gte=timezone.now())
|
||||
return queryset.exclude(billed_until__isnull=False, billed_until__gte=now)
|
||||
elif self.value() == 'pending':
|
||||
return queryset.filter(
|
||||
Q(pk__in=self.get_pending_metric_pks(queryset)) | Q(
|
||||
Q(billed_until__isnull=True) | Q(~Q(service__billing_period=Service.NEVER) &
|
||||
Q(billed_until__lt=timezone.now()))
|
||||
)
|
||||
)
|
||||
return self.filter_pending(queryset)
|
||||
elif self.value() == 'not_pending':
|
||||
return queryset.exclude(
|
||||
Q(pk__in=self.get_pending_metric_pks(queryset)) | Q(
|
||||
Q(billed_until__isnull=True) | Q(~Q(service__billing_period=Service.NEVER) &
|
||||
Q(billed_until__lt=timezone.now()))
|
||||
)
|
||||
)
|
||||
return self.filter_pending(queryset, reverse=True)
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.db.models import F, Q, Sum
|
|||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -103,6 +104,49 @@ class OrderQuerySet(models.QuerySet):
|
|||
def inactive(self, **kwargs):
|
||||
""" return inactive orders """
|
||||
return self.filter(cancelled_on__lte=timezone.now(), **kwargs)
|
||||
|
||||
def update_by_instance(self, instance, service=None, commit=True):
|
||||
updates = []
|
||||
if service is None:
|
||||
Service = apps.get_model(settings.ORDERS_SERVICE_MODEL)
|
||||
services = Service.objects.filter_by_instance(instance)
|
||||
else:
|
||||
services = [service]
|
||||
for service in services:
|
||||
orders = Order.objects.by_object(instance, service=service)
|
||||
orders = orders.select_related('service').active()
|
||||
if service.handler.matches(instance):
|
||||
if not orders:
|
||||
account_id = getattr(instance, 'account_id', instance.pk)
|
||||
if account_id is None:
|
||||
# New account workaround -> user.account_id == None
|
||||
continue
|
||||
ignore = service.handler.get_ignore(instance)
|
||||
order = self.model(
|
||||
content_object=instance,
|
||||
content_object_repr=str(instance),
|
||||
service=service,
|
||||
account_id=account_id,
|
||||
ignore=ignore)
|
||||
if commit:
|
||||
order.save()
|
||||
updates.append((order, 'created'))
|
||||
logger.info("CREATED new order id: {id}".format(id=order.id))
|
||||
else:
|
||||
if len(orders) > 1:
|
||||
raise ValueError("A single active order was expected.")
|
||||
order = orders[0]
|
||||
updates.append((order, 'updated'))
|
||||
if commit:
|
||||
order.update()
|
||||
elif orders:
|
||||
if len(orders) > 1:
|
||||
raise ValueError("A single active order was expected.")
|
||||
order = orders[0]
|
||||
order.cancel(commit=commit)
|
||||
logger.info("CANCELLED order id: {id}".format(id=order.id))
|
||||
updates.append((order, 'cancelled'))
|
||||
return updates
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
|
@ -132,54 +176,16 @@ class Order(models.Model):
|
|||
def __str__(self):
|
||||
return str(self.service)
|
||||
|
||||
@classmethod
|
||||
def update_orders(cls, instance, service=None, commit=True):
|
||||
updates = []
|
||||
if service is None:
|
||||
Service = apps.get_model(settings.ORDERS_SERVICE_MODEL)
|
||||
services = Service.get_services(instance)
|
||||
else:
|
||||
services = [service]
|
||||
for service in services:
|
||||
orders = Order.objects.by_object(instance, service=service)
|
||||
orders = orders.select_related('service').active()
|
||||
if service.handler.matches(instance):
|
||||
if not orders:
|
||||
account_id = getattr(instance, 'account_id', instance.pk)
|
||||
if account_id is None:
|
||||
# New account workaround -> user.account_id == None
|
||||
continue
|
||||
ignore = service.handler.get_ignore(instance)
|
||||
order = cls(
|
||||
content_object=instance,
|
||||
content_object_repr=str(instance),
|
||||
service=service,
|
||||
account_id=account_id,
|
||||
ignore=ignore)
|
||||
if commit:
|
||||
order.save()
|
||||
updates.append((order, 'created'))
|
||||
logger.info("CREATED new order id: {id}".format(id=order.id))
|
||||
else:
|
||||
if len(orders) > 1:
|
||||
raise ValueError("A single active order was expected.")
|
||||
order = orders[0]
|
||||
updates.append((order, 'updated'))
|
||||
if commit:
|
||||
order.update()
|
||||
elif orders:
|
||||
if len(orders) > 1:
|
||||
raise ValueError("A single active order was expected.")
|
||||
order = orders[0]
|
||||
order.cancel(commit=commit)
|
||||
logger.info("CANCELLED order id: {id}".format(id=order.id))
|
||||
updates.append((order, 'cancelled'))
|
||||
return updates
|
||||
|
||||
@classmethod
|
||||
def get_bill_backend(cls):
|
||||
return import_class(settings.ORDERS_BILLING_BACKEND)()
|
||||
|
||||
def clean(self):
|
||||
if self.billed_on < self.registered_on:
|
||||
raise ValidationError(_("Billed date can not be earlier than registered on."))
|
||||
if self.billed_until and not self.billed_on:
|
||||
raise ValidationError(_("Billed on is missing while billed until is being provided."))
|
||||
|
||||
def update(self):
|
||||
instance = self.content_object
|
||||
if instance is None:
|
||||
|
@ -189,7 +195,7 @@ class Order(models.Model):
|
|||
if handler.metric:
|
||||
metric = handler.get_metric(instance)
|
||||
if metric is not None:
|
||||
MetricStorage.store(self, metric)
|
||||
MetricStorage.objects.store(self, metric)
|
||||
metric = ', metric:{}'.format(metric)
|
||||
description = handler.get_order_description(instance)
|
||||
logger.info("UPDATED order id:{id}, description:{description}{metric}".format(
|
||||
|
@ -229,6 +235,8 @@ class Order(models.Model):
|
|||
for metric in self.metrics.filter(created_on__lt=end).order_by('id'):
|
||||
created = metric.created_on
|
||||
if created > ini:
|
||||
if prev is None:
|
||||
raise ValueError("Metric storage information is inconsistent.")
|
||||
cini = prev.created_on
|
||||
if not result:
|
||||
cini = ini
|
||||
|
@ -259,27 +267,13 @@ class Order(models.Model):
|
|||
return decimal.Decimal(0)
|
||||
|
||||
|
||||
class MetricStorage(models.Model):
|
||||
""" Stores metric state for future billing """
|
||||
order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics')
|
||||
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||
# TODO time field?
|
||||
updated_on = models.DateTimeField(_("updated"))
|
||||
|
||||
class Meta:
|
||||
get_latest_by = 'id'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.order)
|
||||
|
||||
@classmethod
|
||||
def store(cls, order, value):
|
||||
class MetricStorageQuerySet(models.QuerySet):
|
||||
def store(self, order, value):
|
||||
now = timezone.now()
|
||||
try:
|
||||
last = cls.objects.filter(order=order).latest()
|
||||
except cls.DoesNotExist:
|
||||
cls.objects.create(order=order, value=value, updated_on=now)
|
||||
last = self.filter(order=order).latest()
|
||||
except self.model.DoesNotExist:
|
||||
self.create(order=order, value=value, updated_on=now)
|
||||
else:
|
||||
# Metric storage has per-day granularity (last value of the day is what counts)
|
||||
if last.created_on == now.date():
|
||||
|
@ -289,7 +283,24 @@ class MetricStorage(models.Model):
|
|||
else:
|
||||
error = decimal.Decimal(str(settings.ORDERS_METRIC_ERROR))
|
||||
if value > last.value+error or value < last.value-error:
|
||||
cls.objects.create(order=order, value=value, updated_on=now)
|
||||
self.create(order=order, value=value, updated_on=now)
|
||||
else:
|
||||
last.updated_on = now
|
||||
last.save(update_fields=['updated_on'])
|
||||
|
||||
|
||||
class MetricStorage(models.Model):
|
||||
""" Stores metric state for future billing """
|
||||
order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics')
|
||||
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
||||
created_on = models.DateField(_("created"), auto_now_add=True)
|
||||
# TODO time field?
|
||||
updated_on = models.DateTimeField(_("updated"))
|
||||
|
||||
objects = MetricStorageQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
get_latest_by = 'id'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.order)
|
||||
|
|
|
@ -32,8 +32,8 @@ def update_orders(sender, **kwargs):
|
|||
if sender._meta.app_label not in settings.ORDERS_EXCLUDED_APPS:
|
||||
instance = kwargs['instance']
|
||||
if type(instance) in services:
|
||||
Order.update_orders(instance)
|
||||
Order.objects.update_by_instance(instance)
|
||||
elif not hasattr(instance, 'account'):
|
||||
related = helpers.get_related_object(instance)
|
||||
if related and related != instance:
|
||||
Order.update_orders(related)
|
||||
Order.objects.update_by_instance(related)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n l10n staticfiles admin_urls utils %}
|
||||
{% load i18n l10n staticfiles admin_urls utils orders %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
|
@ -50,7 +50,7 @@ $(document).ready( function () {
|
|||
{% if not lines %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>{% trans 'Nothing to bill' %}</th></tr>
|
||||
<tr><th>{% trans 'Nothing to bill, all lines have size×quantity 0.' %}</th></tr>
|
||||
</thead>
|
||||
</table>
|
||||
{% else %}
|
||||
|
@ -67,7 +67,7 @@ $(document).ready( function () {
|
|||
<br> Discount per {{ discount.type }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{{ line.ini | date }} to {{ line.end | date }}</td>
|
||||
<td>{{ line | periodformat }}</td>
|
||||
<td>{{ line.size | floatformat:"-2" }}×{{ line.metric | floatformat:"-2"}}</td>
|
||||
<td>
|
||||
{{ line.subtotal | floatformat:"-2" }} €
|
||||
|
|
19
orchestra/contrib/orders/templatetags/orders.py
Normal file
19
orchestra/contrib/orders/templatetags/orders.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import datetime
|
||||
|
||||
from django import template
|
||||
from django.template.defaultfilters import date
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def periodformat(line):
|
||||
if line.ini == line.end:
|
||||
return date(line.ini)
|
||||
if line.ini.day == 1 and line.end.day == 1:
|
||||
end = line.end - datetime.timedelta(days=1)
|
||||
if line.ini.month == end.month:
|
||||
return date(line.ini, "N Y")
|
||||
return '%s to %s' % (date(line.ini, "N Y"), date(end, "N Y"))
|
||||
return '%s to %s' % (date(line.ini), date(line.end))
|
|
@ -164,6 +164,23 @@ class Resource(models.Model):
|
|||
return tasks.monitor(self.pk)
|
||||
|
||||
|
||||
class ResourceDataQuerySet(models.QuerySet):
|
||||
def get_or_create(self, obj, resource):
|
||||
ct = ContentType.objects.get_for_model(type(obj))
|
||||
try:
|
||||
return self.get(
|
||||
content_type=ct,
|
||||
object_id=obj.pk,
|
||||
resource=resource
|
||||
), False
|
||||
except self.model.DoesNotExist:
|
||||
return self.create(
|
||||
content_object=obj,
|
||||
resource=resource,
|
||||
allocated=resource.default_allocation
|
||||
), True
|
||||
|
||||
|
||||
class ResourceData(models.Model):
|
||||
""" Stores computed resource usage and allocation """
|
||||
resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
|
||||
|
@ -177,6 +194,7 @@ class ResourceData(models.Model):
|
|||
editable=False)
|
||||
|
||||
content_object = GenericForeignKey()
|
||||
objects = ResourceDataQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('resource', 'content_type', 'object_id')
|
||||
|
@ -185,22 +203,6 @@ class ResourceData(models.Model):
|
|||
def __str__(self):
|
||||
return "%s: %s" % (str(self.resource), str(self.content_object))
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, obj, resource):
|
||||
ct = ContentType.objects.get_for_model(type(obj))
|
||||
try:
|
||||
return cls.objects.get(
|
||||
content_type=ct,
|
||||
object_id=obj.pk,
|
||||
resource=resource
|
||||
), False
|
||||
except cls.DoesNotExist:
|
||||
return cls.objects.create(
|
||||
content_object=obj,
|
||||
resource=resource,
|
||||
allocated=resource.default_allocation
|
||||
), True
|
||||
|
||||
@property
|
||||
def unit(self):
|
||||
return self.resource.unit
|
||||
|
|
|
@ -43,7 +43,7 @@ def monitor(resource_id, ids=None):
|
|||
triggers = []
|
||||
model = resource.content_type.model_class()
|
||||
for obj in model.objects.filter(**kwargs):
|
||||
data, __ = ResourceData.get_or_create(obj, resource)
|
||||
data, __ = ResourceData.objects.get_or_create(obj, resource)
|
||||
data.update()
|
||||
if not resource.disable_trigger:
|
||||
a = data.used
|
||||
|
|
|
@ -57,7 +57,8 @@ class PHPListService(SoftwareService):
|
|||
return settings.SAAS_PHPLIST_DB_USER
|
||||
|
||||
def get_account(self):
|
||||
return self.instance.account.get_main()
|
||||
account_model = self.instance._meta.get_field_by_name('account')[0]
|
||||
return account_model.objects.get_main()
|
||||
|
||||
def validate(self):
|
||||
super(PHPListService, self).validate()
|
||||
|
|
|
@ -247,7 +247,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
|||
|
||||
def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False):
|
||||
"""
|
||||
discounts: already applied discounts on price
|
||||
discounts: extra discounts to apply
|
||||
computed: price = price*size already performed
|
||||
"""
|
||||
if len(dates) == 2:
|
||||
|
@ -271,14 +271,17 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
|||
'metric': metric,
|
||||
'discounts': [],
|
||||
})
|
||||
discounted = 0
|
||||
for dtype, dprice in discounts:
|
||||
self.generate_discount(line, dtype, dprice)
|
||||
discounted += dprice
|
||||
# TODO this is needed for all discounts?
|
||||
subtotal += discounted
|
||||
if subtotal > price:
|
||||
self.generate_discount(line, self._PLAN, price-subtotal)
|
||||
plan_discount = price-subtotal
|
||||
self.generate_discount(line, self._PLAN, plan_discount)
|
||||
subtotal += plan_discount
|
||||
for dtype, dprice in discounts:
|
||||
subtotal += dprice
|
||||
# Prevent compensations to refund money
|
||||
if dtype == self._COMPENSATION and subtotal < 0:
|
||||
dprice -= subtotal
|
||||
if dprice:
|
||||
self.generate_discount(line, dtype, dprice)
|
||||
return line
|
||||
|
||||
def assign_compensations(self, givers, receivers, **options):
|
||||
|
@ -318,7 +321,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
|||
cend = comp.end
|
||||
if only_beyond:
|
||||
cini = beyond
|
||||
elif not only_beyond:
|
||||
elif only_beyond:
|
||||
continue
|
||||
dsize += self.get_price_size(cini, cend)
|
||||
# Extend billing point a little bit to benefit from a substantial discount
|
||||
|
@ -359,8 +362,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
|||
if intersect:
|
||||
csize += self.get_price_size(intersect.ini, intersect.end)
|
||||
price = self.get_price(account, metric, position=position, rates=rates)
|
||||
price = price * size
|
||||
cprice = price * csize
|
||||
price = price * size
|
||||
if order in priced:
|
||||
priced[order][0] += price
|
||||
priced[order][1] += cprice
|
||||
|
@ -368,23 +371,25 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
|||
priced[order] = (price, cprice)
|
||||
lines = []
|
||||
for order, prices in priced.items():
|
||||
discounts = ()
|
||||
# Generate lines and discounts from order.nominal_price
|
||||
price, cprice = prices
|
||||
# Compensations > new_billed_until
|
||||
dsize, new_end = self.apply_compensations(order, only_beyond=True)
|
||||
cprice += dsize*price
|
||||
if cprice:
|
||||
discounts = (
|
||||
(self._COMPENSATION, -cprice),
|
||||
)
|
||||
if new_end:
|
||||
size = self.get_price_size(order.new_billed_until, new_end)
|
||||
price += price*size
|
||||
order.new_billed_until = new_end
|
||||
line = self.generate_line(
|
||||
order, price, ini, new_end or end, discounts=discounts, computed=True)
|
||||
lines.append(line)
|
||||
if hasattr(order, 'new_billed_until'):
|
||||
discounts = ()
|
||||
# Generate lines and discounts from order.nominal_price
|
||||
price, cprice = prices
|
||||
a = order.id
|
||||
# Compensations > new_billed_until
|
||||
dsize, new_end = self.apply_compensations(order, only_beyond=True)
|
||||
cprice += dsize*price
|
||||
if cprice:
|
||||
discounts = (
|
||||
(self._COMPENSATION, -cprice),
|
||||
)
|
||||
if new_end:
|
||||
size = self.get_price_size(order.new_billed_until, new_end)
|
||||
price += price*size
|
||||
order.new_billed_until = new_end
|
||||
line = self.generate_line(
|
||||
order, price, ini, new_end or end, discounts=discounts, computed=True)
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
def bill_registered_or_renew_events(self, account, porders, rates):
|
||||
|
@ -503,7 +508,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
|||
recharges = []
|
||||
rini = order.billed_on
|
||||
rend = min(bp, order.billed_until)
|
||||
bmetric = order.billed_metric
|
||||
bmetric = order.billed_metric or 0
|
||||
bsize = self.get_price_size(rini, order.billed_until)
|
||||
prepay_discount = self.get_price(account, bmetric) * bsize
|
||||
prepay_discount = round(prepay_discount, 2)
|
||||
|
|
|
@ -19,6 +19,18 @@ autodiscover_modules('handlers')
|
|||
rate_class = import_class(settings.SERVICES_RATE_CLASS)
|
||||
|
||||
|
||||
class ServiceQuerySet(models.QuerySet):
|
||||
def filter_by_instance(self, instance):
|
||||
cache = caches.get_request_cache()
|
||||
ct = ContentType.objects.get_for_model(instance)
|
||||
key = 'services.Service-%i' % ct.pk
|
||||
services = cache.get(key)
|
||||
if services is None:
|
||||
services = self.filter(content_type=ct, is_active=True)
|
||||
cache.set(key, services)
|
||||
return services
|
||||
|
||||
|
||||
class Service(models.Model):
|
||||
NEVER = ''
|
||||
# DAILY = 'DAILY'
|
||||
|
@ -152,20 +164,11 @@ class Service(models.Model):
|
|||
),
|
||||
default=PREPAY)
|
||||
|
||||
objects = ServiceQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.description
|
||||
|
||||
@classmethod
|
||||
def get_services(cls, instance):
|
||||
cache = caches.get_request_cache()
|
||||
ct = ContentType.objects.get_for_model(instance)
|
||||
key = 'services.Service-%i' % ct.pk
|
||||
services = cache.get(key)
|
||||
if services is None:
|
||||
services = cls.objects.filter(content_type=ct, is_active=True)
|
||||
cache.set(key, services)
|
||||
return services
|
||||
|
||||
@cached_property
|
||||
def handler(self):
|
||||
""" Accessor of this service handler instance """
|
||||
|
@ -251,5 +254,5 @@ class Service(models.Model):
|
|||
if related_model._meta.model_name != 'account':
|
||||
queryset = queryset.select_related('account').all()
|
||||
for instance in queryset:
|
||||
updates += order_model.update_orders(instance, service=self, commit=commit)
|
||||
updates += order_model.objects.update_by_instance(instance, service=self, commit=commit)
|
||||
return updates
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
metric=&
|
||||
nominal_price=28.10&
|
||||
tax=21&
|
||||
pricing_period=BILLING_PERIOD&
|
||||
pricing_period=NEVER&
|
||||
rate_algorithm=orchestra.contrib.plans.ratings.step_price&
|
||||
on_cancel=COMPENSATE&
|
||||
payment_style=PREPAY">Mailbox</option>
|
||||
|
|
|
@ -67,7 +67,7 @@ class MailboxBillingTest(BaseTestCase):
|
|||
return self.resource
|
||||
|
||||
def allocate_disk(self, mailbox, value):
|
||||
data, __ = ResourceData.get_or_create(mailbox, self.resource)
|
||||
data, __ = ResourceData.objects.get_or_create(mailbox, self.resource)
|
||||
data.allocated = value
|
||||
data.save()
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ class BaseTrafficBillingTest(BaseTestCase):
|
|||
|
||||
def report_traffic(self, account, value):
|
||||
MonitorData.objects.create(monitor=FTPTrafficMonitor.get_name(), content_object=account.systemusers.get(), value=value)
|
||||
data, __ = ResourceData.get_or_create(account, self.resource)
|
||||
data, __ = ResourceData.objects.get_or_create(account, self.resource)
|
||||
data.update()
|
||||
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import collections
|
|||
import random
|
||||
import string
|
||||
from io import StringIO
|
||||
from itertools import tee
|
||||
|
||||
|
||||
def import_class(cls):
|
||||
|
@ -118,3 +119,9 @@ def cmp_to_key(mycmp):
|
|||
return mycmp(self.obj, other.obj) != 0
|
||||
return K
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
|
||||
a, b = tee(iterable)
|
||||
next(b, None)
|
||||
return zip(a, b)
|
||||
|
|
Loading…
Reference in a new issue